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

Are arguments loaded into the cache for empty 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. 当我指定 -DNO_INC 选项时,调用 T::inc(...) 不会对性能产生负面影响,因为对空函数的调用已优化,我的假设是否正确?
  2. 我想知道当调用T::inc(a, i)时变量(ai)是否仍然加载到缓存中(假设它们还没有)虽然函数体是空.

感谢任何建议!

因为使用了 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    [=10=]x2a,%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 以提高可读性)

Compiler Explorer is an very useful tool to look at the assembly of your generated program, because there is no other way to figure out if the compiler optimized something or not for sure. 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

编译器优化了整个循环!

Is my assumption correct that calls to t.inc(...) do not affect the performance negatively when I specify the -DNO_INC option because the call to the empty function is optimized?

是的。

If my assumption holds, does it also hold for more complex function bodies (in the #else branch)?

不,对于 "complex" 的一些定义。编译器使用启发式方法来确定是否值得内联一个函数,并以此为基础做出决定。

I wonder if the variables (a and i) are still loaded into the cache when t.inc(a, i) is called (assuming they are not there yet) although the function body is empty.

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

Is my assumption correct that calls to t.inc(...) do not affect the performance negatively when I specify the -DNO_INC option because the call to the empty function is optimized? If my assumption holds, does it also hold for more complex function bodies (in the #else branch)?

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

编译器优化了所有的东西

main:                                   # @main
        movl    , %eax
        retq

只有 42 在 eax 和 returned 中被引入。

然而,对于更复杂的情况,需要更多指令来计算 return 值。 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    , %eax
        retq
.LBB0_1:
        movl    , %eax    
        retq

I wonder if the variables (a and i) are still loaded into the cache when t.inc(a, i) is called (assuming they are not there yet) although the function body is empty.

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

顺便说一下:您不需要创建 T 的实例(即 T t;)来调用 [=33= 中的静态函数].这是在破坏目的。称它为 T::inc(...) 而不是 t.inc(...).