本文属于原创文章。转载请联系作者。
本文首发地址为:http://www.jianshu.com/p/225c209085f9
2016年09月20日 添加
sentinel
的特殊用法,__attribute__ ((sentinel(0, 1)))
前言
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);
方法使用了该宏。
因为,我们在正常的应用开发中不会使用到该方法。所以,下面通过其它的示例代码对其进行说明。
DISPATCH_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
#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__ < 3
// rdar://problem/6857843
#define DISPATCH_NONNULL_ALL
#else
#define DISPATCH_NONNULL_ALL __attribute__((__nonnull__))
#endif
__nonnull__
用于指定函数或方法的入参类型不为空指针。
它有两种写法,__attribute__((__nonnull__))
或者 __attribute__((__nonnull__(1,2))
。
第一种要求所有的入参类型不为空指针,第二种要求指定位置的参数不为空指针。
比如下面的方法声明中,要求第二个参数 block
不能为空指针。当 block
为空时,根本没有必要调用该方法,所以这里使用 DISPATCH_NONNULL2
对其进行限制。
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,如果不满足这个规则,编译器会产生警告⚠️。
一个特殊的使用方法为:
void f1(void *a, ...) __attribute__ ((sentinel(0, 1)));
sentinel(n, 0) 和 sentinel(n, 1) 之间的区别是处理变长参数前面最后一个被命名参数的方式不同。比如,下面示例中的
void* a
。sentinel(0, 0) 意味着该参数 不被包含到空值中断列表中。sentinel(0,1) 意味着该参数被包含到空值中断列表中。
在下面的示例中,第二个函数使用
sentinel(0, 1)
进行修饰,用容易理解的读法就是,可变列表中,最后一个NULL
后面有0
个变量,该NULL
值可以出现在void *a
的位置也可以。
而第一个函数使用
sentinel(0, 0)
进行修饰,用容易理解的读法就是,**void *a
后面的可变列表中,最后一个NULL
后面有0
个变量**。因为调用函数时,只传了一个NULL
,参数,该参数只满足了对void *a
的赋值,却不满足从后往前数第0个为NULL
,所以会产生可变参数不够的警告⚠️。
pure
#define DISPATCH_PURE __attribute__((__pure__))
宏将 __attribute__((__pure__))
定义为 DISPATCH_PURE
。
__pure__
的中文意思是纯净的。它只依赖于入参和全局变量进行处理。并且不会对外部造成影响。
pure
修饰的函数不具有可重入性,但是可以通过关中断、信号量(即P、V操作)等手段对全局变量加以保护以实现。
当被 pure
修饰的函数的返回结果没有被使用时,编译器会直接丢弃调用被 pure
修饰的函数的相关代码。
下面以 Implications of pure and constant functions中的例子进行说明。
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 没有使用,该行代码可以忽略掉。
所以,编译优化后代码为:
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 中的例子进行说明。
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));
}
}
输出日志:
testfunction被调用
lookup被调用
编译器发现,lookup(val)
被调用了三次,且在该过程中,val
的值没有发生变化。所以编译器只调用了一次该函数,并缓存了其结果。
特殊情况1
请注意,虽然 const
修饰的函数是幂等的,但是当被 const
修饰的函数有入参时,我没有发现能够合理优化如下代码的编译器。
在下面的代码中, square
函数的入参并没有发生变化,所以,合理的优化应该是只调用一次 square
函数,并缓存结果以提供给后续的代码使用。但实际上,square
函数被调用了1000次。
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
修饰的函数有没有入参时,编译结果却发生了非常大的变化。编译器会尽可能的在编译期间执行里面的代码,并将结果直接赋值给调用方。
示例代码:
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);
}
}
优化结果:
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
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
#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
#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)
#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。
switch (__builtin_expect(x, 5)) {
default: break;
case 0: // ...
case 3: // ...
case 5: // ...
}
如果没有任何优化,处理器会经常进入default分支执行,导致资源浪费。在这种情况下,编译后的代码会调整分支的顺序以达到提高资源效率的最优化。
switch (__builtin_expect(x, 5)) {
case 5: // ...
default: break;
case 0: // ...
case 3: // ...
}
ns_returns_retained
#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
可以检测平台并合理设置引用计数。
__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
#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
typedef void (*dispatch_function_t)(void *);
该行代码是 base.h
文件中唯一的函数指针。其定义了一个名为 dispatch_function_t
的函数指针,它接收一个空指针,并且该函数返回为空(即无返回值)。