前言
GCD 是一個跨平臺的庫,它使用了大量的系統底層知識以及編譯器優化內容。
本系列文章將主要分析該庫的一些值得學習的地方。取其精華,去其糟粕。
base
base.h
主要宣告瞭庫中常用的巨集。通過它,GCD 實現了對多平臺的相容。
attribute
__attribute__
是 GCC
的一大特色1,clang
實現大部分的描述符並新增了一些擴充套件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);
方法使用了該巨集。因為,我們在正常的應用開發中不會使用到該方法。所以,下面通過其它的示例程式碼對其進行說明。
1234DISPATCH_NORETURN extern void objc_terminate(void);int sun_terminate() {objc_terminate();}void objc_terminate(void)
是objc
庫的一個退出方法。在這裡,通過
extern
將其重新宣告並新增DISPATCH_NORETURN
巨集。int sun_terminater()
是我自定義的一個終止函式,它不接收引數,並返回int
型別的結果。在 clang 中,當函式宣告瞭返回值,卻沒有提供時,編譯器報出
Control reaches end of non-void function
的❗️錯誤提示。如下圖所示。 但是,這裡因為有被
noreturn
描述的函式objc_terminate();
的存在,原來的錯誤提示會被編譯器丟失,即開發者看不到原先的❗️錯誤提示。一種更復雜的情況是,當函式有分支處理時,需要所有的分支都包含有被
noreturn
描述的任意函式。否則,系統仍然會提示錯誤。如下圖所示。
nothrow
#define DISPATCH_NOTHROW __attribute__((__nothrow__))
巨集將 __attribute__((__nothrow__))
定義為 DISPATCH_NOTHROW
。
該描述符的含義是,函式不會丟擲異常。因為 iOS 中使用異常處理的情況比較少,所以這裡就不進行展開講解。
nonnull
1 2 3 4 5 6 7 8 |
#define DISPATCH_NONNULL1 __attribute__((__nonnull__(1))) #define DISPATCH_NONNULL2 __attribute__((__nonnull__(2))) #define DISPATCH_NONNULL3 __attribute__((__nonnull__(3))) #define DISPATCH_NONNULL4 __attribute__((__nonnull__(4))) #define DISPATCH_NONNULL5 __attribute__((__nonnull__(5))) #define DISPATCH_NONNULL6 __attribute__((__nonnull__(6))) #define DISPATCH_NONNULL7 __attribute__((__nonnull__(7))) #if __clang__ && __clang_major__ |
__nonnull__
用於指定函式或方法的入參型別不為空指標。
它有兩種寫法,__attribute__((__nonnull__))
或者 __attribute__((__nonnull__(1,2))
。
第一種要求所有的入參型別不為空指標,第二種要求指定位置的引數不為空指標。
比如下面的方法宣告中,要求第二個引數 block
不能為空指標。當 block
為空時,根本沒有必要呼叫該方法,所以這裡使用 DISPATCH_NONNULL2
對其進行限制。
1 2 3 4 |
DISPATCH_EXPORT DISPATCH_NONNULL2 DISPATCH_RETURNS_RETAINED_BLOCK DISPATCH_WARN_RESULT DISPATCH_NOTHROW dispatch_block_t dispatch_block_create(dispatch_block_flags_t flags, dispatch_block_t block); |
sentinel
#define DISPATCH_SENTINEL __attribute__((__sentinel__))
巨集將 __attribute__((__sentinel__))
定義為 DISPATCH_SENTINEL
。
__sentinel__
的中文意思是哨兵。
當接收可變引數時,可以通過它指定NULL所在的位置。
預設情況下,NULL所在的位置為從後往前數第0個為NULL。
常見於陣列或者字典等集合型別接受多個引數時指定最後一個為NULL。
通過它,函式內部可以知道何時終止引數的處理。
示例如下程式碼如下:
修飾符指定了從後往前數第三個為NULL,如果不滿足這個規則,編譯器會產生警告⚠️。
pure
#define DISPATCH_PURE __attribute__((__pure__))
巨集將 __attribute__((__pure__))
定義為 DISPATCH_PURE
。
__pure__
的中文意思是純淨的。它只依賴於入參和全域性變數進行處理。並且不會對外部造成影響。
pure
修飾的函式不具有可重入性,但是可以通過關中斷、訊號量(即P、V操作)等手段對全域性變數加以保護以實現。
當被 pure
修飾的函式的返回結果沒有被使用時,編譯器會直接丟棄呼叫被 pure
修飾的函式的相關程式碼。
下面以 Implications of pure and constant functions中的例子進行說明。
1 2 3 4 5 6 7 8 9 10 11 |
int someimpurefunction(int a, int b); int somepurefunction(int a, int b) __attribute__((pure)); int testfunction(int a, int b, int c) { int res1 = someimpurefunction(c, b); int res2 = somepurefunction(b, c); int res3 = a + b - c; return a; } |
程式碼分析:
我們很容易發現,testfunction
函式內的部分程式碼是可以被忽略的。
比如 int res2 = somepurefunction(b, c);
,只有返回值 res2
是由該函式生成的,在它的內部不會影響全域性變數,所以該行程式碼可以全部忽略。
程式碼行:int res2 = somepurefunction(b, c);
在執行過程中可能影響全域性變數,所以無法忽略,但是其返回值沒有用處,所以,返回值可以忽略返回值。
程式碼行: int res3 = a + b - c;
很明顯,因為返回結果 res3 沒有使用,該行程式碼可以忽略掉。
所以,編譯優化後程式碼為:
1 2 3 4 5 6 7 |
int someimpurefunction(int a, int b); int testfunction(int a, int b, int c) { someimpurefunction(c, b); return a; } |
const
DISPATCH_CONST __attribute__((__const__))
巨集將 __attribute__((__const__))
定義為 DISPATCH_CONST
。
__const__
與 __pure__
類似,但是 __const
不能依賴於全域性變數。也就意味著,const
修飾的函式是冪等的,具有可重入性。
下面仍然通過改造 Implications of pure and constant functions 中的例子進行說明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
const struct { const char *str; int val; } strings[] = { { "foo", 31 }, { "bar", 34 }, { "baz", -24 } }; const char*lookup(int val) __attribute__ ((const)); int count = 0; const char*lookup(int val) { printf("lookup被呼叫\n"); int i; for(i = 0; i < sizeof(strings)/sizeof(*strings); i++) if(strings[i].val == val) return strings[i].str; return NULL; } void testfunction(int val, const char * *str, unsigned long *len) { printf("testfunction被呼叫\n"); if(lookup(val)) { *str = lookup(val); *len = strlen(lookup(val)); } } |
輸出日誌:
1 2 |
testfunction被呼叫 lookup被呼叫 |
編譯器發現,lookup(val)
被呼叫了三次,且在該過程中,val
的值沒有發生變化。所以編譯器只呼叫了一次該函式,並快取了其結果。
特殊情況1
請注意,雖然 const
修飾的函式是冪等的,但是當被 const
修飾的函式有入參時,我沒有發現能夠合理優化如下程式碼的編譯器。
在下面的程式碼中, square
函式的入參並沒有發生變化,所以,合理的優化應該是隻呼叫一次 square
函式,並快取結果以提供給後續的程式碼使用。但實際上,square
函式被呼叫了1000次。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int square (int i) __attribute__ ((const)); int count = 0; int square (int i){ printf("square被呼叫%d次\n", count++); return i; } void test(){ for(int i = 0; i < 1000; i++) { int result = square(31415); printf("square(31415)=%d\n", result); } } |
特殊情況2
當被 const
修飾的函式有沒有入參時,編譯結果卻發生了非常大的變化。編譯器會盡可能的在編譯期間執行裡面的程式碼,並將結果直接賦值給呼叫方。
示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int retainConst () __attribute__ ((const)); int count = 0; int retainConst(){ int i = 100; printf("retainConst被呼叫%d次\n", count++); return i*i; } void test1(){ for(int i = 0; i < 1000; i++) { int result1 = retainConst(); printf("retainConst()=%d\n", result1); } } |
優化結果:
1 2 3 4 5 |
void test1(){ for(int i = 0; i < 1000; i++) { printf("retainConst()=%d\n", 1000); } } |
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
1 2 3 4 5 6 7 |
int fn () __attribute__ ((warn_unused_result)); int foo () { if (fn () < 0) return -1; fn (); return 0; } |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#if TARGET_OS_WIN32 && defined(__DISPATCH_BUILDING_DISPATCH__) && \ defined(__cplusplus) #define DISPATCH_EXPORT extern "C" extern __declspec(dllexport) #elif TARGET_OS_WIN32 && defined(__DISPATCH_BUILDING_DISPATCH__) #define DISPATCH_EXPORT extern __declspec(dllexport) #elif TARGET_OS_WIN32 && defined(__cplusplus) #define DISPATCH_EXPORT extern "C" extern __declspec(dllimport) #elif TARGET_OS_WIN32 #define DISPATCH_EXPORT extern __declspec(dllimport) #elif __GNUC__ #define DISPATCH_EXPORT extern __attribute__((visibility("default"))) #else #define DISPATCH_EXPORT extern #endif |
DISPATCH_EXPORT
可以在檔案中宣告一個全域性變數或函式。
DISPATCH_INLINE
1 2 3 4 5 |
#if __GNUC__ #define DISPATCH_INLINE static __inline__ #else #define DISPATCH_INLINE static inline #endif |
DISPATCH_INLINE
可以將函式宣告為靜態行內函數。
__builtin_expect((x), (v))
#define DISPATCH_EXPECT(x, v) __builtin_expect((x), (v))
巨集將 __builtin_expect((x), (v))
定義為 DISPATCH_EXPECT(x, v)
1 2 3 4 5 |
#if __GNUC__ #define DISPATCH_EXPECT(x, v) __builtin_expect((x), (v)) #else #define DISPATCH_EXPECT(x, v) (x) #endif |
__builtin_expect
可以指定某個值的預期值。這樣可以讓編譯器對相關的分支程式碼進行優化。
下面部分需要有部分的處理器相關的知識才能理解該部分。
在現代的處理器設計中,因為 cpu 和記憶體的速度不匹配。所以現在的 cpu 會讀取多條指令並執行(多流水線)。比如,遇到分支時,它會同時計算分支的條件,並選擇第一個分支執行。
比如下面的程式碼,__builtin_expect(x, 5)
程式碼告訴編譯器,x 的值具有很大的概率是5。
1 2 3 4 5 6 |
switch (__builtin_expect(x, 5)) { default: break; case 0: // ... case 3: // ... case 5: // ... } |
如果沒有任何優化,處理器會經常進入default分支執行,導致資源浪費。在這種情況下,編譯後的程式碼會調整分支的順序以達到提高資源效率的最優化。
1 2 3 4 5 6 |
switch (__builtin_expect(x, 5)) { case 5: // ... default: break; case 0: // ... case 3: // ... } |
ns_returns_retained
1 2 3 4 5 6 7 8 9 10 11 |
#ifndef DISPATCH_RETURNS_RETAINED_BLOCK #if defined(__has_attribute) #if __has_attribute(ns_returns_retained) #define DISPATCH_RETURNS_RETAINED_BLOCK __attribute__((__ns_returns_retained__)) #else #define DISPATCH_RETURNS_RETAINED_BLOCK #endif #else #define DISPATCH_RETURNS_RETAINED_BLOCK #endif #endif |
__ns_returns_retained__
告訴編譯器,該函式的返回值需要進行retain
操作。
因為,在 iOS 系統中,dispatch_block_t
是被當做物件管理的,而物件的生命週期是通過引用計數管理的。
所以,當一個物件初始化後,需要將其引用計數設定為1,防止其銷燬。
但是在其它平臺。它可以被當做結構體處理,所以,不需要引用計數設定為1。為了相容這兩種模式,通過 DISPATCH_RETURNS_RETAINED_BLOCK
可以檢測平臺併合理設定引用計數。
1 2 3 4 5 |
__OSX_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) DISPATCH_EXPORT DISPATCH_NONNULL2 DISPATCH_RETURNS_RETAINED_BLOCK DISPATCH_WARN_RESULT DISPATCH_NOTHROW dispatch_block_t dispatch_block_create(dispatch_block_flags_t flags, dispatch_block_t block); |
DISPATCH_ENUM
1 2 3 4 5 6 7 8 |
#if defined(__has_feature) && defined(__has_extension) #if __has_feature(objc_fixed_enum) || __has_extension(cxx_strong_enums) #define DISPATCH_ENUM(name, type, ...) \ typedef enum : type { __VA_ARGS__ } name##_t #else #define DISPATCH_ENUM(name, type, ...) \ enum { __VA_ARGS__ }; typedef type name##_t #endif |
__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
1 |
typedef void (*dispatch_function_t)(void *); |
該行程式碼是 base.h
檔案中唯一的函式指標。其定義了一個名為 dispatch_function_t
的函式指標,它接收一個空指標,並且該函式返回為空(即無返回值)。