iOS HackeriOS开发IOS个人开发

我应该使用propetry还是_ivar?

2015-12-08  本文已影响571人  tongxyj

原文地址:https://www.bignerdranch.com/blog/should-i-use-a-property-or-an-instance-variable/
如原作者发现有侵权行为可责令我在24小时之内删除,前提是你能看到。

我们团队中的一个实习生问我一个看起来没什么大碍的问题:“_ivar和property有人更愿意使用后者这个问题你怎么看的?”在Big Nerd Ranch公司,访问一个成员变量时,我们更愿意使用property,但是在OC论坛里,你会找到很多程序猿对这个问题各种各样的回答:

使用property有一些明显的优势,特别是你在断点调试模式:当你要访问的变量发生改变时,property提供了一块单独的区域可以让你打断点,你可以重写set或get方法在其中打印日志或者做一些功能性的工作等等.在众多的答案中很多程序猿偏爱_ivars的原因之一是担心使用property所带来的性能开销。我们认为对于大多数应用来说这种开销是微不足道的,虽然如斯我们还是很乐意证明这个观点。而开发一个能够明显对比出property和_ivar两者的区别的app是有可能的吗?(答案是肯定的.)那么在使用property的setter和getter发送消息时到底有多少性能开销呢?(暗示:这恐怕是以毫秒级的时间来衡量的)
测试property的性能开销

PropertyPerformance project on Github
我们并不需要非常精密的进行一个有关property的压力测试。这个app运行了一个没有多大意义的但是开销很大的运算,它尽可能快的去调用setNeedsDisplay方法,它想看看iOS系统重绘屏幕的极限(每秒120frames)到底有多快。这个运算是一个进行浮点数加法的循环,所以我们可以估算出系统在进行这样的循环时为了适应这种数量级的迭代所消耗的性能。
开始,让我们假设有两种不同的循环:一个通过property的getter来访问成员变量,另一种直接操作_ivar.


 // Inner loop using properties.
 for (NSUInteger i = 0; i < loopSize; i++) {
     x += self.propertyValue;
 }
 
 // Inner loop using instance variables.
 for (NSUInteger i = 0; i < loopSize; i++) {
     x += \_ivarValue;
 }

下面的图表描述了在iPhone5上该app的FPS性能:


当循环的数量级很高时,在iOS5中,使用property方式的循环要比使用_ivar方式慢5倍而在iOS7中property要比_ivar慢3.5呗。让我们来细细分析这是为什么,首先是propety版本:

; Inner loop using properties.
0xc4a7a:  mov    r0, r11         ; move address of "self" into r0
                                 将self的地址给r0
0xc4a7c:  mov    r1, r6          ; move getter method name into r1                                   将gettter的方法名给r1 
0xc4a7e:  blx    0xc6fc0         ; call objc_msgSend(self, getterName)                               self调用getterName方法
0xc4a82:  vmov   d16, r0, r0     ; move the result of the getter into d16                            将getter的返回值给d16
0xc4a86:  subs   r5, #1          ; subtract 1 from our loop counter                                  将循环计数器-1
0xc4a88:  vadd.f32 d9, d9, d16   ; add d9 and d16 and store result into d16                          将d9和d16中的结果相加存储进d16
0xc4a8c:  bne    0xc4a7a         ; repeat unless our loop counter is now 0                           重复上面的操作知道循环计数器为0

在每一趟循环中,系统都要去调用objc_msgSend来调用property的getter,并将取得的结果存储进d16寄存器,然后(可能还要更晚)通过调用vadd.f32(NEON:一种高逼格的指令集寄存器)来进行真正的加法运算。从这点上看在iOS6和iOS7上没什么区别,特别要强调的是,苹果已经优化了objc_msgSend的执行速度,其实对于所有的account(这个词实在想不出是啥意思)来说已经很快了。
再来看看_ivar的版本:

; Inner loop using instance variables.(上边翻译过了,意思都差不多)
0xc4aba:  vadd.f32 d9, d9, d0    ; add d9 and d0 and store result into d9
0xc4abe:  subs   r0, #1          ; subtract 1 from our loop counter
0xc4ac0:  bne    0xc4aba         ; repeat unless our loop counter is now 0 

在直接调用_ivar的版本中,在进入循环之前,编译器就可以直接读取出当前存储在d0寄存器中_ivar中的值。让我们出乎意料的是,编译器做出了一个假设:那就是在进行循环的时候_ivar的值是不会发生改变的。更简单点说是因为_ivar并不是被声明为一个volatile(和const相反)类型的变量,所以编译器可以自由的假设"_ivar不会通过某种无法预测的方式改变";想看更多详情,去ARM网站的Compiler Optimization and the volatile Keyword了解。

objc_msgSend的性能开销
这是一个运行起来有很多乐趣的app,但它的测试也不是特别准确,因为不断地去重绘屏幕本身就是一件很耗性能的事,况且这其中还有很多未知的因素与之关联而影响测试的结果。如果我们抛弃UI层面的东西而把重心放在时间性能上,我们可以得出一些更好的结论。对于时间层面的信息需要注意的是:之前我们用的设备是一部搭载iOS7的iPhone5,并且去读取的那个32位浮点数已经存储在一级缓存中了。在这种情形下,直接读取_ivar花费大约3纳秒(你没看错就是纳秒),通过property读取变量花费大约11纳秒。
实际运用中的考虑
我们的应用有必要考虑这种以纳秒为单位的性能差异吗?答案是肯定的,但是我们是怎样做的呢?根本没在意。来看看下边这个方法,每个人都认识它:

[self.navigationController pushViewController:self.someOtherViewController animated:YES];

使用self.someOtherViewController这种方式要比_someOtherViewController
更慢?Yeah,几乎慢了8纳秒。使用一些技巧Mark D talked about with DTrace(动态跟踪),我们可以提供一些情形。在iOS7上,上面那一行代码运行的结果调用objc_msgSend的次数多余800次,等到someOtherViewController这个VC出现在屏幕上后,最终调用的次数会超过150000.想想这这个情形吧:仅仅考虑调用objc_msgSend花费的时间,使用property访问的方式让一个控制器呈现在屏幕上花费的时间是_ivar方式的1/800,几乎可以忽略不计了,因为这点,还考虑使用property的存取器能够让代码的一致性得以保留,所以property胜出了,是时候停止对property的担心,让我们拥抱它吧!(PS:这段写的比较绕,我也是看懂个大概,现在说说我的理解:第一行说使用property要比_ivar慢个8纳秒,但结论又让我们拥抱property看似矛盾,但好像不是这样,具我对中间几句话的反复理解,仅仅考虑调用objc_msgSend的时间看property只有_ivar的1/800分之一,也就是苹果对property做了优化,可能想推荐我们使用吧,而慢出的这8豪微秒可能是其他因素造成的,上文也提到了这个app去不断地重绘屏幕本来就是一件耗性能的事,而且有其他的未知因素,所以测试结果不是很准确,这段中说用DTrace这种高逼格的技术模拟出了一个只考虑objc——msgSend调用时间的场景,摒弃了UI层面的影响,得出的结论是让我们使用propetry,而且这样比较好看,可能感觉前面是点语法后面就出来个下划线不太美观吧,总之这段得出的结论还是挺出乎我的意料。)

简单地研究下NEON(原文中为Dipping a Toe into NEON,dip a toe好像是英语中的一句谚语,原文是“Dip a toe in any ocean and you are linked to all the world's oceans as they are one continuous mass”,意思是你把你的脚趾伸进任何一个海洋中就等于你伸进了全世界的海洋,因为它们以一个连通的区域,代之刚涉足某个领域吧,可能作者本人对NEON这种高逼格的指令寄存器也了解不多,才这么写的,感觉比较有意思,所以拿出翻译一下。)
因为NEON这个东西和本文探讨的主题没多少关系,所以我们没有必要探讨太多在样例app中循环内部所发生的汇编层面的加法运算。汇编结构vadd.f32是一种NEON。它有两个64位的NEON寄存器,可以认为每个寄存器都具备进行32位浮点数运算的功能(分别位于内存中的高32位和低32位),它们进行32位的模拟运算,将64位的运算结果分别存储在NEON寄存器的高32位和低32位。然而,在所有编译器件生成的循环中,我们只能看到这么多,因为在每一次循环中我们只执行了一次加法,在当层循环结束时vadd.f32操作的另一半已经被舍弃了,所以它潜在性能的一半都被我们浪费了!
我们可以使用在arm_neon.h中找到的方法,写一个NEON-enalbled版本的循环。warning:盲目地导入这个文件并使用其中定义的一些方法可能会让你的app在模拟器和一些老的设备上无法运行,因为它们不支持NEON指令集。下面是一个使用64位NEON进行循环的版本:

{
    // A float32x2_t corresponds to a 64-bit NEON register we treat as having
    // two 32-bit floats in each half. vmov_n_f32() initializes both halves
    // to the same value - 0.0f, in this case.
    // float32x2_t是一个64位NEON寄存器,分别有两个32位float在它的高32位和低32位,vmov_n_32()这个方法为其两个float初始化位0.0f
    float32x2_t x_pair = vmov_n_f32(0.0f);

    // Note that we now increment by 2 since we're doing two adds on each pass.
    // 注意一点现在的增量是2因为我们在高32位和低32位上同时相加
    for (NSUInteger i = 0; i < loopSize; i += 2) {
        // Construct a pair of 32-bit floats, both initialized to our ivar.
        // 构造一系列的32位浮点数用来初始化我们的ivar
        float32x2_t pair = vmov_n_f32(_value);

        // Perform vadd.f32
        // 进行加法运算
        x_pair = vadd_f32(x_pair, pair);
    }

    // To get our final result, we need to extract both halves, or lanes, of
    // our accumulator, and add them together, storing the result in the
    // CGFloat x.
    // 为了得到最终的结果,我们需要解析两部分的加法器,将和存储在x中
    x = vget_lane_f32(x_pair, 0) + vget_lane_f32(x_pair, 1);
}

到这还没有结束-NEON指令集还可以让我们使用128位宽的寄存器同时进行4个32位浮点运算;去NEON registers看这些不同界别的寄存器之间是如何相互工作的。下面还有个128位的NEON循环demo,它的策略在逻辑上和64位版本的相同,不同的是同时有4个变量在循环中参与运算而不是两个:

{
    float32x4_t x_quad = vmovq_n_f32(0.0f);
    for (NSUInteger i = 0; i < loopSize; i += 4) {
        float32x4_t quad = vmovq_n_f32(_property);
        x_quad = vaddq_f32(x_quad, quad);
    }
    x  = vgetq_lane_f32(x_quad, 0);
    x += vgetq_lane_f32(x_quad, 1);
    x += vgetq_lane_f32(x_quad, 2);
    x += vgetq_lane_f32(x_quad, 3);
}

这些NEON版本和之前的版本对比结果如何呢?64位NEON版比普通_ivar版的FPS快两倍,128位NEON办比普通_ivar版快四倍:


NEON中还有巨大的潜能没有被发掘。大多数应用程序可能永远都不需要,但是知道它有这种能力还是极好的。
ps:后面这部分关于NEON的介绍其实没什么卵用,作者的观点其实在上一段就表明了,鼓励大家用property而不是直接操作_ivar,性能消耗啥的,go to the hell。

上一篇下一篇

猜你喜欢

热点阅读