是否为空函数将参数加载到缓存中?

时间:2018-08-30 10:08:32

标签: c++ caching compiler-optimization static-functions

我知道C ++编译器会优化空(静态)函数。

基于此知识,我编写了一段代码,每当定义了一些标识符(使用编译器的-D选项)时,这些代码都应该进行优化。 考虑下面的虚拟示例:

#include <iostream>

#ifdef NO_INC

struct T {
    static inline void inc(int& v, int i) {}
};

#else

struct T {
    static inline void inc(int& v, int i) {
        v += i;
    }
};

#endif

int main(int argc, char* argv[]) {
    int a = 42;

    for (int i = 0; i < argc; ++i)
        T::inc(a, i);

    std::cout << a;
}

所需的行为如下: 只要定义了NO_INC标识符(在编译时使用-DNO_INC),就应该优化对T::inc(...)的所有调用(由于函数体为空)。否则,对T::inc(...)的调用应触发一个给定值i的增量。

我对此有两个问题:

  1. 我的假设是否正确,因为对空函数的调用已优化,因此在指定T::inc(...)选项时,对-DNO_INC的调用不会对性能产生负面影响?
  2. 我不知道在调用a时变量(iT::inc(a, i))是否仍被加载到缓存中(假定它们还不存在),尽管函数体为空。

谢谢您的建议!

3 个答案:

答案 0 :(得分:3)

Compiler Explorer是查看生成程序汇编的非常有用的工具,因为没有其他方法可以确定编译器是否确定了某些优化。 Demo

随着实际增加,您的main看起来像:

main:                                   # @main
        push    rax
        test    edi, edi
        jle     .LBB0_1
        lea     eax, [rdi - 1]
        lea     ecx, [rdi - 2]
        imul    rcx, rax
        shr     rcx
        lea     esi, [rcx + rdi]
        add     esi, 41
        jmp     .LBB0_3
.LBB0_1:
        mov     esi, 42
.LBB0_3:
        mov     edi, offset std::cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        xor     eax, eax
        pop     rcx
        ret

如您所见,编译器完全内联了对T::inc的调用,并直接进行了递增。

对于空的T::inc,您会得到:

main:                                   # @main
        push    rax
        mov     edi, offset std::cout
        mov     esi, 42
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        xor     eax, eax
        pop     rcx
        ret

编译器优化了整个循环!

  

我的假设是否正确,因为我优化了对空函数的调用,所以在指定t.inc(...)选项时,对-DNO_INC的调用不会对性能产生负面影响?

是的

  

如果我的假设成立,那么它是否也适用于更复杂的函数体(在#else分支中)?

否,对于“复杂”的某些定义。编译器使用启发式方法来确定是否值得内联函数,并将其决策基于该函数,而不是其他任何事物。

  

我不知道在调用a时变量(it.inc(a, i))是否仍被加载到缓存中(假设它们还不存在),尽管函数体为空。

不,如上所述,该循环甚至不存在。

答案 1 :(得分:3)

  

我的假设正确吗,因为对空函数的调用已优化,所以在指定-DNO_INC选项时对t.inc(...)的调用不会对性能产生负面影响?如果我的假设成立,那么它是否也适用于更复杂的函数体(在#else分支中)?

你是对的。我在compiler explorer中修改了您的示例(即删除了使程序混乱的cout),以使发生的情况更加明显。

编译器优化一切工作和出局

main:                                   # @main
        movl    $42, %eax
        retq

只有42位被带入eax并返回。

但是,对于更复杂的情况,需要更多指令来计算返回值。 See here

main:                                   # @main
        testl   %edi, %edi
        jle     .LBB0_1
        leal    -1(%rdi), %eax
        leal    -2(%rdi), %ecx
        imulq   %rax, %rcx
        shrq    %rcx
        leal    (%rcx,%rdi), %eax
        addl    $41, %eax
        retq
.LBB0_1:
        movl    $42, %eax    
        retq
  

我想知道,尽管函数体为空,但是在调用t.inc(a,i)时是否仍将变量(a和i)加载到缓存中(假设它们还不存在)。

仅当编译器无法推理未使用它们时才加载它们。请参阅第二个编译器资源管理器示例。

顺便:您无需创建T的实例(即T t;)即可在类中调用静态函数。这是失败的目的。像T::inc(...)t.inc(...)更叫它。

答案 2 :(得分:1)

由于使用了inline关键字,因此可以放心假设1。使用这些功能不会对性能产生负面影响。

通过运行代码

g ++ -c -Os -g

objdump -S

确认这一点;摘录:

int main(int argc, char* argv[]) {
    T t;
    int a = 42;
    1020:   b8 2a 00 00 00          mov    $0x2a,%eax
    for (int i = 0; i < argc; ++i)
    1025:   31 d2                   xor    %edx,%edx
    1027:   39 fa                   cmp    %edi,%edx
    1029:   7d 06                   jge    1031 <main+0x11>
        v += i;
    102b:   01 d0                   add    %edx,%eax
    for (int i = 0; i < argc; ++i)
    102d:   ff c2                   inc    %edx
    102f:   eb f6                   jmp    1027 <main+0x7>
        t.inc(a, i);
    return a;
}
    1031:   c3                      retq

(为了获得更好的可读性,我用return替换了cout)