GCC __builtin_expect与kernel指令序列优化
在内核级的开发过程中,代码运行速度尤为重要,让我们先看两段汇编代码。
.LC1: .string "Hula!" .LC2: .string "Woo~" call __isoc99_scanf movl 28(%esp), %eax testl %eax, %eax je .L2 movl $.LC1, (%esp) call puts .L3: xorl %eax, %eax leave ret .L2: movl $.LC2, (%esp) call puts jmp .L3
.LC1: .string "Hula!" .LC2: .string "Woo~" call __isoc99_scanf movl 28(%esp), %eax testl %eax, %eax jne .L5 movl $.LC2, (%esp) call puts .L3: xorl %eax, %eax ret .L5: movl $.LC1, (%esp) call puts jmp .L3
上面两段代码为了方便阅读,都省略了一些与本次主题无关的代码。稍微阅读一下就可以发现,这两段代码做的事情其实是一样的。这是一个简单的if分支结构的编译结果,两段代码的差异在于是讲if段还是else段放在前面。这两者有着什么样的区别呢?
这要从现代的处理器架构说起。相信大家都知道流水线技术,就是CPU可以在统一个时钟周期内同时执行多条指令,当前指令尚未执行完毕,实际上就已经开始处理后面的指令了。然而当处理器遇到分支的时候,就无法判断即将执行的是哪个分支,流水线优化就受到了限制。
后来,随着处理器技术的发展,处理器开始直接预取分支后面的指令,如果发现分支预判错误,则抛弃之前的执行结果,重新转入正确的分支继续执行。更加现代的处理器甚至能够预取更多后面的指令,对于不依赖之前执行结果的指令都可以按照一定的规则预先执行得到结果。
再看上面的例子,我们就明白了,直接连接在je或者jne指令后面的分支可以在分支条件判断结束之前就开始运行,因此执行速度会更快,相反另一条分支则会慢一些。
在这个例子中两个分支都非常短,在更复杂的情况下,如果单个分支就很长,那么预取正确的指令还有助于Cache的命中。
GCC提供了__builtin_expect宏,作为编译分支时候的暗示。用法是__builtin_expect(var, expected_value),也就是说,告诉编译器var这个变量的值比较可能是什么。在kernel中这个宏被用在likely和unlikely这两个宏定义中:
#define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)
显而易见,这两个宏的意思是,条件x是很有可能成立,还是很有可能不成立。
int main() { int a; scanf("%d", &a); if (likely(a)) { printf("Hula!\n"); } else { printf("Woo~\n"); } return 0; }
上面第一段汇编代码,实际上就是这个程序编译产生的。相应的,把likely改为unlikely,就可以得到第二段的汇编代码。
在内核中,比如就绪队列不太可能为空,runqueue里的所有任务很有可能都在CFS queue里,以及一些很少见的竞争与冒险的情况,都有针对性地使用这种技术进行了优化。