目前探究OC上的if和switch,其它语言我猜也是差不多的。
if的汇编
首先写下简单的if代码:
- (void)viewDidLoad {
[super viewDidLoad];
NSNumber *num = @(2);
if (num.integerValue == 1) {
NSLog(@"num = 1");
} else if(num.integerValue == 2) {
NSLog(@"num = 2");
}
}
这里先用NSNumber的原因是防止被编译器优化掉,当然也可以再build setting里面设置防止编译器优化。
其汇编代码是这样的:
0x102b167b8 <+136>: cmp x0, #0x1 ; =0x1
0x102b167bc <+140>: b.ne 0x102b167d0 ; <+160> at ViewController.m
0x102b167c0 <+144>: adrp x0, 2
0x102b167c4 <+148>: add x0, x0, #0x60 ; =0x60
0x102b167c8 <+152>: bl 0x102b16b2c ; symbol stub for: NSLog
0x102b167cc <+156>: b 0x102b16800 ; <+208> at ViewController.m:25:1
0x102b167d0 <+160>: adrp x8, 2
0x102b167d4 <+164>: add x8, x8, #0xcc0 ; =0xcc0
0x102b167d8 <+168>: ldr x9, [sp, #0x8]
0x102b167dc <+172>: ldr x1, [x8]
0x102b167e0 <+176>: mov x0, x9
0x102b167e4 <+180>: bl 0x102b16b68 ; symbol stub for: objc_msgSend
0x102b167e8 <+184>: cmp x0, #0x2 ; =0x2
0x102b167ec <+188>: b.ne 0x102b167fc ; <+204> at ViewController.m
0x102b167f0 <+192>: adrp x0, 2
0x102b167f4 <+196>: add x0, x0, #0x80 ; =0x80
0x102b167f8 <+200>: bl 0x102b16b2c ; symbol stub for: NSLog
仔细看其实很好理解,在0x102b167b8
行里让x0(num.integerValue)
跟1比较,通过结果的不同决定后面跳转到哪个地方执行。
接下来再看下switch的表现情况:
switch的汇编代码(只有两个case)
- (void)viewDidLoad {
[super viewDidLoad];
NSNumber *num = @(2);
NSInteger i = num.integerValue;
switch (i) {
case 1:
NSLog(@"num = 1");
break;
case 2:
NSLog(@"num = 2");
break;
}
}
汇编代码如下:
0x10010e7d0 <+140>: ldr x8, [sp, #0x18]
0x10010e7d4 <+144>: subs x9, x8, #0x2 ; =0x2
0x10010e7d8 <+148>: str x9, [sp, #0x8]
0x10010e7dc <+152>: b.eq 0x10010e7f4 ; <+176> at ViewController.m:27:13
0x10010e7e0 <+156>: b 0x10010e800 ; <+188> at ViewController.m:30:1
0x10010e7e4 <+160>: adrp x0, 2
0x10010e7e8 <+164>: add x0, x0, #0x60 ; =0x60
0x10010e7ec <+168>: bl 0x10010eb2c ; symbol stub for: NSLog
0x10010e7f0 <+172>: b 0x10010e800 ; <+188> at ViewController.m:30:1
-> 0x10010e7f4 <+176>: adrp x0, 2
0x10010e7f8 <+180>: add x0, x0, #0x80 ; =0x80
0x10010e7fc <+184>: bl 0x10010eb2c ; symbol stub for: NSLog
上面代码中的0x10010e7d0
里面的x8
就代表着i
,下一行的x9
就代表i-2
了,注意这行subs
这个命令跟cmp
一样也会影响到标志寄存器的(add
这类操作命令都会),因此0x10010e7dc
是通过i-2是否等于0来判断到底执行哪一段汇编代码。看上去跟if
并没有太大区别。
switch的汇编代码(多于3个case)
下面试试将switch的case数增多,另外也让这些case变得不连续:
- (void)viewDidLoad {
[super viewDidLoad];
NSNumber *num = @(3);
NSInteger i = num.integerValue;
switch (i) {
case 1:
NSLog(@"num = 1");
break;
case 3:
NSLog(@"num = 3");
break;
case 4:
NSLog(@"num = 4");
break;
case 7:
NSLog(@"num = 7");
break;
}
}
汇编代码跟刚才变得不一样了,从switch开始代码如下:
0x1047be768 <+116>: subs x0, x0, #0x1 ; =0x1
0x1047be76c <+120>: mov x1, x0
0x1047be770 <+124>: subs x0, x0, #0x6 ; =0x6
0x1047be774 <+128>: str x1, [sp, #0x8]
0x1047be778 <+132>: str x0, [sp]
0x1047be77c <+136>: b.hi 0x1047be7d4 ; <+224> at ViewController.m:38:1
0x1047be780 <+140>: adrp x8, 0
0x1047be784 <+144>: add x8, x8, #0x7f4 ; =0x7f4
0x1047be788 <+148>: ldr x9, [sp, #0x8]
0x1047be78c <+152>: ldrsw x10, [x8, x9, lsl #2]
0x1047be790 <+156>: add x8, x10, x8
0x1047be794 <+160>: br x8
0x1047be798 <+164>: adrp x0, 2
0x1047be79c <+168>: add x0, x0, #0x60 ; =0x60
0x1047be7a0 <+172>: bl 0x1047beb1c ; symbol stub for: NSLog
0x1047be7a4 <+176>: b 0x1047be7d4 ; <+224> at ViewController.m:38:1
0x1047be7a8 <+180>: adrp x0, 2
0x1047be7ac <+184>: add x0, x0, #0x80 ; =0x80
0x1047be7b0 <+188>: bl 0x1047beb1c ; symbol stub for: NSLog
0x1047be7b4 <+192>: b 0x1047be7d4 ; <+224> at ViewController.m:38:1
0x1047be7b8 <+196>: adrp x0, 2
0x1047be7bc <+200>: add x0, x0, #0xa0 ; =0xa0
0x1047be7c0 <+204>: bl 0x1047beb1c ; symbol stub for: NSLog
0x1047be7c4 <+208>: b 0x1047be7d4 ; <+224> at ViewController.m:38:1
0x1047be7c8 <+212>: adrp x0, 2
0x1047be7cc <+216>: add x0, x0, #0xc0 ; =0xc0
0x1047be7d0 <+220>: bl 0x1047beb1c ; symbol stub for: NSLog
0x1047be7d4 <+224>: add x8, sp, #0x18
0x1047be768
中,首先让x0
减去1
,这里的x0
就是i
,这里就是i-1
的意思,注意到代码中的case是从1
开始,其实这个1
就是这个意思。从0x1047be770
上看,x0
又减去了6
,x0
总共减了7
,而代码中最大的是7
,如果都命中看这些case,最后x0就是负数,而0x1047be77c
中的b.hi
是无符号大于的意思,在0x1047be770
的sub
操作中如果不产生借位操作就会触发跳转。下面分三种情况分析一下:
-
x0
小于1
,在0x1047be768
的时候,x0
已经变成了负数,由于这是无符号的,所以x0
变成了特别大的数,不会在0x1047be770
的运算中产生借位。 -
x0
大于7
,在0x1047be770
中,x0
都不会产生借位 -
x0
在1
到7
之间,运算产生产生借位。
前面两种都会在0x1047be77c
中触发跳转,直接跳到0x1047be7d4
跳出switch
。
接下来再看看命中条件后的代码:
0x1047be780
中的adrp
指的是让当前代码执行的地址出入后12位,再让最后的数左移12为相加后赋值给x8,比如这句的意思是:
x8 = 0x1047be780&0xfffff000+0<12 = 0x1047be000
结合0x1047be784
,x8
就变成了0x1047be7f4
通过xcode中的debug->debug workflow->view memory查看0x1047be7f4的内存如下:
A4 FF FF FF
E0 FF FF FF
B4 FF FF FF
C4 FF FF FF
E0 FF FF FF
E0 FF FF FF
D4 FF FF FF
可以看出这里存的头7个数都是负数,注意到第二行,第五行和第六行都是一样的,分别对应switch里面没有命中case的数字2,5,6。
0x1047be788
里面的x9
从[sp, #0x8]
里面拿出值,而这个值在0x1047be774
里面存过,其实就是x1
,而这个x1
是x0-1
获得的,也就是i-1
。
0x1047be78c
这一行代表从x8
偏移x9
左移2位的地址里面取值放到x10
,左移2位就是乘以4的意思,也就是x8
偏移x9
*4个字节。比如这个case是3,我们拿到的就是FFFFFFB4
(注意这是小端模式下,内存的数字得从右往左读)
0x1047be790
和0x1047be794
说明将要跳x8
这个地址,而x8
则是x10
+x8
,其实就是x8
偏移了这个x10
的地址,先打印下x10
的int类型是:
p (int)0xffffffffffffffb4
(int) $4 = -76
x8
是0x1047be7f4
,减去76(注意是10进制),得到0x1047be7a8
,就是跳转的地址了。
再看下如果switch中没有匹配的数字,即i = 2,5,6的时候,x10
就等于FFFFFFE0
,打印一下这个值的int类型是:
p (int)0xFFFFFFE0
(int) $5 = -32
用x8
减去32就得到0x1047be7d4
,跟i>7或者i<1跳转的地方是一样的。
switch的汇编代码(多于3个case,case跨度特别大的情况)
下面改造一下代码,使得case间的跨度增加
- (void)viewDidLoad {
[super viewDidLoad];
NSNumber *num = @(3);
NSInteger i = num.integerValue;
switch (i) {
case 1:
NSLog(@"num = 1");
break;
case 3:
NSLog(@"num = 3");
break;
case 4:
NSLog(@"num = 4");
break;
case 41:
NSLog(@"num = 41");
break;
}
}
汇编代码如下:
0x104306764 <+120>: subs x0, x0, #0x1 ; =0x1
0x104306768 <+124>: str x1, [sp, #0x28]
0x10430676c <+128>: str x0, [sp, #0x20]
0x104306770 <+132>: b.eq 0x1043067b4 ; <+200> at ViewController.m:23:13
0x104306774 <+136>: b 0x104306778 ; <+140> at ViewController.m:21:5
0x104306778 <+140>: ldr x8, [sp, #0x28]
0x10430677c <+144>: subs x9, x8, #0x3 ; =0x3
0x104306780 <+148>: str x9, [sp, #0x18]
0x104306784 <+152>: b.eq 0x1043067c4 ; <+216> at ViewController.m:27:13
0x104306788 <+156>: b 0x10430678c ; <+160> at ViewController.m:21:5
0x10430678c <+160>: ldr x8, [sp, #0x28]
0x104306790 <+164>: subs x9, x8, #0x4 ; =0x4
0x104306794 <+168>: str x9, [sp, #0x10]
0x104306798 <+172>: b.eq 0x1043067d4 ; <+232> at ViewController.m:31:13
0x10430679c <+176>: b 0x1043067a0 ; <+180> at ViewController.m:21:5
0x1043067a0 <+180>: ldr x8, [sp, #0x28]
0x1043067a4 <+184>: subs x9, x8, #0x29 ; =0x29
0x1043067a8 <+188>: str x9, [sp, #0x8]
0x1043067ac <+192>: b.eq 0x1043067e4 ; <+248> at ViewController.m:34:13
0x1043067b0 <+196>: b 0x1043067f0 ; <+260> at ViewController.m:38:1
0x1043067b4 <+200>: adrp x0, 2
0x1043067b8 <+204>: add x0, x0, #0x60 ; =0x60
0x1043067bc <+208>: bl 0x104306b1c ; symbol stub for: NSLog
0x1043067c0 <+212>: b 0x1043067f0 ; <+260> at ViewController.m:38:1
0x1043067c4 <+216>: adrp x0, 2
0x1043067c8 <+220>: add x0, x0, #0x80 ; =0x80
0x1043067cc <+224>: bl 0x104306b1c ; symbol stub for: NSLog
0x1043067d0 <+228>: b 0x1043067f0 ; <+260> at ViewController.m:38:1
0x1043067d4 <+232>: adrp x0, 2
0x1043067d8 <+236>: add x0, x0, #0xa0 ; =0xa0
0x1043067dc <+240>: bl 0x104306b1c ; symbol stub for: NSLog
0x1043067e0 <+244>: b 0x1043067f0 ; <+260> at ViewController.m:38:1
0x1043067e4 <+248>: adrp x0, 2
0x1043067e8 <+252>: add x0, x0, #0xc0 ; =0xc0
0x1043067ec <+256>: bl 0x104306b1c ; symbol stub for: NSLog
0x1043067f0 <+260>: sub x8, x29, #0x28 ; =0x28
可以看出,基本变回了跟if差不多的汇编,这个测试过在跨度大于等于40的时候就是这样。
总结
switch语法在case超过3个且跨度少于40的时候,汇编会采用空间换时间的方式,提供一个表以作跳转的偏移量查询。在oc中switch通常和枚举一起使用,通常会满足这个条件。这种时候使用switch会稍微提升速度和稍稍增加包大小。不过这个优化比较微小,不算太必要。
网友评论