[PHP 源码] EXPECTED 和 UNEXPECTED 到底是啥?
背景
我们经常能在php或者swoole源码看到里看到EXPECTED/UNEXPECTED/likely/unlikely这样的宏,比如说下面的情景:
// zend/zend_alloc.c/zend_mm_alloc_small
// small规格内存缓存在free_slot链表中
if (EXPECTED(heap->free_slot[bin_num] != NULL)) {
zend_mm_free_slot *p = heap->free_slot[bin_num];
heap->free_slot[bin_num] = p->next_free_slot;
return (void*)p;
} else {
return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}
这段代码是用来获取small规格的内存,如果在free_slot上没获取到,那么再去上一级”缓慢”的获取,转化为我们业务代码,有点类似于从数据库获取数据,以下是伪代码:
if (EXPECTED($userInfo = $redis->get($userId))) {
return $userInfo;
} else {
$userInfo = $db->query("select * from user where id = {$userId}");
$redis->set($userId, $userInfo);
return $userInfo;
}
我们在平时业务中,经常会写类似这样的代码:数据从数据库获取一次之后就会被放入缓存,以后每次读取都是从缓存中获取。其实就是说:每次执行这段代码,大概率是走从缓存获取数据的分支。
这就是EXPECTED的语意,UNEXPECTED则相反。
解释
我们首先来看一下这个宏展开是什么
# define EXPECTED(condition) __builtin_expect(!!(condition), 1)
# define UNEXPECTED(condition) __builtin_expect(!!(condition), 0)
__builtin_expect
是GCC编译器的一个内置宏,原型是
long __builtin_expect(long exp, long c);
这个函数的用法和解释是:
1. 这个函数的返回值就是exp
2. 告诉编译器期望exp等于c
举个例子:
// 不期望执行foo函数
if (__builtin_expect(x, 0)) {
foo();
}
// 期望执行bar函数
if (__builtin_expect(y, 1)) {
bar();
}
那么这个宏使用之后有什么效果呢?我们先从缓存层级开始说起
每次访问数据,cpu都会从最顶层开始获取,这层没有再去下一层获取,获取到数据又在这一层缓存起来,每一层都作为它下一层的缓存,这样一层一层向下访问。造成这样的结果是有原因的:离CPU越近的存储器,速度越快,每字节的成本越高,同时容量也因此越小。寄存器速度最快,离CPU最近,成本最高,大小非常有限,其次是高速缓存(缓存也是分级,有L1,L2等缓存),再次是主存(普通内存),再次是本地磁盘。
寄存器和cache的速度一般是内存的几十倍甚至上百倍,如果我们能够有效的利用这个缓存来为我们程序加速,可能会带来很大提升。
如何使用好cache呢?一般遵循时间局部性和空间局部性两个原则
- 时间局部性:被引用过一次的存储器位置在未来会被多次引用(通常在循环中)
- 空间局部性:如果一个存储器的位置被引用,那么将来他附近的位置也会被引用
出于空间局部性考虑,操作系统会在获取数据时选择缓存数据附近位置的数据,典型的例子是读磁盘,每次都是读取一个连续的页。当然,指令也不例外。试想:如果代码没有任何跳转,是从上到下一条条指令执行的话,缓存亲和性是比较高的,但如果出现了跳转,就可能导致预缓存的指令根本用不上,又要从内存读一遍,执行一条指令只需要一个时钟周期,但是读内存却需要几十个时钟周期,如果每次加载指令都需要从内存读的话,这效率也太慢了,大部分时间都是在等内存。
如果有一种情况,我们已经知道了代码运行时各个分支的执行情况,事先就按这个顺序把指令安排好位置,那么我们是不是就可以避免cache miss的尴尬了?
__builtin_expect
的产生也是因为这种情况,因为有些分支我们是事先知道他的执行概率的,比如说
if (malloc(sizeof(int)) == NULL) {
// error handle
}
再比如说参数检查,像这种出错情况本身就很少,如果还把出错处理的指令安排在紧接着当前指令之后的话,会增加没必要的跳转,缓存亲和性会很差,白白浪费了宝贵的cache。
总结
对于一些程序执行前我们就知道的非常有可能执行或者不执行的分支,我们可以用__builtin_expect
来优化,增加缓存亲和性。其实提升缓存亲和性一直以来都是非常重要的一种优化手段,它对于cpu密集型程序可能会有非常大的提升,php7比起php5在这方面就做了非常多的优化,比如说hashTable结构的改变,从原来的大量随机内存IO,变成了顺序结构,访问的内存空间都是连续的,执行效率提升非常多。
本作品采用《CC 协议》,转载必须注明作者和本文链接