最近在优化一段代码时,发现一个完成同样功能的函数,在使用不同的传参方式的时候,编译器生成的汇编代码有较大的差异,经过多方查阅资料才解决了心中的疑惑。

该函数将两个无符号整数相加,将结果保存到第三个参数当中,并返回进位,代码如下所示。感谢Compiler Explorer提供的生成汇编代码的便利工具,其中编译器为x86-64 gcc 8.2,编译选项为-O3。

unsigned add(const unsigned a, const unsigned b, unsigned &c) {
    c = a + b;
    return c < a;
}

unsigned add(const unsigned &a, const unsigned &b, unsigned &c) {
    c = a + b;
    return c < a;
}
_Z3addjjRj:
    addl    %esi, %edi
    setc    %al
    movl    %edi, (%rdx)
    movzbl  %al, %eax
    ret
_Z3addRKjS0_Rj:
    movl    (%rsi), %eax
    addl    (%rdi), %eax
    movl    %eax, (%rdx)
    cmpl    (%rdi), %eax
    setb    %al
    movzbl  %al, %eax
    ret

上面两个函数完成相同的功能,唯一的区别在于参数a和参数b传递的方式不同,第一个函数采用了值传递的方式,第二个函数采用了引用传递的方式。但是,两个函数生成的汇编代码差异较大。第一个函数生成的汇编代码反映了代码的真实意图,执行完加法运算后,从CF标志位取出进位标志并返回;第二个函数则没有对代码进行优化,做法比较直白,当然效率也会变低。

既然两个函数只有参数传递方式不同,那问题肯定出在这里。除了值传递和引用传递外,还可以使用指针传递的方式,经过测试,使用指针传递时生成的代码也与上述第二种情况相同。经过不断查询资料和思考,最后发现了问题所在:当使用指针或引用进行参数传递的时候,编译器无法保证不同的指针是否指向相同的地址。在上述第二种情况种,如果三个参数引用的是同一个整数,那么对代码进行优化就会导致错误。

在C语言中,有一个类型限定符叫做restrict,用来提示编译器该对象只能通过这个特定的指针进行修改,编译器就可以做出针对这种情况的优化。虽然C++标准中暂时还没有引入这个特性,但一些编译器也提供了支持,例如g++提供的__restrict。对指针或引用添加了该限定符后,编译器优化掉了代码中的比较运算,如下所示。

unsigned add(const unsigned * __restrict a,
    const unsigned * __restrict b, unsigned *c) {
    *c = *a + *b;
    return *c < *a;
}
_Z3addPKjS0_Pj:
    movl    (%rsi), %eax
    addl    (%rdi), %eax
    movl    %eax, (%rdx)
    setc    %al
    movzbl  %al, %eax
    ret

解决了心中的疑惑,也要对自己的知识盲点进行补习,下面简单介绍一下restrict的含义。在cppreference中提到,“只有指向对象类型的指针才能是restrict限定”,也就是说这个限定符只能用于限定指针,而不能直接应用于对象(g++中提供的__restrict好像可以限定引用)。如果对指针进行了restrict限定,那么对该指针指向的对象的任何访问,都只能通过这个指针来实现。restrict限定符为编译器的优化提供了指示,如果编译器进行了优化,而实际的代码却违反了上述规定,计算出来的结果就有可能是错误的,在C标准中称为未定义行为。更多的内容请参考cppreference

2 个评论

回复 htliu 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注