Swift 性能相關

起初的疑問源自于「在 Swift 中的, Struct:Protocol 比 抽象類 好在哪里?」。但是找來找去都是 Swift 性能相關的東西。整理了點筆記,供大家可以參考一下。

一些疑問

在正題開始之前,不知道你是否有如下的疑問:

  • 為什么說?Swift?相比較于?Objective-C?會更加?快??
  • 為什么在編譯?Swift?的時候這么?慢??
  • 如何更?優雅?的去寫 Swift ?

如果你也有類似疑問,希望這篇筆記能幫你解釋一下上面幾個問題的一些原因。(ps.上面幾個問題都很大,如果有不同的想法和了解,也希望你能分享出來,大家一起討論一下。)

Swift中的類型

首先,我們先統一一下關于類型的幾個概念。

  • 平凡類型

有些類型只需要按照字節表示進行操作,而不需要額外工作,我們將這種類型叫做平凡類型 (trivial)。比如,Int 和 Float 就是平凡類型,那些只包含平凡值的 struct 或者 enum 也是平凡類型。

 
  • 引用類型

對于引用類型,值實例是一個對某個對象的引用。復制這個值實例意味著創建一個新的引用,這將使引用計數增加。銷毀這個值實例意味著銷毀一個引用,這會使引用計數減少。不斷減少引用計數,最后當然它會變成 0,并導致對象被銷毀。但是需要特別注意的是,我們這里談到的復制和銷毀值,只是對引用計數的操作,而不是復制或者銷毀對象本身。

 
  • 組合類型

類似 AClass 這類,引用類型包含平凡類型的,其實還是引用類型,但是對于平凡類型包含引用類型,我們暫且稱之為組合類型。

 

影響性能的主要因素

主要原因在下面幾個方面:

  • 內存分配 (?Allocation?):主要在于?堆內存分配?還是?棧內存分配?。
  • 引用計數 (?Reference counting?):主要在于如何?權衡?引用計數。
  • 方法調度 (?Method dispatch?):主要在于?靜態調度?和?動態調度?的問題。

內存分配(Allocation)

今天主要談一談?內存分區?中的?堆?和?棧?。

  • 堆(?heap?)?

堆是用于存放進程運行中被 動態分配的內存段 ,它的大小并不固定,可動態擴張或 縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張); 當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)

  • 棧 (?stack heap?)?

棧又稱堆棧, 是 用戶存放程序臨時創建的局部變量 ,也就是說我們函數括弧“{}” 中定義的變量(但不包括static聲明的變量,static意味著在 數據段 中存放變量)。除此以外, 在函數被調用時,其參數也會被壓入發起調用的進程棧中,并且待到調用結束后,函數的返回值 也會被存放回棧中。由于棧的先進先出特點,所以 棧特別方便用來保存/恢復調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數據的內存區。

在 Swift 中,對于?平凡類型?來說都是存在?棧?中的,而?引用類型?則是存在于?堆?中的,如下圖所示:

Swift 性能相關

我們都知道,Swift建議我們多用?平凡類型?,那么?平凡類型?比?引用類型?好在哪呢?換句話說「在?棧?中的數據和?堆中的數據相比有什么優勢?」

  • 數據結構
    • 存放在棧中的數據結構較為簡單,只有一些值相關的東西
    • 存放在堆中的數據較為復雜,如上圖所示,會有type、retainCount等。
  • 數據的分配與讀取
    • 存放在棧中的數據從棧區底部推入 (push),從棧區頂部彈出 (pop),類似一個數據結構中的棧。由于我們只能夠修改棧的末端,因此我們可以通過維護一個指向棧末端的指針來實現這種數據結構,并且在其中進行內存的分配和釋放只需要重新分配該整數即可。所以棧上分配和釋放內存的代價是很小。
    • 存放在堆中的數據并不是直接 push/pop,類似數據結構中的鏈表,需要通過一定的算法找出最優的未使用的內存塊,再存放數據。同時銷毀內存時也需要重新插值。
  • 多線程處理
    • 棧是線程獨有的,因此不需要考慮線程安全問題。
    • 堆中的數據是多線程共享的,所以為了防止線程不安全,需同步鎖來解決這個問題題。

綜上幾點,在內存分配的時候,盡可能選擇?棧?而不是?堆?會讓程序運行起來更加快。

引用計數(Reference counting)

首先?引用計數?是一種?內存管理技術?,不需要程序員直接去操作指針來管理內存。

而采用?引用計數?的?內存管理技術?,會帶來一些性能上的影響。主要以下兩個方面:

  • 需要通過大量的?release/retain?代碼去維護一個對象生命周期。
  • 存放在?堆區?的是多線程共享的,所以對于 retainCount 的每一次修改都需要通過同步鎖等來保證線程安全。

對于?自動引用計數?來說, 在添加 release/retain 的時候采用的是一個寧可多寫也不漏寫的原則,所以 release/retain 有一定的冗余。這個冗余量大概在?10%?的左右(如下圖,圖片來自于?iOS可執行文件瘦身方法?)。

Swift 性能相關

而這也是為什么雖然 ARC 底層對于內存管理的算法進行了優化,在速度上也并沒有比 MRC 寫出來的快的原因。?這篇文章?詳細描述了 ARC 和 MRC 在速度上的比較。

綜上,雖然因為自動引用計數的引入,大大減少了內存管理相關的事情,但是對于引用計數來說,過多或者冗余的引用計數是會減慢程序的運行的。

而對于引用計數來說,還有一個?權衡問題?,具體如何權衡會再后文解釋。

方法調度 (Method dispatch)

在 Swift 中, 方法的調度主要分為兩種:

  • 靜態調度?: 可以進行inline和其他編譯期優化,在執行的時候,會直接跳到方法的實現。
 
  • 動態調度?: 在執行的時候,會根據運行時,采用?V-Table?的方式,找到方法的執行體,然后執行。無法進行編譯期優化。?V-Table?不同于 OC 的調度,在 OC 中,是先在運行時的時候先在子類中尋找方法,如果找不到,再去父類尋找方法。而對于 V-Table 來說,它的調度過程如下圖:

Swift 性能相關

因此,在性能上「?靜態調度?>?動態調度?」并且「?Swift中的V-Table?>?Objective-C 的動態調度?」。

協議類型 (Protocol types)

在 Swift 引入了一個?協議類型?的概念,示例如下:

 

在上述代碼中,?Drawable?就稱為協議類型,由于?平凡類型?沒有繼承,所以實現多態上出現了一些棘手的問題,但是 Swift 引入了 協議類型 很好的解決了?平凡類型?多態的問題,但是在設計?協議類型?的時候有兩個最主要的問題:

  • 對于類似?Drawable?的協議類型來說,如何去調度一個方法?
  • 對于不同的類型,具有不同的size,當保存到 drawables 數組時,如何保證內存對齊?

對于第一個問題,如何去調度一個方法?因為對于?平凡類型?來說,并沒有什么虛函數指針,所以在 Swift 中并沒有?V-Table?的方式,但是還是用到了一個叫做?The Protocol Witness Table (PWT)?的函數表,如下圖所示:

Swift 性能相關

對于每一個 Struct:Protocol 都會生成一個 StructProtocol 的?PWT?。

對于第二個問題,如何保證內存對齊問題?

Swift 性能相關

有一個簡單粗暴的方式就是,取最大的Size作為數組的內存對齊的標準,但是這樣一來不但會造成內存浪費的問題,還會有一個更棘手的問題,如何去尋找最大的Size。所以為了解決這個問題,Swift 引入一個叫做?Existential Container?的數據結構。

Swift 性能相關

  • Existential Container

Swift 性能相關

這是一個最普通的 Existential Container。

  • 前三個word:Value buffer。用來存儲Inline的值,如果word數大于3,則采用指針的方式,在堆上分配對應需要大小的內存
  • 第四個word:Value Witness Table(VWT)。每個類型都對應這樣一個表,用來存儲值的創建,釋放,拷貝等操作函數。(管理 Existential Container 生命周期)
  • 第五個word:Protocol Witness Table(PWT),用來存儲協議的函數。

用偽代碼表示如下:

 

所以,對于上文代碼中的 Point 和 Line 最后的數據結構大致如下:

Swift 性能相關

這里需要注意的幾個點:

  • 在 ABI 穩定之前 value buffer 的 size 可能會變,對于是不是 3個 word 還在 Swift 團隊還在權衡.
  • Existential Container 的 size 不是只有 5 個 word。示例如下:

Swift 性能相關

對于這個大小差異最主要在于這個 PWT 指針,對于 Any 來說,沒有具體的函數實現,所以不需要 PWT 這個指針,但是對于 ProtocolOne&ProtocolTwo 的組合協議,是需要兩個 PWT 指針來表示的。

OK,由于 Existential Container 的引入,我們可以將協議作為類型來解決?平凡類型?沒有繼承的問題,所以 Struct:Protocol 和 抽象類就越來越像了。

回到我們最初的疑問,「在 Swift 中的, Struct:Protocol 比 抽象類 好在哪里?」

  • 由于 Swift 只能是單繼承,所以 抽象類 很容易造成?「上帝類」?,而Protocol可以是一個多這多個則沒有這個問題
  • 在內存分配上上,Struct是在棧中的,而抽象類是在堆中的,所以?簡單數據?的Struct:Protocol會再性能上比抽象類更加好
  • (寫起來更加有逼格算不算?)

但是,雖然表面上協議類型確實比抽象類更加的?“好”?,但是我還是想說,不要隨隨便便把協議當做類型來使用。

為什么這么說?先來看一段代碼:

 

首先,我們把 Drawable 協議當做一個類型,作為 Pair 的屬性,由于協議類型的 value buffer 只有三個 word,所以如果一個 struct(比如上文的Line) 超過三個 word,那么會將值保存到堆中,因此會造成下圖的現象:

Swift 性能相關

一個簡單的復制,導致屬性的copy,從而引起?大量的堆內存分配?。

所以,不要隨隨便便把協議當做類型來使用。上面的情況發生于無形之中,你卻沒有發現。

當然,如果你非要將協議當做類型也是可以解決的,首先需要把Line改為class而不是struct,目的就是引入引用計數。所以,將Line改為class之后,就變成了如下圖所示:

Swift 性能相關

至于修改了 line 的 x1 導致所有 pair 下的 line 的 x1 的值都變了,我們可以引入?Copy On Write?來解決。

當我們 Line 使用平凡類型時,由于line占用了4個word,當把協議作為類型時,無法將line存在 value buffer 中,導致了堆內存分配,同時每一次復制都會引發堆內存分配,所以我們采用了引用類型來替代平凡類型,增加了引用計數而降低了堆內存分配,這就是一個很好的引用計數權衡的問題。

泛型(Generic code)

首先,如果我們把協議當做類型來處理,我們稱之為?「動態多態」?,代碼如下:

 

而如果我們使用泛型來改寫的話,我們稱之為?「靜態多態」?,代碼如下:

 

而這里所謂的?動態?和?靜態?的區別在哪里呢?

在 Xcode 8 之前,唯一的區別就是由于使用了泛型,所以在調度方法是,我們已經可以根據上下文確定了這個?T?到底是什么類型,所以并不需要?Existential Container?,所以泛型沒有使用?Existential Container?,但是因為還是多態,所以還是需要VWT和PWT作為隱形參數傳遞,對于臨時變量仍然按照ValueBuffer的邏輯存儲 - 分配3個word,如果存儲數據大小超過3個word,則在堆上開辟內存存儲。如圖所示:

Swift 性能相關

這樣的形式其實和把協議作為類型并沒有什么區別。唯一的就是沒有?Existential Container?的中間層了。

但是,在 Xcode 8 之后,引入了?Whole-Module Optimization?使泛型的寫法更加靜態化。

首先,由于可以根據上下文知道確定的類型,所以編譯器會為每一個類型都生成一個drawACopy的方法,示例如下:

 

由于每個類型都生成了一個drawACopy的方法,drawACopyOfAPoint的調用就吧編程了一個靜態調度,再根據前文靜態調度的時候,編譯器會做 inline 處理,所以上面的代碼經過編譯器處理之后代碼如下:

 

由于編譯器一步步的處理,再也不需要 vwt、pwt及value buffer了。所以對于泛型來做多態來說,就叫做靜態多態。

幾點總結

  • 為什么在編譯 Swift 的時候這么慢
    • 因為編譯做了很多事情,例如 靜態調度的inline處理,靜態多態的分析處理等
  • 為什么說 Swift 相比較于 Objective-C 會更加快
    • 對于Swift來說,更多的靜態的,比如靜態調度、靜態多態等。
    • 更多的棧內存分配
    • 更少的引用計數
  • 如何更優雅的去寫 Swift
    • 不要把協議當做類型來處理
    • 如果需要把協議當做類型來處理的時候,需要注意 big Value 的復制就引起堆內存分配的問題。可以用 Indirect Storage + Copy On Write 來處理。
    • 對于一些抽象,可以采用 Struct:Protocol 來代替抽象類。至少不會有?上帝類?出現,而且處理的好的話性能是比抽象類更好的。

參考資料

  • Understanding Swift Performance
  • 真實世界中的 Swift 性能優化
  • Exploring Swift Memory Layout
  • 水平有限,若有錯誤,希望多多指正!coderonevv#gmail.com

更多

工作之余,寫了點筆記,如果需要可以在我的?GitHub?看。

查看原文:?Swift 性能相關

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/253471.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/253471.shtml
英文地址,請注明出處:http://en.pswp.cn/news/253471.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

linux_NandFlash_driver_超詳細分析 .

分類: Linux 驅動 Nand 驅動 2011-11-06 23:16 474人閱讀 評論(0) 收藏 舉報 今天學習了NandFlash的驅動,硬件操作非常簡單,就是這個linux下的驅動比較復雜,主要還是MTD層的問題,用了一下午時間整理出來一份詳細的分析…

HTTPS 路徑配置

1: 首先安裝 fiddlercertmaker.exe 文件2:Tools -> HTTPS 3: Connections 勾中Allow remote computer to connect轉載于:https://www.cnblogs.com/eason-d/p/7492177.html

CMOS圖像傳感器——相位對焦

之前介紹了許多自動對焦的方案 自動對焦方法學習_滄海一升的博客-CSDN博客自動對焦的各類方法學習介紹https://blog.csdn.net/qq_21842097/article/details/121373263 在里面提到了遮蔽像素相位檢測法,原理上算是相位檢測法(Phase Detection Auto Focus,PDAF)的一種。…

51nod 1343 行列式的根

這題分塊搞一搞,算到最后發現結果就是算矩陣J的行列式,要取模m,那個sign消掉了。 參考文獻在這,歐幾里德算法的應用,金斌 代碼還是比較好寫的,python代碼 n,mmap(int,raw_input().split()) a[] for i in ra…

Spring Cloud Config 和Spring Cloud Bus實現配置中心

2019獨角獸企業重金招聘Python工程師標準>>> Spring Cloud是很多組件的集合,Spring將常用的技術框架進行包裝和整合,如mybatis zookeeper rabbitmq redis等等,還有一些科技公司貢獻出來的一些經過生產環境驗證的組件如奈飛公司貢獻…

ARM Linux啟動過程分析

1. 引 言 Linux 最初是由瑞典赫爾辛基大學的學生 Linus Torvalds在1991 年開發出來的,之后在 GNU的支持下,Linux 獲得了巨大的發展。雖然 Linux 在桌面 PC 機上的普及程度遠不及微軟的 Windows 操作系統,但它的發展速度之快、用戶數量的日益…

你有沒有靠譜的基因?一個人靠不靠譜,其實就看這三點:“凡事有交代,件件有著落,事事有回音。”...

你有沒有靠譜的基因?一個人靠不靠譜,其實就看這三點:“凡事有交代,件件有著落,事事有回音。” 故事一、做了就忘了? 一天上班后,我讓小王給上級部門送一個材料。 一個小時過去了,沒…

CMOS圖像傳感器——閃爍(flicker)現象

一、概述 閃爍(Flicker),通常發生在室內場景,曝光時間設置如果不是光源能量周期的整數倍,則圖像不同位置處積累的信號強度不同,并呈周期性變化,這是單幀圖像的情況。在視頻序列上,如果滿足一定條件,視頻會出現條紋模式在垂直方向上緩慢移動。 二、形成原因 1、光源 …

一條命令教你安裝centos下面的pip服務

yum install -y python-pip轉載于:https://blog.51cto.com/12131824/2177874

strcpy,memcpy,memset函數實現

strcpy 實現,只能拷貝字符串 char* strcpy(char* des,const char* source) {char* rdes; assert((des ! NULL) && (source ! NULL));while((*des *source)!\0);return r; } memcpy 實現,注意目的地址和源地址重合的情況,以及強制類…

CMOS圖像傳感器——圖像傳感器噪聲

圖像傳感器噪聲取決于圖像傳感器的制作工藝、內部結構及內部補償技術等原因,噪聲反應了圖像傳感器的內部特性。CMOS圖像傳感器基本原理見: CMOS圖像傳感——概述_滄海一升的博客-CSDN博客_cmos圖像傳感器CMOS圖像傳感器基本介紹https://blog.csdn.net/qq_21842097/article/d…

TI Davinci DM6441嵌入式Linux移植攻略——UBL移植篇

目錄(?)[] 一DM6441的Boot過程簡介二DM6441的UBL移植 CCS文件夾Common文件夾GNU文件夾 移植DDR2移植Nand Flash其它 聲明:本文參考網友zjb_integrated的文章《TI Davinci DM6446開發攻略——UBL移植》和《DAVINCI DM365-DM368開發攻略——U-BOOT-2010.12及UBL的移…

python接口自動化測試(二)-requests.get()

環境搭建好后,接下來我們先來了解一下requests的一些簡單使用,主要包括: requests常用請求方法使用,包括:get,postrequests庫中的Session、Cookie的使用其它高級部分:認證、代理、證書驗證、超時…

從一個Android碼農視角回顧2018GDD大會

兩天的GDD大會結束了,很開心,可以看得出,這次Google真的很用心。不但分享的內容質量很高。而且又有得吃又有得玩,還有許多好看的小姐姐,真不妄我請了兩天年假來參加這個大會。先來幾張圖鎮樓 哈哈,跑題了。…

Python3.x和Python2.x的區別[轉]

Python3.x和Python2.x的區別 1.性能 Py3.0運行 pystone benchmark的速度比Py2.5慢30%。Guido認為Py3.0有極大的優化空間,在字符串和整形操作上可 以取得很好的優化結果。 Py3.1性能比Py2.5慢15%,還有很大的提升空間。 2.編碼 Py3.X源碼文件默認使用utf-8…

數字圖像處理——圖像銳化

圖像增強是圖像處理的一個重要環節,早期的圖像處理就是從圖像增強開始的,人們研究對質量低的圖像進行處理以獲得改善質量后的圖像。現今的圖像增強還為后續的圖像處理,如圖像信息提取、圖像識別等,提供更高識別度的圖像。 從圖像處理技術來看,圖像的攝取、編碼、傳輸和處理…

DAVINCI DM365-DM368開發攻略——U-BOOT-2010.12及UBL的移植

從盛夏走到深秋,我們繼續DAVINCI DM365-DM368的開發。說來慚愧,人家51CTO熱情支持本博客,而本人卻一直沒有像其他博客之星一樣頻繁更新博客,心里確實說不過去。管理公司確實很累,有更急的客戶的項目要做,我…

陳天藝1636050045假設跑步者1小時40分鐘35秒跑了24英里。編寫一個程序顯示每小時以公里為單位的平均速度值...

public class AverageSpeed{ public static void main(String[]args){ double speedkm 60/(45.5/14); double speedm speedkm /1.6; system.out.println(“averagespeed ”speedm "m/h") } }轉載于:https://www.cnblogs.com/Archon-Cty/p/7…

EMVA 1288 測試標準

一、概述 如果要對比兩臺相機的性能,我們應該關注哪些參數呢,是焦距、像素、還是光圈大小?這些參數通常廣為人知,并且很容易做出對比。但在一些專業領域,例如機器視覺、自動駕駛等行業,計算機算法對圖像有著獨特的要求,這些標準有些已經跟不上數字成像的發展步伐,而且其…

Spring Boot - 修改Tomcat默認的8080端口

前言 默認情況下,Spring Boot內置的Tomcat服務會使用8080端口啟動,我們可以使用以下任何技巧去更改默認的Tomcat端口; 注:我們可以通過server.port0配置,去自動配置一個未被占用的http端口,由操作系統實現。…