c++ - 使用 goto 可以创建编译器无法在 C++ 中生成的优化吗?

标签 c++

使用 gotos 代替 oops 是否可以产生比使用循环代替编译器生成的指令更有效的一系列跳转指令?

例如:如果我有一个嵌套在 switch 语句中的 while 循环,它会嵌套在另一个循环中,而这个循环又会嵌套在另一个 switch case 中,使用 goto 实际上可以胜过编译器在仅使用循环时生成的跳转指令没有 gotos?

最佳答案

使用 goto 可能会获得一点速度优势。然而,反之亦然。编译器在检测和展开循环或使用 SIMD 指令优化循环方面变得非常擅长。您很可能会杀死编译器的所有这些优化选项,因为它们不是为优化 goto 语句而构建的。 您还可以编写函数来防止 goto。这样您就可以让编译器内联函数并摆脱跳转。

如果您考虑将 goto 用于优化目的,我会说,这是一个非常糟糕的主意。用干净的代码表达你的算法并在以后优化。如果您需要更多性能,请考虑您的数据及其访问权限。这就是您可以获得或降低性能的关键。

既然你想要代码来证明这一点,我构造了下面的例子。我在 Intel i7-3537U 上使用了带有 -O3 的 gcc 6.3.1。如果您尝试重现该示例,您的结果可能因您的编译器或硬件而异。

#include <iostream>
#include <array>
#include "ToolBox/Instrumentation/Profiler.hpp"

constexpr size_t kilo = 1024;
constexpr size_t mega = kilo * 1024;

constexpr size_t size = 512*mega;
using container = std::array<char, size>;
enum class Measurements {
    jump,
    loop
};

// Simple vector addition using for loop
void sum(container& result, const container& data1, const container& data2) {
    profile(Measurements::loop);
    for(unsigned int i = 0; i < size; ++i) {
        result[i] = data1[i] + data2[i];
    }
}

//  Simple vector addition using jumps
void sum_jump(container& result, const container& data1, const container& data2) {
    profile(Measurements::jump);
    unsigned int i = 0;
label:
    result[i] = data1[i] + data2[i];
    i++;
    if(i == size) goto label;
}

int main() {
    // This segment is just for benchmarking purposes
    // Just ignore this
    ToolBox::Instrumentation::Profiler<Measurements, std::chrono::nanoseconds, 2> profiler(
        std::cout,
        {
            {Measurements::jump, "jump"},
            {Measurements::loop, "loop"}
        }
    );


    // allocate memory to execute our sum functions on
    container data1, data2, result;

    // run the benchmark 100 times to account for caching of the data
    for(unsigned i = 0; i < 100; i++) {

    sum_jump(result, data1, data2);
    sum(result, data1, data2);

    }
}

程序的输出如下:

Runtimes for 12Measurements
        jump:       100x            2972 nanoseconds            29 nanoseconds/execution
        loop:       100x            2820 nanoseconds            28 nanoseconds/execution

好的,我们看到运行时没有时间差异,因为我们受限于内存带宽而不是 cpu 指令。但是让我们看看生成的汇编程序指令:

Dump of assembler code for function sum(std::array<char, 536870912ul>&, std::array<char, 536870912ul> const&, std::array<char, 536870912ul> const&):
   0x00000000004025c0 <+0>:     push   %r15
   0x00000000004025c2 <+2>:     push   %r14
   0x00000000004025c4 <+4>:     push   %r12
   0x00000000004025c6 <+6>:     push   %rbx
   0x00000000004025c7 <+7>:     push   %rax
   0x00000000004025c8 <+8>:     mov    %rdx,%r15
   0x00000000004025cb <+11>:    mov    %rsi,%r12
   0x00000000004025ce <+14>:    mov    %rdi,%rbx
   0x00000000004025d1 <+17>:    callq  0x402110 <_ZNSt6chrono3_V212system_clock3nowEv@plt>
   0x00000000004025d6 <+22>:    mov    %rax,%r14
   0x00000000004025d9 <+25>:    lea    0x20000000(%rbx),%rcx
   0x00000000004025e0 <+32>:    lea    0x20000000(%r12),%rax
   0x00000000004025e8 <+40>:    lea    0x20000000(%r15),%rsi
   0x00000000004025ef <+47>:    cmp    %rax,%rbx
   0x00000000004025f2 <+50>:    sbb    %al,%al
   0x00000000004025f4 <+52>:    cmp    %rcx,%r12
   0x00000000004025f7 <+55>:    sbb    %dl,%dl
   0x00000000004025f9 <+57>:    and    %al,%dl
   0x00000000004025fb <+59>:    cmp    %rsi,%rbx
   0x00000000004025fe <+62>:    sbb    %al,%al
   0x0000000000402600 <+64>:    cmp    %rcx,%r15
   0x0000000000402603 <+67>:    sbb    %cl,%cl
   0x0000000000402605 <+69>:    test   $0x1,%dl
   0x0000000000402608 <+72>:    jne    0x40268b <sum(std::array<char, 536870912ul>&, std::array<char, 536870912ul> const&, std::array<char, 536870912ul> const&)+203>
   0x000000000040260e <+78>:    and    %cl,%al
   0x0000000000402610 <+80>:    and    $0x1,%al
   0x0000000000402612 <+82>:    jne    0x40268b <sum(std::array<char, 536870912ul>&, std::array<char, 536870912ul> const&, std::array<char, 536870912ul> const&)+203>
   0x0000000000402614 <+84>:    xor    %eax,%eax
   0x0000000000402616 <+86>:    nopw   %cs:0x0(%rax,%rax,1)
   0x0000000000402620 <+96>:    movdqu (%r12,%rax,1),%xmm0
   0x0000000000402626 <+102>:   movdqu 0x10(%r12,%rax,1),%xmm1
   0x000000000040262d <+109>:   movdqu (%r15,%rax,1),%xmm2
   0x0000000000402633 <+115>:   movdqu 0x10(%r15,%rax,1),%xmm3
   0x000000000040263a <+122>:   paddb  %xmm0,%xmm2
   0x000000000040263e <+126>:   paddb  %xmm1,%xmm3
   0x0000000000402642 <+130>:   movdqu %xmm2,(%rbx,%rax,1)
   0x0000000000402647 <+135>:   movdqu %xmm3,0x10(%rbx,%rax,1)
   0x000000000040264d <+141>:   movdqu 0x20(%r12,%rax,1),%xmm0
   0x0000000000402654 <+148>:   movdqu 0x30(%r12,%rax,1),%xmm1
   0x000000000040265b <+155>:   movdqu 0x20(%r15,%rax,1),%xmm2
   0x0000000000402662 <+162>:   movdqu 0x30(%r15,%rax,1),%xmm3
   0x0000000000402669 <+169>:   paddb  %xmm0,%xmm2
   0x000000000040266d <+173>:   paddb  %xmm1,%xmm3
   0x0000000000402671 <+177>:   movdqu %xmm2,0x20(%rbx,%rax,1)
   0x0000000000402677 <+183>:   movdqu %xmm3,0x30(%rbx,%rax,1)
   0x000000000040267d <+189>:   add    $0x40,%rax
   0x0000000000402681 <+193>:   cmp    $0x20000000,%rax
   0x0000000000402687 <+199>:   jne    0x402620 <sum(std::array<char, 536870912ul>&, std::array<char, 536870912ul> const&, std::array<char, 536870912ul> const&)+96>
   0x0000000000402689 <+201>:   jmp    0x4026d5 <sum(std::array<char, 536870912ul>&, std::array<char, 536870912ul> const&, std::array<char, 536870912ul> const&)+277>
   0x000000000040268b <+203>:   xor    %eax,%eax
   0x000000000040268d <+205>:   nopl   (%rax)
   0x0000000000402690 <+208>:   movzbl (%r15,%rax,1),%ecx
   0x0000000000402695 <+213>:   add    (%r12,%rax,1),%cl
   0x0000000000402699 <+217>:   mov    %cl,(%rbx,%rax,1)
   0x000000000040269c <+220>:   movzbl 0x1(%r15,%rax,1),%ecx
   0x00000000004026a2 <+226>:   add    0x1(%r12,%rax,1),%cl
   0x00000000004026a7 <+231>:   mov    %cl,0x1(%rbx,%rax,1)
   0x00000000004026ab <+235>:   movzbl 0x2(%r15,%rax,1),%ecx
   0x00000000004026b1 <+241>:   add    0x2(%r12,%rax,1),%cl
   0x00000000004026b6 <+246>:   mov    %cl,0x2(%rbx,%rax,1)
   0x00000000004026ba <+250>:   movzbl 0x3(%r15,%rax,1),%ecx
   0x00000000004026c0 <+256>:   add    0x3(%r12,%rax,1),%cl
   0x00000000004026c5 <+261>:   mov    %cl,0x3(%rbx,%rax,1)
   0x00000000004026c9 <+265>:   add    $0x4,%rax
   0x00000000004026cd <+269>:   cmp    $0x20000000,%rax
   0x00000000004026d3 <+275>:   jne    0x402690 <sum(std::array<char, 536870912ul>&, std::array<char, 536870912ul> const&, std::array<char, 536870912ul> const&)+208>
   0x00000000004026d5 <+277>:   callq  0x402110 <_ZNSt6chrono3_V212system_clock3nowEv@plt>
   0x00000000004026da <+282>:   sub    %r14,%rax
   0x00000000004026dd <+285>:   add    %rax,0x202b74(%rip)        # 0x605258 <_ZN7ToolBox15Instrumentation6detail19ProfilerMeasurementI12MeasurementsLS3_1EE14totalTimeSpentE>
   0x00000000004026e4 <+292>:   incl   0x202b76(%rip)        # 0x605260 <_ZN7ToolBox15Instrumentation6detail19ProfilerMeasurementI12MeasurementsLS3_1EE10executionsE>
   0x00000000004026ea <+298>:   add    $0x8,%rsp
   0x00000000004026ee <+302>:   pop    %rbx
   0x00000000004026ef <+303>:   pop    %r12
   0x00000000004026f1 <+305>:   pop    %r14
   0x00000000004026f3 <+307>:   pop    %r15
   0x00000000004026f5 <+309>:   retq   
End of assembler dump.

正如我们所见,编译器已将循环矢量化并使用 simd 指令(例如 paddb)。

现在是带有跳跃的版本:

Dump of assembler code for function sum_jump(std::array<char, 536870912ul>&, std::array<char, 536870912ul> const&, std::array<char, 536870912ul> const&):
   0x0000000000402700 <+0>:     push   %r15
   0x0000000000402702 <+2>:     push   %r14
   0x0000000000402704 <+4>:     push   %r12
   0x0000000000402706 <+6>:     push   %rbx
   0x0000000000402707 <+7>:     push   %rax
   0x0000000000402708 <+8>:     mov    %rdx,%rbx
   0x000000000040270b <+11>:    mov    %rsi,%r14
   0x000000000040270e <+14>:    mov    %rdi,%r15
   0x0000000000402711 <+17>:    callq  0x402110 <_ZNSt6chrono3_V212system_clock3nowEv@plt>
   0x0000000000402716 <+22>:    mov    %rax,%r12
   0x0000000000402719 <+25>:    mov    (%rbx),%al
   0x000000000040271b <+27>:    add    (%r14),%al
   0x000000000040271e <+30>:    mov    %al,(%r15)
   0x0000000000402721 <+33>:    callq  0x402110 <_ZNSt6chrono3_V212system_clock3nowEv@plt>
   0x0000000000402726 <+38>:    sub    %r12,%rax
   0x0000000000402729 <+41>:    add    %rax,0x202b38(%rip)        # 0x605268 <_ZN7ToolBox15Instrumentation6detail19ProfilerMeasurementI12MeasurementsLS3_0EE14totalTimeSpentE>
   0x0000000000402730 <+48>:    incl   0x202b3a(%rip)        # 0x605270 <_ZN7ToolBox15Instrumentation6detail19ProfilerMeasurementI12MeasurementsLS3_0EE10executionsE>
   0x0000000000402736 <+54>:    add    $0x8,%rsp
   0x000000000040273a <+58>:    pop    %rbx
   0x000000000040273b <+59>:    pop    %r12
   0x000000000040273d <+61>:    pop    %r14
   0x000000000040273f <+63>:    pop    %r15
   0x0000000000402741 <+65>:    retq   
End of assembler dump.

这里我们没有触发优化。

您可以自己编译程序并检查 asm 代码:​​

gdb -batch -ex 'file a.out' -ex 'disassemble sum'

我也尝试过使用这种方法进行矩阵乘法,但 gcc 足够聪明,可以使用 goto/label 语法检测矩阵乘法。

结论:

即使我们没有看到速度损失,我们也看到 gcc 无法使用可以加快计算速度的优化。在更具 CPU 挑战性的任务中,这可能会导致性能下降。

关于c++ - 使用 goto 可以创建编译器无法在 C++ 中生成的优化吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43880787/

相关文章:

c++ - 将字符串转换为 const char* 问题

c++ - OpenCV 2.4 CascadeClassified detectMultiScale 参数

c++ - 必须使用指向抽象类的指针会造成困难

c++ - 继承 : Access both private and public parts of another class

c++ - 不使用模板重载乘法运算符

C++ [Windows] 可执行文件所在文件夹的路径

c# - 如何将现有内存缓冲区包装为 GDI 的 DC

c++ - 找不到 boost::async 到底做了什么

c++ - 使用在该结构中包含结构的 using-definition

c++ - 模板的实例化