[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

相關文章
相關標籤/搜索