[2016 版] 常见操做性能对比

做者:Mike Ash,原文连接,原文日期:2016-04-15
译者:Yake;校对:numbbbbb;定稿:shankshtml

在我开始作 Friday Q&A 以前,我曾发表过一些关于常见操做性能测试的文章,并对结果进行了讨论。最近的一篇是在 2008 年 10 月 5 日,在 10.5 的 Mac 系统和最先的 iPhone 操做系统上。已经好长一段时间没有更新了。git

以前的文章

若是你想和以前的文章作对比,能够阅读下述内容:github

(注意苹果的手机操做系统直到 2010 年才被称为iOS服务器

概述

性能测试可能会很危险。测试报告看起来一般很不天然,除非你有特定的能够模仿真实应用场景的应用。这些特殊的测试确定不真实,而且测试结果可能没法真实地反应项目的实际性能。虽然不能对全部的事都给出确切的结果,但它能让你了解大概的数量级。iphone

测量高速操做是很难的一件事,好比 Objective-C 的消息发送或者是数学运算。因为如今 CPU 有复杂的设置与并行机制,一个操做独立花费的时间可能与它在复杂的真实项目中花费的时间并不相符。若是操做足够独立,将这类操做的代码添加到代码中时,CPU 能够并行处理,那可能根本不会增长那个操做自己执行须要的时间。另外一方面,若是它占用了重要资源,就可能会让运行时间大大增长。async

性能也可能依赖于一些外部因素。许多现代 CPU 在低温环境下运行很快,可是变热后就会慢下来。文件系统的性能将会依赖于硬件以及文件系统的状态。即便是相关的性能也会有所不一样。函数

当性能特别重要时,你老是但愿能测量并作图表分析,以便确切地知道在你的代码中哪里花费了时间,这样就直到应该把注意力集中在哪里。若是能找到代码中下降性能的地方,你必定会很开心。

总之,对各类操做的速度有个大体的概念将会十分有用。也许这能避免你在文件系统中存一大堆数据。为之付出一些努力是值得的,不过最终可能只是少发了一条消息,这么算又不太值。总之,谁也说不许结果如何。

方法

你能够在GitHub中获取这些测试的代码

代码是用Objective-C++写的,核心的性能测试是用 C 语言写的。目前我对 Swift 的了解还不够深刻,所以没法测试 Swift 的性能。

基础的技术很简单:把目标操做放入一个循环中持续几秒钟。用总的运行时间除以循环次数获得操做每次执行的时间。循环时间是硬编码的,我会尽可能延长测试时间,从而减小环境因素的影响。

我试图将循环自己的开支考虑在内。这种开支对于较慢操做的影响彻底不重要,可是对于较快操做的影响却至关大。所以,我会对一个空的循环进行计时,而后从其余测试的时间中减去每次循环的时间。

在有些测试中,测试代码可能会被流水线机制(校对注:CPU 的一种优化机制)优化,从而和被测试的代码并行。这使得那些测试时间惊人地短,从而致使彻底错误的结果。考虑到这些因素,一些高速操做会被手动展开,每次循环会执行十次测试,我但愿经过这种方式让结果变得更真实。

测试的编译与运行没有通过优化。这与咱们一般的作法相反,可是我以为对测试来讲这样作更好。对于那些几乎彻底依赖于外部代码的操做,例如与文件相关的操做或者 JSON 解析,结果没什么变化。但对于简单的操做例如数学计算或者方法调用,编译器极可能会直接把毫无心义的测试代码优化掉。此外,优化也会改变循环的编译方式,这会使得计算循环自己执行时间变得很复杂。

Mac 测试用的是个人 2013 年的 Mac Pro:3.5GHz,Xeon E5 处理器,系统是 10.11.4。iOS 测试用的是个人 iPhone 6s ,系统是iOS 9.3.1.

Mac 测试

下面是 Mac 测试的数据。每个测试都会列出测试内容、测试循环次数、测试须要的总时间以及每一次操做花费的时间。全部的时间都减掉了循环自己的消耗。

Name    Iterations    Total time (sec)    Time per (ns)
16 byte memcpy    1000000000    0.7    0.7
C++ virtual method call    1000000000    1.5    1.5
IMP-cached message send    1000000000    1.6    1.6
Objective-C message send    1000000000    2.6    2.6
Floating-point division with integer conversion    1000000000    3.7    3.7
Floating-point division    1000000000    3.7    3.7
Integer division    1000000000    6.2    6.2
ObjC retain and release    100000000    2.3    23.2
Autorelease pool push/pop    100000000    2.5    25.2
Dispatch_sync    100000000    2.9    29.0
16-byte malloc/free    100000000    5.5    55.4
Object creation    10000000    1.0    101.0
NSInvocation message send    10000000    1.7    174.3
16MB malloc/free    10000000    3.2    317.1
Dispatch queue create/destroy    10000000    4.1    411.2
Simple JSON encode    1000000    1.4    1421.0
Simple JSON decode    1000000    2.7    2659.5
Simple binary plist decode    1000000    2.7    2666.1
NSView create/destroy    1000000    3.3    3272.1
Simple XML plist decode    1000000    5.5    5481.6
Read 16 byte file    1000000    6.4    6449.0
Simple binary plist encode    1000000    8.8    8813.2
Dispatch_async and wait    1000000    9.3    9343.5
Simple XML plist encode    1000000    9.5    9480.9
Zero-zecond delayed perform    100000    2.0    19615.0
pthread create/join    100000    2.8    27755.3
1MB memcpy    100000    5.6    56310.6
Write 16 byte file    10000    1.7    165444.3
Write 16 byte file (atomic)    10000    2.4    237907.9
Read 16MB file    1000    3.4    3355650.0
NSWindow create/destroy    1000    10.6    10590507.9
NSTask process spawn    100    6.7    66679149.2
Write 16MB file (atomic)    30    2.8    94322686.1
Write 16MB file    30    3.1    104137671.1

这个表中最突出的是第一条。16-byte memcpy测试每次用时不到一纳秒。请看生成代码,虽然咱们关闭了优化,可是编译器很聪明地将memcpy调用转换成了一系列的mov指令。这点颇有趣:你写的方法调用不必定真的会调用这个方法。

一个真正的 C++ 方法调用和拥有IMP缓存的ObjC消息发送消耗相同的时间。它们真正作的操做如出一辙:一个经过函数指针实现的非直接方法调用。

一个普通的Objective-C消息发送,和咱们想的同样,相对较慢。然而,objc-msgSend的速度依然震惊到我了。它先是执行了一个完整的哈希表查询,而后又间接跳向告终果,一共只花了 2.6 纳秒!这差很少是 9 个 CPU 周期。一样的操做在 10.5 系统中须要超过 12 个周期,这么看性能确实有不小的提高。若是你只是作Objective-C的消息发送操做,这台电脑每秒钟能够执行四亿次。

使用NSInvocation来调用方法相对较慢。NSInvacation须要在运行时建立消息,和编译器在编译时作的事同样。幸运的是,NSInvocation在实际项目中通常不会成为性能瓶颈。不过和 10.5 对比,它的速度有所降低,一个NSInvocation调用大约花了以前两倍的时间,即便此次测试是在更快的硬件环境下进行的。

一对retainrelease操做一共消耗 23 纳秒。修改一个对象的引用计数必须是线程安全的,必须使用原子操做,这在纳秒级 CPU 中代价很高。

autoreleasepool比以前快了不少。在以前的测试中,建立并销毁一个自动释放池花费了超过 300 纳秒的时间。此次测试中,只用了 25 纳秒,自动释放池的实现已经彻底改写了,新的实现快的多,因此这没什么好惊讶的。释放池曾经是NSAutoReleasePool类型的实例,但如今使用运行时方法来完成,只须要作一些指针操做。25 纳秒,你能够放心地把@autoreleasepool放在任何须要自动释放的地方。

分配和释放 16 字节花费的时间没有多大变化,可是较大空间的分配速度显著提高。过去分类和释放 16MB 大约须要 4.5 微秒的时间,但如今只须要 300 纳秒。通常应用都会作不少的内存分配工做,因此这是个很大的提高。

Objective-C对象的建立速度也提高了不少,从过去的 300 纳秒到如今的 100 纳秒。显然,一个典型的应用会建立并销毁不少 Objective-C 对象,因此这个提高效果显著。另外一方面,建立并销毁一个对象的时间,至关于发送 40 个消息,因此这仍是一个代价很高的操做。另外,大多数对象建立和销毁须要的时间都远大于一个简单的NSObject实例。

dispatch_queue的测试在不一样的操做中表现出了有趣的差别。dispatch_sync在一个非竞争队列中特别快,时间在 30 纳秒如下。GCD 很高效,在本例中不作任何跨线程的调用,因此一共只须要执行一次加锁和释放操做。dispatch_async花费的时间就长得多,它须要先找到一条工做线程来使用,唤醒线程,而后在线程中执行任务。和 Objective-C 对象相比,建立并销毁一个diapatch_queue对象要快不少。GCD 可以共享不少内容,因此建立队列成本很低。

我此次增长了JSON以及plist的编码和解码测试,这个测试以前没有作过。因为 iPhone 的普及,这类操做受到愈来愈多的关注。这个测试编码并解码了一个包含三个元素的字典。正如预期的那样,它比消息发送这种简单而且低级的事务要慢,但仍在微妙的范围内。有趣的是,JSON比属性列表表现更好,哪怕是二进制的属性列表也比JSON慢,出乎意料。这多是由于JSON用途更广,所以得到更多关注;也多是由于JSON格式解析起来更快;或者是由于用一个只包含三个元素的字典测试不太合适,数据量更大时它们之间的速度差异可能会改变。

同步任务所需时间不少,大概是dispatch_async时间的两倍。看起来,运行时循环还有不少有待提高的地方。

建立一个pthread并等它终止,是另一个相对较为重量级的操做,时间大概在将近 30 纳秒。所以咱们理解了为何GCD只使用一个线程池,而且只在必要时才建立新的线程。然而,这个测试已经比过去的测试快多了,一样的测试,过去须要花超过 100 微秒的时间。

建立一个NSView实例很快,大约 3 微秒。不一样的是,建立一个NSWindow就慢得多,耗费大约 10 微秒时间。NSView是较为轻量的一种结构,它表明了界面中的一片区域, 而NSWindow则表明了窗口服务器中的一块像素缓存。建立一个NSWindow类型的对象须要让窗口服务建立必要的结构,还须要不少设置工做,给NSWindow类型的对象添加所需的各类内部对象,例如标题栏上的视图。这样说来,相比NSWindow,我更推荐使用NSView

文件存取确定很慢。SSD已经提高了不少性能,但仍是有不少的耗时的操做。因此只在必要的时候存取文件,能不用就别用。

iOS 测试

下面是 iOS 的测试结果

Name    Iterations    Total time (sec)    Time per (ns)
C++ virtual method call    1000000000    0.8    0.8
IMP-cached message send    1000000000    1.2    1.2
Floating-point division with integer conversion    1000000000    1.5    1.5
Integer division    1000000000    2.1    2.1
Objective-C message send    1000000000    2.7    2.7
Floating-point division    1000000000    3.5    3.5
16 byte memcpy    1000000000    5.3    5.3
Autorelease pool push/pop    100000000    1.5    14.7
ObjC retain and release    100000000    3.7    36.9
Dispatch_sync    100000000    7.9    79.0
16-byte malloc/free    100000000    8.6    86.2
Object creation    10000000    1.2    119.8
NSInvocation message send    10000000    2.7    268.3
Dispatch queue create/destroy    10000000    6.4    636.0
Simple JSON encode    1000000    1.5    1464.5
16MB malloc/free    10000000    15.2    1524.7
Simple binary plist decode    1000000    2.4    2430.0
Simple JSON decode    1000000    2.5    2515.9
UIView create/destroy    1000000    3.8    3800.7
Simple XML plist decode    1000000    5.5    5519.2
Simple binary plist encode    1000000    7.6    7617.7
Simple XML plist encode    1000000    10.5    10457.4
Dispatch_async and wait    1000000    18.1    18096.2
Zero-zecond delayed perform    100000    2.4    24229.2
Read 16 byte file    1000000    27.2    27156.1
pthread create/join    100000    3.7    37232.0
1MB memcpy    100000    11.7    116557.3
Write 16 byte file    10000    20.2    2022447.6
Write 16 byte file (atomic)    10000    30.6    3055743.8
Read 16MB file    1000    6.2    6169527.5
Write 16MB file (atomic)    30    1.6    52226907.3
Write 16MB file    30    2.3    78285962.9

最明显的是,它和 Mac 测试的结果很类似。看看过去的测试结果,iPhone 上的结果都相对较慢。一个 Objective-C 消息发送在 Mac 大约为 4.9 纳秒,在 iPhone 上要花很长时间,约为 200 纳秒。一个 C++ 的虚函数调用在 Mac 上花费大约 1 纳秒的时间,iphone上须要 80 纳秒。malloc/free 一段小的内存在 Mac 上约为 50 纳秒,可是在 iPhone 上须要大约 2 微秒的时间。

对比新旧测试,在现在的移动设备时代,不少事情都发生了变化。大多数状况下 iPhone 的数据只比 Mac 差一点,有些操做甚至更快。例如,自动释放池在 iPhone 上是至关快的。我猜ARM64更擅长执行自动释放池的代码。

读写小文件是 iPhone 的一大弱点。16MB 的文件测试与 Mac 的测试结果差很少,可是 16 字节的文件测试 iPhone 花了 Mac 10 倍的时间。相比 Mac,iPhone 的存储设备吞吐量很高,可是有一些额外的延迟。

结论

关注性能可让你写出高质量的代码,不过你只须要记住项目中常见操做的大体性能。性能会随着软件和硬件的提高发生变化。在过去的几年中 Mac 已经有了不错的提高,不过 iPhone 的进步更大。只用了 8 年时间,iPhone 就从比 Mac 慢一百倍进化到了同等性能。

今天就到此为止吧,下次再来讨论一些更有趣的东西。Friday Q&A 是由读者的建议驱动的,因此若是你想在某次的讨论中看到某个主题,请把它发送到这里

本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 http://swift.gg

相关文章
相关标签/搜索