一种条件语句的编译优化方式

看下面的C/C++代码片段:

if (a < b) {
    // a<b的处理逻辑
} else {
    // a>=b的处理逻辑
}if (a >= b) {
    // a>=b的处理逻辑
} else {
    // a<b的处理逻辑
}

它们有区别吗?

首先在业务逻辑上没有任何区别,无论哪一种形式的if条件语句都能达到要求,但是在程序执行性能上还是有区别的。我们可以写一个demo代码例子来看一下:

void foo(int a, int b) {
    if (a < b) {
        puts("a<b");
    } else {
        puts("a>=b");
    }
}

void bar(int a, int b) {
    if (a >= b) {
        puts("a>=b");
    } else {
    	puts("a<b");
    }
}

显然foo()和bar()函数它们的功能完全一样,使用GCC10 -O2进行编译,下面是生成的汇编语言:

.LC0:
        .string "a<b"
.LC1:
        .string "a>=b"
foo(int, int):
        cmp     edi, esi
        jge     .L2
        mov     edi, OFFSET FLAT:.LC0
        jmp     puts
.L2:
        mov     edi, OFFSET FLAT:.LC1
        jmp     puts
bar(int, int):
        cmp     edi, esi
        jl      .L5
        mov     edi, OFFSET FLAT:.LC1
        jmp     puts
.L5:
        mov     edi, OFFSET FLAT:.LC0
        jmp     puts

两个函数的参数a在寄存器edi中,参数b在寄存器esi中,先看第6、14行指令:cmp edi, esi是对a和b的值进行比较。

在foo()函数中,比较结果使用了指令:jge .L2(第7行),它的意思是如果a>=b(ge即大于等于,是greater equals的缩写),会跳到标号.L2指向的位置处(即第11行)处开始执行,否则(即a<b)继续执行。

而在bar()函数中,比较结果使用了指令:jl .L5(第15行),它的意思是如果a<b(l即小于,是less的缩写),会跳到标号.L5指向的位置处(即第19行)开始执行,否则(即a>=b)继续执行。

也就是说,foo()函数当a>=b时发生条件跳转,当a<b时继续执行,而bar()函数是当a<b时发生条件跳转,当a>=b时继续执行。既然foo()和bar()函数逻辑一样,那我们在实际开发中使用哪一种形式呢?答案是看情况,应该根据a<b或者a>=b哪一种更可能发生来选择使用哪一种形式。

我们知道,指令是以指令流的形式在CPU流水线中解码并执行的,如果指令流的流程没有发生跳转的话,它就会按照指令流中的指令顺序依次执行,并且在前面指令的执行过程中,CPU的解码单元还会预取后面的指令并预先进行译码操作,前面的指令执行完毕紧接着执行已经译码完成的后面的指令,一环扣一环的向前推进。如果指令流在执行过程中发生了跳转,显然跳转指令执行完成后,尽管它后面的指令已经译码完了,但是因为需要执行跳转后的指令,需要重新译码新的指令,不但前面预先译码的指令被废弃不用,而且CPU的流水线需要暂停下来等待新的译码操作完成,也就是执行和译码环节前后衔接不上了,造成了性能损失。因此,为了减少这样没必要的跳转造成的流水线性能损失,编译器在产生条件语句的汇编指令时,尽可能的让能够大概率满足条件的指令能够顺序执行,而小概率满足条件的指令放在跳转以后的地方去执行。

基于此,对于函数foo(),它的if语句是if (a<b),既然编程人员这么写,编译器就会认为a小于b是一个大概率事件,就产生了更有利于满足条件a<b的执行的指令流,第8行汇编指令就是a<b的分支逻辑,汇编指令6、7、8、9是顺序的执行流,没有被打断。那么,a>=b就被编译器认为是一个小概率的事件了,如果发生了a>=b的条件,指令流就从第7行跳转到第14行,中间发生了断档,没有顺次执行,它的执行效率肯定要比a<b时要低。如果在实际应用时,确实更可能a<b,那么编译器就猜对了,它产生的汇编指令流会有更好的性能,在实际执行时,指令流会大概率不会被中断,从而可以获得较高的执行性能。同样的原因,在函数bar()中,编译器生成了更有利于满足条件a>=b的执行的指令流。

因此,根据上面分析的结论,我们在编程实践中,如果能够预先知道那种条件发生的可能性更大,可以针对性的编写if条件语句,让编译器生成更有利于发生该条件时的指令流。比如,对于前面的例子,在编程时如果我们认为a小于b的概率非常大,那就采用foo()函数的写法,如果a大于等于b的概率非常大,那就采用bar()函数的写法。

不过,编译器并不是严格的按照程序使用的if语句的条件生成汇编指令流的,它也有特殊情况。如果是整数类型(如int、short、long等)在进行相等=和不等!=的比较时,编译器会认为不相等是一个大概率的事件,显然int型的取值范围是0-4G,因此如果a和b的值是均匀分布的话,它们相等的可能性是1/4G,在编译器看来是极低的概率。因此不管if语句是写成if (a==b),还是if (a!=b),编译器都生成更有利于a!=b的指令流。看下面的代码:

void foo(int a, int b) {
    if (a == b) {
        puts("a<b");
    } else {
        puts("a>=b");
    }
}

void bar(int a, int b) {
    if (a != b) {
        puts("a>=b");
    } else {
    	puts("a<b");
    }
}

编译器生成的汇编指令完全一样:

foo(int, int):
        cmp     edi, esi
        je      .L4
        mov     edi, OFFSET FLAT:.LC1
        jmp     puts
.L4:
        mov     edi, OFFSET FLAT:.LC0
        jmp     puts
bar(int, int):
        cmp     edi, esi
        je      .L6
        mov     edi, OFFSET FLAT:.LC1
        jmp     puts
.L6:
        mov     edi, OFFSET FLAT:.LC0
        jmp     puts

可见,编译器在第3行和第11行生成的汇编指令完全一样,都是更有利于执行a!=b的条件。同样编译器也认为一个指针是空指针的情况也是一个小概率事件,如果有判断一个指针p是否是空指针的if语句:if (p==nullptr),编译器也会生成更有利于执行p!=nullptr条件的指令流。

当然这些优化并不是C++标准规定,是否使用这样的优化策略,取决于编译器,并不是编译器的标准行为,在实际编程中如果需要这样的优化策略,就得要查看一下编译器优化后生成的汇编指令了,看编译器是否提供这样的优化策略,显然有点麻烦。好在C++20标准中提供了两种属性来支持这种情况的优化:likely和unlikey,程序员可以在他认为更有可能发生的if条件上标注属性[[likely]],而更不可能发生的if条件上标注[[unlikely]],来给编译器优化时进行指示。

比如前面说过,对于int型的相等比较,GCC编译器生成了更有利于不相等的指令流,如果编程人员认为在实际运行时二者相等概率更大,可以使用likely属性来对if语句进行标记,告诉编译器该条if语句为真的可能性更大,这样编译器就会生成更有利于该条件为真时执行的指令流。
代码片段如下:

void foo(int a, int b) {
    if (a ==b) [[likely]] { //  [[likely]] 告诉编译器,a==b的可能性更高
        puts("a==b");
    } else {
        puts("a!=b");
    }
}

生成的汇编指令如下:

foo(int, int):
        cmp     edi, esi
        jne     .L2
        mov     edi, OFFSET FLAT:.LC0
        jmp     puts
.L2:
        mov     edi, OFFSET FLAT:.LC1
        jmp     puts

同不使用[[likely]]的版本相比,第三行的条件跳转指令由je变成了jne,它的意思是,当a!=b时,跳转到.L2处执行,如果a== b继续执行后面的指令流,显然编译器遵循了[[likely]]的指示,生成的指令流更有利于a==b条件为真时的执行。

likely和unlikely属性是C++20标准引入的,如果开发低于该标准的C++代码,也可以使用下面的宏来指示条件判断为真的可能性:

#define likely(x)  __builtin_expect(!!(x), 1)   
#define unlikely(x)  __builtin_expect(!!(x), 0)

使用上面的宏来指示编译器生成if条件语句:

void foo(int a, int b) {
    if (likely(a==b)) {
        puts("a==b");
    } else {
        puts("a!=b");
    }
}

编译器生成的汇编指令如下:

foo(int, int):
        cmp     edi, esi
        jne     .L2
        mov     edi, OFFSET FLAT:.LC0
        jmp     puts
.L2:
        mov     edi, OFFSET FLAT:.LC1
        jmp     puts

和使用C++20的属性[[likely]]生成的汇编指令完全一样,生成的汇编指令流也是有利于a==b条件分支的执行。

综上,如果编写程序时,对程序的执行性能要求很高,在编写一些条件语句时,如果知道实际运行时该条件为真的可能性,可以针对性地添加likely或者unlikely标志来指示编译器来生成性能更好的条件跳转指令流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值