作者:Mike Ash,原文鏈接,原文日期:2016-04-15
譯者:Yake;校對:numbbbbb;定稿:shanks
在我開始做 Friday Q&A 之前,我曾發(fā)表過一些關(guān)于常見操作性能測試的文章,并對結(jié)果進(jìn)行了討論。最近的一篇是在 2008 年 10 月 5 日,在 10.5 的 Mac 系統(tǒng)和最早的 iPhone 操作系統(tǒng)上。已經(jīng)好長一段時間沒有更新了。
之前的文章
如果你想和之前的文章做對比,可以閱讀下述內(nèi)容:
(注意蘋果的手機(jī)操作系統(tǒng)直到 2010 年才被稱為iOS)
概述
性能測試可能會很危險。測試報告看起來通常很不自然,除非你有特定的可以模仿真實應(yīng)用場景的應(yīng)用。這些特殊的測試肯定不真實,并且測試結(jié)果可能無法真實地反應(yīng)項目的實際性能。雖然不能對所有的事都給出確切的結(jié)果,但它能讓你了解大概的數(shù)量級。
測量高速操作是很難的一件事,比如 Objective-C 的消息發(fā)送或者是數(shù)學(xué)運(yùn)算。由于現(xiàn)在 CPU 有復(fù)雜的設(shè)置與并行機(jī)制,一個操作獨(dú)立花費(fèi)的時間可能與它在復(fù)雜的真實項目中花費(fèi)的時間并不相符。如果操作足夠獨(dú)立,將這類操作的代碼添加到代碼中時,CPU 可以并行處理,那可能根本不會增加那個操作本身執(zhí)行需要的時間。另一方面,如果它占用了重要資源,就可能會讓運(yùn)行時間大大增加。
性能也可能依賴于一些外部因素。許多現(xiàn)代 CPU 在低溫環(huán)境下運(yùn)行很快,但是變熱后就會慢下來。文件系統(tǒng)的性能將會依賴于硬件以及文件系統(tǒng)的狀態(tài)。即使是相關(guān)的性能也會有所不同。
當(dāng)性能特別重要時,你總是希望能測量并做圖表分析,以便確切地知道在你的代碼中哪里花費(fèi)了時間,這樣就直到應(yīng)該把注意力集中在哪里。如果能找到代碼中降低性能的地方,你一定會很開心。
總之,對各種操作的速度有個大致的概念將會十分有用。也許這能避免你在文件系統(tǒng)中存一大堆數(shù)據(jù)。為之付出一些努力是值得的,不過最終可能只是少發(fā)了一條消息,這么算又不太值。總之,誰也說不準(zhǔn)結(jié)果如何。
方法
你可以在GitHub中獲取這些測試的代碼。
代碼是用Objective-C++寫的,核心的性能測試是用 C 語言寫的。目前我對 Swift 的了解還不夠深入,因此無法測試 Swift 的性能。
基礎(chǔ)的技術(shù)很簡單:把目標(biāo)操作放入一個循環(huán)中持續(xù)幾秒鐘。用總的運(yùn)行時間除以循環(huán)次數(shù)得到操作每次執(zhí)行的時間。循環(huán)時間是硬編碼的,我會盡量延長測試時間,從而減少環(huán)境因素的影響。
我試圖將循環(huán)本身的開支考慮在內(nèi)。這種開支對于較慢操作的影響完全不重要,但是對于較快操作的影響卻相當(dāng)大。因此,我會對一個空的循環(huán)進(jìn)行計時,然后從其他測試的時間中減去每次循環(huán)的時間。
在有些測試中,測試代碼可能會被流水線機(jī)制(校對注:CPU 的一種優(yōu)化機(jī)制)優(yōu)化,從而和被測試的代碼并行。這使得那些測試時間驚人地短,從而導(dǎo)致完全錯誤的結(jié)果。考慮到這些因素,一些高速操作會被手動展開,每次循環(huán)會執(zhí)行十次測試,我希望通過這種方式讓結(jié)果變得更真實。
測試的編譯與運(yùn)行沒有經(jīng)過優(yōu)化。這與我們通常的做法相反,但是我覺得對測試來說這樣做更好。對于那些幾乎完全依賴于外部代碼的操作,例如與文件相關(guān)的操作或者 JSON 解析,結(jié)果沒什么變化。但對于簡單的操作例如數(shù)學(xué)計算或者方法調(diào)用,編譯器很可能會直接把毫無意義的測試代碼優(yōu)化掉。此外,優(yōu)化也會改變循環(huán)的編譯方式,這會使得計算循環(huán)本身執(zhí)行時間變得很復(fù)雜。
Mac 測試用的是我的 2013 年的 Mac Pro:3.5GHz,Xeon E5 處理器,系統(tǒng)是 10.11.4。iOS 測試用的是我的 iPhone 6s ,系統(tǒng)是iOS 9.3.1.
Mac 測試
下面是 Mac 測試的數(shù)據(jù)。每一個測試都會列出測試內(nèi)容、測試循環(huán)次數(shù)、測試需要的總時間以及每一次操作花費(fèi)的時間。所有的時間都減掉了循環(huán)本身的消耗。
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測試每次用時不到一納秒。請看生成代碼,雖然我們關(guān)閉了優(yōu)化,但是編譯器很聰明地將memcpy調(diào)用轉(zhuǎn)換成了一系列的mov指令。這點很有趣:你寫的方法調(diào)用不一定真的會調(diào)用這個方法。
一個真正的 C++ 方法調(diào)用和擁有IMP緩存的ObjC消息發(fā)送消耗相同的時間。它們真正做的操作一模一樣:一個通過函數(shù)指針實現(xiàn)的非直接方法調(diào)用。
一個普通的Objective-C消息發(fā)送,和我們想的一樣,相對較慢。然而,objc-msgSend的速度依然震驚到我了。它先是執(zhí)行了一個完整的哈希表查詢,然后又間接跳向了結(jié)果,一共只花了 2.6 納秒!這差不多是 9 個 CPU 周期。同樣的操作在 10.5 系統(tǒng)中需要超過 12 個周期,這么看性能確實有不小的提升。如果你只是做Objective-C的消息發(fā)送操作,這臺電腦每秒鐘可以執(zhí)行四億次。
使用NSInvocation來調(diào)用方法相對較慢。NSInvacation需要在運(yùn)行時創(chuàng)建消息,和編譯器在編譯時做的事一樣。幸運(yùn)的是,NSInvocation在實際項目中一般不會成為性能瓶頸。不過和 10.5 對比,它的速度有所下降,一個NSInvocation調(diào)用大約花了之前兩倍的時間,即使這次測試是在更快的硬件環(huán)境下進(jìn)行的。
一對retain和release操作一共消耗 23 納秒。修改一個對象的引用計數(shù)必須是線程安全的,必須使用原子操作,這在納秒級 CPU 中代價很高。
autoreleasepool比之前快了很多。在之前的測試中,創(chuàng)建并銷毀一個自動釋放池花費(fèi)了超過 300 納秒的時間。這次測試中,只用了 25 納秒,自動釋放池的實現(xiàn)已經(jīng)完全改寫了,新的實現(xiàn)快的多,所以這沒什么好驚訝的。釋放池曾經(jīng)是NSAutoReleasePool類型的實例,但現(xiàn)在使用運(yùn)行時方法來完成,只需要做一些指針操作。25 納秒,你可以放心地把@autoreleasepool放在任何需要自動釋放的地方。
分配和釋放 16 字節(jié)花費(fèi)的時間沒有多大變化,但是較大空間的分配速度顯著提升。過去分類和釋放 16MB 大約需要 4.5 微秒的時間,但現(xiàn)在只需要 300 納秒。一般應(yīng)用都會做很多的內(nèi)存分配工作,所以這是個很大的提升。
Objective-C對象的創(chuàng)建速度也提升了很多,從過去的 300 納秒到現(xiàn)在的 100 納秒。顯然,一個典型的應(yīng)用會創(chuàng)建并銷毀很多 Objective-C 對象,所以這個提升效果顯著。另一方面,創(chuàng)建并銷毀一個對象的時間,相當(dāng)于發(fā)送 40 個消息,所以這還是一個代價很高的操作。另外,大多數(shù)對象創(chuàng)建和銷毀需要的時間都遠(yuǎn)大于一個簡單的NSObject實例。
dispatch_queue的測試在不同的操作中表現(xiàn)出了有趣的差異。dispatch_sync在一個非競爭隊列中特別快,時間在 30 納秒以下。GCD 很高效,在本例中不做任何跨線程的調(diào)用,所以一共只需要執(zhí)行一次加鎖和釋放操作。dispatch_async花費(fèi)的時間就長得多,它需要先找到一條工作線程來使用,喚醒線程,然后在線程中執(zhí)行任務(wù)。和 Objective-C 對象相比,創(chuàng)建并銷毀一個diapatch_queue對象要快很多。GCD 能夠共享很多內(nèi)容,所以創(chuàng)建隊列成本很低。
我這次增加了JSON以及plist的編碼和解碼測試,這個測試之前沒有做過。由于 iPhone 的普及,這類操作受到越來越多的關(guān)注。這個測試編碼并解碼了一個包含三個元素的字典。正如預(yù)期的那樣,它比消息發(fā)送這種簡單并且低級的事務(wù)要慢,但仍在微妙的范圍內(nèi)。有趣的是,JSON比屬性列表表現(xiàn)更好,哪怕是二進(jìn)制的屬性列表也比JSON慢,出乎意料。這可能是因為JSON用途更廣,因此獲得更多關(guān)注;也可能是因為JSON格式解析起來更快;或者是因為用一個只包含三個元素的字典測試不太合適,數(shù)據(jù)量更大時它們之間的速度差別可能會改變。
同步任務(wù)所需時間很多,大概是dispatch_async時間的兩倍??雌饋?,運(yùn)行時循環(huán)還有很多有待提升的地方。
創(chuàng)建一個pthread并等它終止,是另外一個相對較為重量級的操作,時間大概在將近 30 納秒。因此我們理解了為什么GCD只使用一個線程池,并且只在必要時才創(chuàng)建新的線程。然而,這個測試已經(jīng)比過去的測試快多了,同樣的測試,過去需要花超過 100 微秒的時間。
創(chuàng)建一個NSView實例很快,大約 3 微秒。不同的是,創(chuàng)建一個NSWindow就慢得多,耗費(fèi)大約 10 微秒時間。NSView是較為輕量的一種結(jié)構(gòu),它代表了界面中的一片區(qū)域, 而NSWindow則代表了窗口服務(wù)器中的一塊像素緩存。創(chuàng)建一個NSWindow類型的對象需要讓窗口服務(wù)創(chuàng)建必要的結(jié)構(gòu),還需要很多設(shè)置工作,給NSWindow類型的對象添加所需的各種內(nèi)部對象,例如標(biāo)題欄上的視圖。這樣說來,相比NSWindow,我更推薦使用NSView。
文件存取肯定很慢。SSD已經(jīng)提升了很多性能,但還是有很多的耗時的操作。所以只在必要的時候存取文件,能不用就別用。
iOS 測試
下面是 iOS 的測試結(jié)果
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 測試的結(jié)果很相似。看看過去的測試結(jié)果,iPhone 上的結(jié)果都相對較慢。一個 Objective-C 消息發(fā)送在 Mac 大約為 4.9 納秒,在 iPhone 上要花很長時間,約為 200 納秒。一個 C++ 的虛函數(shù)調(diào)用在 Mac 上花費(fèi)大約 1 納秒的時間,iphone上需要 80 納秒。malloc/free 一段小的內(nèi)存在 Mac 上約為 50 納秒,但是在 iPhone 上需要大約 2 微秒的時間。
對比新舊測試,在如今的移動設(shè)備時代,很多事情都發(fā)生了變化。大多數(shù)情況下 iPhone 的數(shù)據(jù)只比 Mac 差一點,有些操作甚至更快。例如,自動釋放池在 iPhone 上是相當(dāng)快的。我猜ARM64更擅長執(zhí)行自動釋放池的代碼。
讀寫小文件是 iPhone 的一大弱點。16MB 的文件測試與 Mac 的測試結(jié)果差不多,但是 16 字節(jié)的文件測試 iPhone 花了 Mac 10 倍的時間。相比 Mac,iPhone 的存儲設(shè)備吞吐量很高,但是有一些額外的延遲。
結(jié)論
關(guān)注性能可以讓你寫出高質(zhì)量的代碼,不過你只需要記住項目中常見操作的大致性能。性能會隨著軟件和硬件的提升發(fā)生變化。在過去的幾年中 Mac 已經(jīng)有了不錯的提升,不過 iPhone 的進(jìn)步更大。只用了 8 年時間,iPhone 就從比 Mac 慢一百倍進(jìn)化到了同等性能。
今天就到此為止吧,下次再來討論一些更有趣的東西。Friday Q&A 是由讀者的建議驅(qū)動的,所以如果你想在某次的討論中看到某個主題,請把它發(fā)送到這里
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請訪問 http://swift.gg。