在linux内核源码或一些比较成熟的c语言架构源码中,我们常会见到类似下面的代码:
1 if (unlikely(!packet)) { 2 return res_failed; 3 } 4 5 // OR 6 7 if (likely(packet->type = HTTP)) { 8 do_something(); 9 }
有的地方可能会用大写,LIKELY() / UNLIKELY(),意思一样。
然后我们看一下unlikely和likely的定义,大部分都是类似如下的宏定义:
1 #define likely(x) __builtin_expect(!!(x), 1) 2 #define unlikely(x) __builtin_expect(!!(x), 0)
GCC 中用的是 _G_BOOLEAN_EXPR(expr) 来代替 !!(expr), 意思一样,都是把expr或x转成相应布尔变量。
两个定义无一例外调用了一个内置函数 __builtin_expect(bool expr, int x)。
先解释一下: LIKELY 和 UNLIKELY 不会对原expr的布尔值产生任何影响,也就是说只要expr == true, LIKELY(expr) 与 UNLIKELY(expr) 都为 true。他们起的只是编译器优化作用。
我们先测试一段代码:
1 /** 2 * @author Lhfcws 3 * @file test__builtin_expect.c 4 * @time 2013-07-22 5 **/ 6 7 #define LIKELY(x) __builtin_expect(!!(x), 1) 8 #define UNLIKELY(x) __builtin_expect(!!(x), 0) 9 10 int test_likely(int x) { 11 if(LIKELY(x)) 12 x = 0x00; 13 else 14 x = 0xff; 15 16 return x; 17 } 18 19 int test_unlikely(int x) { 20 if(UNLIKELY(x)) 21 x = 0x00; 22 else 23 x = 0xff; 24 25 return x; 26 } 27 28 int test_justif(int x) { 29 if(x) 30 x = 0x00; 31 else 32 x = 0xff; 33 34 return x; 35 }
可见,三个函数唯一的区别就是 if (x) 那里。
我们执行一下命令编译和反汇编(要用 __builtin_expect 的话 -fprofile-arcs 必须加):
gcc -fprofile-arcs -c test__builtin_expect.c -o test__builtin_expect.o objdump -d test__builtin_expect > builtin.asm
此时打开生成的asm文件,就可以看到上面代码由gcc编译生成的汇编代码。我们截取那三个函数的汇编源码查看。
1 00000000 <test_likely>: 2 0: 55 push %ebp 3 1: 89 e5 mov %esp,%ebp 4 3: 83 7d 08 00 cmpl $0x0,0x8(%ebp) 5 7: 0f 95 c0 setne %al 6 a: 0f b6 c0 movzbl %al,%eax 7 d: 85 c0 test %eax,%eax 8 f: 74 09 je 1a <test_likely+0x1a> 9 11: c7 45 08 00 00 00 00 movl $0x0,0x8(%ebp) 10 18: eb 23 jmp 3d <test_likely+0x3d> 11 1a: c7 45 08 ff 00 00 00 movl $0xff,0x8(%ebp) 12 21: a1 20 00 00 00 mov 0x20,%eax 13 26: 8b 15 24 00 00 00 mov 0x24,%edx 14 2c: 83 c0 01 add $0x1,%eax 15 2f: 83 d2 00 adc $0x0,%edx 16 32: a3 20 00 00 00 mov %eax,0x20 17 37: 89 15 24 00 00 00 mov %edx,0x24 18 3d: 8b 4d 08 mov 0x8(%ebp),%ecx 19 40: a1 28 00 00 00 mov 0x28,%eax 20 45: 8b 15 2c 00 00 00 mov 0x2c,%edx 21 4b: 83 c0 01 add $0x1,%eax 22 4e: 83 d2 00 adc $0x0,%edx 23 51: a3 28 00 00 00 mov %eax,0x28 24 56: 89 15 2c 00 00 00 mov %edx,0x2c 25 5c: 89 c8 mov %ecx,%eax 26 5e: 5d pop %ebp 27 5f: c3 ret 28 29 00000060 <test_unlikely>: 30 60: 55 push %ebp 31 61: 89 e5 mov %esp,%ebp 32 63: 83 7d 08 00 cmpl $0x0,0x8(%ebp) 33 67: 0f 95 c0 setne %al 34 6a: 0f b6 c0 movzbl %al,%eax 35 6d: 85 c0 test %eax,%eax 36 6f: 74 09 je 7a <test_unlikely+0x1a> 37 71: c7 45 08 00 00 00 00 movl $0x0,0x8(%ebp) 38 78: eb 23 jmp 9d <test_unlikely+0x3d> 39 7a: c7 45 08 ff 00 00 00 movl $0xff,0x8(%ebp) 40 81: a1 10 00 00 00 mov 0x10,%eax 41 86: 8b 15 14 00 00 00 mov 0x14,%edx 42 8c: 83 c0 01 add $0x1,%eax 43 8f: 83 d2 00 adc $0x0,%edx 44 92: a3 10 00 00 00 mov %eax,0x10 45 97: 89 15 14 00 00 00 mov %edx,0x14 46 9d: 8b 4d 08 mov 0x8(%ebp),%ecx 47 a0: a1 18 00 00 00 mov 0x18,%eax 48 a5: 8b 15 1c 00 00 00 mov 0x1c,%edx 49 ab: 83 c0 01 add $0x1,%eax 50 ae: 83 d2 00 adc $0x0,%edx 51 b1: a3 18 00 00 00 mov %eax,0x18 52 b6: 89 15 1c 00 00 00 mov %edx,0x1c 53 bc: 89 c8 mov %ecx,%eax 54 be: 5d pop %ebp 55 bf: c3 ret 56 57 000000c0 <test_justif>: 58 c0: 55 push %ebp 59 c1: 89 e5 mov %esp,%ebp 60 c3: 83 7d 08 00 cmpl $0x0,0x8(%ebp) 61 c7: 74 09 je d2 <test_justif+0x12> 62 c9: c7 45 08 00 00 00 00 movl $0x0,0x8(%ebp) 63 d0: eb 23 jmp f5 <test_justif+0x35> 64 d2: c7 45 08 ff 00 00 00 movl $0xff,0x8(%ebp) 65 d9: a1 00 00 00 00 mov 0x0,%eax 66 de: 8b 15 04 00 00 00 mov 0x4,%edx 67 e4: 83 c0 01 add $0x1,%eax 68 e7: 83 d2 00 adc $0x0,%edx 69 ea: a3 00 00 00 00 mov %eax,0x0 70 ef: 89 15 04 00 00 00 mov %edx,0x4 71 f5: 8b 4d 08 mov 0x8(%ebp),%ecx 72 f8: a1 08 00 00 00 mov 0x8,%eax 73 fd: 8b 15 0c 00 00 00 mov 0xc,%edx 74 103: 83 c0 01 add $0x1,%eax 75 106: 83 d2 00 adc $0x0,%edx 76 109: a3 08 00 00 00 mov %eax,0x8 77 10e: 89 15 0c 00 00 00 mov %edx,0xc 78 114: 89 c8 mov %ecx,%eax 79 116: 5d pop %ebp 80 117: c3 ret
如上,我们看到,貌似test_likely 和 test_unlikely 没什么区别, test_justif就是少了setne al开始的三行代码而已(实际上是执行__builtin_expect(!!(x), 1)的代码)。
其实这证明了一件事: LIKELY 和 UNLIKELY 的调用不会影响最终结果,实际两者的结果是一样的。
我们之前提到他们起的作用是优化,因此我们编译的时候加上优化指令。
gcc -O2 -fprofile-arcs -c test__builtin_expect.c -o test__builtin_expect.o objdump -d test__builtin_expect > builtin_O2.asm
得到汇编:
1 00000000 <test_likely>: 2 0: 83 ec 04 sub $0x4,%esp 3 3: 8b 44 24 08 mov 0x8(%esp),%eax 4 7: 83 05 00 00 00 00 01 addl $0x1,0x0 5 e: 83 15 04 00 00 00 00 adcl $0x0,0x4 6 15: 85 c0 test %eax,%eax 7 17: 74 06 je 1f <test_likely+0x1f> 8 19: 31 c0 xor %eax,%eax 9 1b: 83 c4 04 add $0x4,%esp 10 1e: c3 ret 11 1f: 83 05 08 00 00 00 01 addl $0x1,0x8 12 26: b8 ff 00 00 00 mov $0xff,%eax 13 2b: 83 15 0c 00 00 00 00 adcl $0x0,0xc 14 32: eb e7 jmp 1b <test_likely+0x1b> 15 34: 8d b6 00 00 00 00 lea 0x0(%esi),%esi 16 3a: 8d bf 00 00 00 00 lea 0x0(%edi),%edi 17 18 00000040 <test_unlikely>: 19 40: 83 ec 04 sub $0x4,%esp 20 43: 8b 54 24 08 mov 0x8(%esp),%edx 21 47: 83 05 10 00 00 00 01 addl $0x1,0x10 22 4e: 83 15 14 00 00 00 00 adcl $0x0,0x14 23 55: 85 d2 test %edx,%edx 24 57: 75 17 jne 70 <test_unlikely+0x30> 25 59: 83 05 18 00 00 00 01 addl $0x1,0x18 26 60: b8 ff 00 00 00 mov $0xff,%eax 27 65: 83 15 1c 00 00 00 00 adcl $0x0,0x1c 28 6c: 83 c4 04 add $0x4,%esp 29 6f: c3 ret 30 70: 31 c0 xor %eax,%eax 31 72: eb f8 jmp 6c <test_unlikely+0x2c> 32 74: 8d b6 00 00 00 00 lea 0x0(%esi),%esi 33 7a: 8d bf 00 00 00 00 lea 0x0(%edi),%edi 34 35 00000080 <test_justif>: 36 80: 83 ec 04 sub $0x4,%esp 37 83: 8b 4c 24 08 mov 0x8(%esp),%ecx 38 87: 83 05 20 00 00 00 01 addl $0x1,0x20 39 8e: 83 15 24 00 00 00 00 adcl $0x0,0x24 40 95: 31 c0 xor %eax,%eax 41 97: 85 c9 test %ecx,%ecx 42 99: 75 10 jne ab <test_justif+0x2b> 43 9b: 83 05 28 00 00 00 01 addl $0x1,0x28 44 a2: b0 ff mov $0xff,%al 45 a4: 83 15 2c 00 00 00 00 adcl $0x0,0x2c 46 ab: 83 c4 04 add $0x4,%esp 47 ae: c3 ret
现在三个函数就有很明显的不同了。
留意一下每个函数其中的三行代码:
1 je ... / jne ... ; 跳转 2 xor %eap, %eap ; 其实是 mov 0x00, %eap,改用 xor 是编译器自己的优化。结果等价。 3 mov 0xff, %eap ; x = 0xff
可以看到,likely版本和unlikely版本最大的区别是跳转的不同。
likely版本编译器会认为执行 x == true 的可能性比较大,因此将 x == false 的情况作为分支,减少跳转开销。
同理,unlikely版本编译器会认为执行 x == false 的可能性比较大,因此将 x == true 的情况作为分支,减少跳转开销。
总结:
likely 和 unlikely 的使用实际上是为了分支优化,不影响结果,据传,众多程序员平常很少注意分支优化情况,因此gcc有了这个选项。。。
unlikely 一般适用于(但不仅限于)一些错误检查,比如本文开头示例。likely适用于主分支场景,即根据期望大部分情况都会执行的场景。