0

我知道 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. 我想知道变量 (ai) 在被调用时是否仍然加载到缓存T::inc(a, i) 中(假设它们还没有),尽管函数体是空的。

感谢您的任何建议!

4

3 回答 3

3

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

随着实际增加,你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分支中)?

不,对于“复杂”的一些定义。编译器使用启发式方法来确定内联函数是否值得,并以此为基础做出决定,而不是其他任何事情。

我想知道变量 (ai) 在被调用时是否仍然加载到缓存t.inc(a, i)中(假设它们还没有),尽管函数体是空的。

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

于 2018-08-30T10:22:10.983 回答
3

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

你说的对。我已经在编译器资源管理器中修改了您的示例(即删除了使程序集混乱的 cout),以使其更加明显。

编译器优化所有内容和输出

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

只有 42 被引入 eax 并返回。

然而,对于更复杂的情况,需要更多指令来计算返回值。看这里

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;)来调用类中的静态函数。这违背了目的。称它为rahter T::inc(...)than t.inc(...)

于 2018-08-30T10:28:22.973 回答
1

因为inline使用了关键字,您可以放心地假设 1. 使用这些函数不应该对性能产生负面影响。

通过运行您的代码

g++ -c -Os -g

对象转储 -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)

于 2018-08-30T10:19:59.317 回答