GCD原始碼分析之base.h

發表於2016-09-10

本文屬於原創文章。轉載請聯絡作者。1711809-d203d3a59d024556

GCD & Base

前言

GCD 是一個跨平臺的庫,它使用了大量的系統底層知識以及編譯器優化內容。
本系列文章將主要分析該庫的一些值得學習的地方。取其精華,去其糟粕。

base

base.h 主要宣告瞭庫中常用的巨集。通過它,GCD 實現了對多平臺的相容。

attribute

__attribute__GCC 的一大特色1clang 實現大部分的描述符並新增了一些擴充套件2

通過它,開發者可以告訴編譯器更多的資訊以達到優化程式的目的。本文的主要內容即為分析 base.h 檔案內的 __attribute__

noreturn

#define DISPATCH_NORETURN __attribute__((__noreturn__)) 巨集將 __attribute__((__noreturn__)) 定義為 DISPATCH_NORETURN

noreturn 用於告訴編譯器,該函式沒有返回(既不需要 return ... 語句)。

noreturn 有如下實際作用:

  • 優化程式碼函式的呼叫者在呼叫函式返回後可能需要做一些清理工作,比如調整桟指標。
    當一個函式被標記為 __noreturn__ 時,那麼編譯器就可以知道這些清理工作能夠被省略掉,這樣可以達到少生成一些無用的程式碼的目的。
  • 抑制編譯器的某些警告在公共標頭檔案中,只有 DISPATCH_EXPORT DISPATCH_NOTHROW DISPATCH_NORETURN void dispatch_main(void); 方法使用了該巨集。

    因為,我們在正常的應用開發中不會使用到該方法。所以,下面通過其它的示例程式碼對其進行說明。

    void objc_terminate(void)objc 庫的一個退出方法。

    在這裡,通過 extern 將其重新宣告並新增 DISPATCH_NORETURN 巨集。

    int sun_terminater() 是我自定義的一個終止函式,它不接收引數,並返回 int 型別的結果。

    在 clang 中,當函式宣告瞭返回值,卻沒有提供時,編譯器報出Control reaches end of non-void function的❗️錯誤提示。如下圖所示。

    111711809-2c7a073f8d455c31

     但是,這裡因為有被 noreturn 描述的函式 objc_terminate(); 的存在,原來的錯誤提示會被編譯器丟失,即開發者看不到原先的❗️錯誤提示。

    一種更復雜的情況是,當函式有分支處理時,需要所有的分支都包含有被noreturn 描述的任意函式。否則,系統仍然會提示錯誤。如下圖所示。

    121711809-38f2816a9df1015d

nothrow

#define DISPATCH_NOTHROW __attribute__((__nothrow__)) 巨集將 __attribute__((__nothrow__)) 定義為 DISPATCH_NOTHROW

該描述符的含義是,函式不會丟擲異常。因為 iOS 中使用異常處理的情況比較少,所以這裡就不進行展開講解。

nonnull

__nonnull__用於指定函式或方法的入參型別不為空指標。

它有兩種寫法,__attribute__((__nonnull__)) 或者 __attribute__((__nonnull__(1,2))

第一種要求所有的入參型別不為空指標,第二種要求指定位置的引數不為空指標。

比如下面的方法宣告中,要求第二個引數 block 不能為空指標。當 block 為空時,根本沒有必要呼叫該方法,所以這裡使用 DISPATCH_NONNULL2 對其進行限制。

sentinel

#define DISPATCH_SENTINEL __attribute__((__sentinel__)) 巨集將 __attribute__((__sentinel__)) 定義為 DISPATCH_SENTINEL

__sentinel__ 的中文意思是哨兵。

當接收可變引數時,可以通過它指定NULL所在的位置。

預設情況下,NULL所在的位置為從後往前數第0個為NULL。
常見於陣列或者字典等集合型別接受多個引數時指定最後一個為NULL。
通過它,函式內部可以知道何時終止引數的處理。

示例如下程式碼如下:

131711809-c33a8382da3168f9

修飾符指定了從後往前數第三個為NULL,如果不滿足這個規則,編譯器會產生警告⚠️。

pure

#define DISPATCH_PURE __attribute__((__pure__)) 巨集將 __attribute__((__pure__)) 定義為 DISPATCH_PURE

__pure__ 的中文意思是純淨的。它只依賴於入參和全域性變數進行處理。並且不會對外部造成影響。

pure 修飾的函式不具有可重入性,但是可以通過關中斷、訊號量(即P、V操作)等手段對全域性變數加以保護以實現。

當被 pure 修飾的函式的返回結果沒有被使用時,編譯器會直接丟棄呼叫被 pure 修飾的函式的相關程式碼。

下面以 Implications of pure and constant functions中的例子進行說明。

程式碼分析:

我們很容易發現,testfunction 函式內的部分程式碼是可以被忽略的。

比如 int res2 = somepurefunction(b, c);,只有返回值 res2 是由該函式生成的,在它的內部不會影響全域性變數,所以該行程式碼可以全部忽略。

程式碼行:int res2 = somepurefunction(b, c); 在執行過程中可能影響全域性變數,所以無法忽略,但是其返回值沒有用處,所以,返回值可以忽略返回值。

程式碼行: int res3 = a + b - c; 很明顯,因為返回結果 res3 沒有使用,該行程式碼可以忽略掉。

所以,編譯優化後程式碼為:

const

DISPATCH_CONST __attribute__((__const__)) 巨集將 __attribute__((__const__)) 定義為 DISPATCH_CONST

__const____pure__ 類似,但是 __const 不能依賴於全域性變數。也就意味著,const 修飾的函式是冪等的,具有可重入性。

下面仍然通過改造 Implications of pure and constant functions 中的例子進行說明。

輸出日誌:

編譯器發現,lookup(val)被呼叫了三次,且在該過程中,val的值沒有發生變化。所以編譯器只呼叫了一次該函式,並快取了其結果。

特殊情況1

請注意,雖然 const 修飾的函式是冪等的,但是當被 const 修飾的函式有入參時,我沒有發現能夠合理優化如下程式碼的編譯器。

在下面的程式碼中, square 函式的入參並沒有發生變化,所以,合理的優化應該是隻呼叫一次 square 函式,並快取結果以提供給後續的程式碼使用。但實際上,square 函式被呼叫了1000次。

特殊情況2

當被 const 修飾的函式有沒有入參時,編譯結果卻發生了非常大的變化。編譯器會盡可能的在編譯期間執行裡面的程式碼,並將結果直接賦值給呼叫方。
示例程式碼:

優化結果:

warn_unused_result

#define DISPATCH_WARN_RESULT __attribute__((__warn_unused_result__)) 巨集將 __attribute__((__warn_unused_result__)) 定義為 DISPATCH_WARN_RESULT

當函式的呼叫方沒有使用函式的返回結果時,會產生一個警告。

比如下面的程式碼會在第5行產生一個⚠️警告。Ignoring return value of function declared with warn_unused_result attribute

malloc

#define DISPATCH_MALLOC __attribute__((__malloc__)) 巨集將 __attribute__((__malloc__)) 定義為 DISPATCH_MALLOC

當一個函式返回的指標是類似於呼叫 malloc 函式時,可以指定該修飾符。

它表示通過這個函式返回的非空指標肯定與當前程式中其它的有效指標不同,即其值肯定不同與當前任何有效指標。這樣編譯器就能夠識別出該指標不存在別名,可以進行更好的優化。

always_inline

#define DISPATCH_ALWAYS_INLINE __attribute__((__always_inline__)) 巨集將 __attribute__((__always_inline__)) 定義為 DISPATCH_ALWAYS_INLINE

內聯(inline) 函式,會導致程式碼增大(同一份程式碼存在多個地方),但是執行速度會加快(不需要跨函式呼叫)。

一般情況下,只有開啟優化選項後,部分函式才能夠被編譯器當做行內函數編譯。但是 __always_inline__ 可以在沒有開啟優化選項時,依然強制某個函式當做行內函數使用。

unavailable

#define DISPATCH_UNAVAILABLE __attribute__((__unavailable__)) 巨集將 __attribute__((__unavailable__)) 定義為 DISPATCH_UNAVAILABLE

__unavailable__用於宣告某個函式不可用。

DISPATCH_EXPORT

DISPATCH_EXPORT 可以在檔案中宣告一個全域性變數或函式。

DISPATCH_INLINE

DISPATCH_INLINE 可以將函式宣告為靜態行內函數。

__builtin_expect((x), (v))

#define DISPATCH_EXPECT(x, v) __builtin_expect((x), (v)) 巨集將 __builtin_expect((x), (v)) 定義為 DISPATCH_EXPECT(x, v)

__builtin_expect 可以指定某個值的預期值。這樣可以讓編譯器對相關的分支程式碼進行優化。

下面部分需要有部分的處理器相關的知識才能理解該部分。

在現代的處理器設計中,因為 cpu 和記憶體的速度不匹配。所以現在的 cpu 會讀取多條指令並執行(多流水線)。比如,遇到分支時,它會同時計算分支的條件,並選擇第一個分支執行。

比如下面的程式碼,__builtin_expect(x, 5) 程式碼告訴編譯器,x 的值具有很大的概率是5。

如果沒有任何優化,處理器會經常進入default分支執行,導致資源浪費。在這種情況下,編譯後的程式碼會調整分支的順序以達到提高資源效率的最優化。

ns_returns_retained

__ns_returns_retained__ 告訴編譯器,該函式的返回值需要進行retain 操作。

因為,在 iOS 系統中,dispatch_block_t 是被當做物件管理的,而物件的生命週期是通過引用計數管理的。

所以,當一個物件初始化後,需要將其引用計數設定為1,防止其銷燬。

但是在其它平臺。它可以被當做結構體處理,所以,不需要引用計數設定為1。為了相容這兩種模式,通過 DISPATCH_RETURNS_RETAINED_BLOCK 可以檢測平臺併合理設定引用計數。

DISPATCH_ENUM

__has_feature__has_extension 是 clang 的語言特性。為了增強安全性,dispatch 希望能夠通過 typedef enum { ... } tagname; 語法進行宣告並定義列舉。但是為了相容一些不支援該語法的平臺版本,dispatch 通過巨集實現對其的相容處理。通過 clang 進行編譯時,

這裡以 block.h 檔案中的 DISPATCH_ENUM(dispatch_block_flags, unsigned long,...) 為例進行說明。當通過 clang 預處理時,其會被轉換為 typedef enum : unsigned long { __VA_ARGS__ } dispatch_block_flags_t;當通過其它平臺進行處理時,會被轉換為標準寫法 enum { __VA_ARGS__ }; typedef unsigned long dispatch_block_flags_t

dispatch_function_t

該行程式碼是 base.h 檔案中唯一的函式指標。其定義了一個名為 dispatch_function_t 的函式指標,它接收一個空指標,並且該函式返回為空(即無返回值)。

相關文章