使用 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/