C++ 为什么for循环体中的一个基本算术运算比两个算术运算执行得慢?
当我尝试测量算术运算的执行时间时,我遇到了非常奇怪的行为。包含C++ 为什么for循环体中的一个基本算术运算比两个算术运算执行得慢?,c++,performance,assembly,cpu-architecture,google-benchmark,C++,Performance,Assembly,Cpu Architecture,Google Benchmark,当我尝试测量算术运算的执行时间时,我遇到了非常奇怪的行为。包含for循环且循环体中有一个算术运算的代码块执行速度始终低于相同的代码块,但for循环体中有两个算术运算。以下是我最后测试的代码: #include <iostream> #include <chrono> #define NUM_ITERATIONS 100000000 int main() { // Block 1: one operation in loop body {
for
循环且循环体中有一个算术运算的代码块执行速度始终低于相同的代码块,但for
循环体中有两个算术运算。以下是我最后测试的代码:
#include <iostream>
#include <chrono>
#define NUM_ITERATIONS 100000000
int main()
{
// Block 1: one operation in loop body
{
int64_t x = 0, y = 0;
auto start = std::chrono::high_resolution_clock::now();
for (long i = 0; i < NUM_ITERATIONS; i++) {x+=31;}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end-start;
std::cout << diff.count() << " seconds. x,y = " << x << "," << y << std::endl;
}
// Block 2: two operations in loop body
{
int64_t x = 0, y = 0;
auto start = std::chrono::high_resolution_clock::now();
for (long i = 0; i < NUM_ITERATIONS; i++) {x+=17; y-=37;}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end-start;
std::cout << diff.count() << " seconds. x,y = " << x << "," << y << std::endl;
}
return 0;
}
#包括
#包括
#定义NUM_迭代100000000
int main()
{
//块1:循环体中的一个操作
{
int64_t x=0,y=0;
自动启动=标准::时钟::高分辨率时钟::现在();
对于(长i=0;i std::coutETA:这是一个猜测,彼得·科尔德斯(Peter Cordes)就其不正确的原因进行了很好的论证。请投票表决彼得的答案
我把答案留在这里是因为有些人发现这些信息很有用。虽然这不能正确解释OP中的行为,但它突出了一些问题,这些问题使得在现代处理器上测量特定指令的速度变得不可行(而且毫无意义)
有根据的猜测:
这是流水线、关闭内核部分电源和
现代处理器采用流水线方式,使多条指令可以同时执行。这是可能的,因为处理器实际上是在微操作上工作的,而不是我们通常认为是机器语言的汇编级指令。处理器“调度”通过将微操作分派到芯片的不同部分,同时跟踪指令之间的依赖关系
假设运行代码的核心有两个算术/逻辑单元(ALU)。一条反复重复的算术指令只需要一个ALU。使用两个ALU没有帮助,因为下一个操作取决于当前ALU的完成情况,所以第二个ALU只能等待
但是在您的两个表达式测试中,表达式是独立的。要计算y
的下一个值,您不必等待x
上的当前操作完成。现在,由于省电功能,第二个ALU可能会首先断电。内核可能会运行几次迭代,然后才意识到它可以使用第二个ALU。此时,它可以为第二个ALU通电,两个表达式循环的大部分运行速度将与一个表达式循环的运行速度一样快。因此,您可能希望两个示例所用的时间大致相同
最后,许多现代处理器使用动态频率缩放。当处理器检测到它运行不努力时,它实际上会稍微降低时钟以节省电源。但是当它被大量使用时(并且芯片的当前温度允许),它可能会将实际时钟速度提高到其额定速度
我假设这是通过启发式实现的。在第二个ALU断电的情况下,启发式可能会决定不值得增加时钟。在两个ALU通电并以最高速度运行的情况下,它可能会决定增加时钟。因此,两个表达式的情况下,应该已经与一个表达式的速度一样快在这种情况下,它实际上以更高的平均时钟频率运行,使它能够在略短的时间内完成两倍的工作
根据您的数字,差异约为14%。我的Windows机器在3.75 GHz左右空闲,如果我在Visual Studio中构建解决方案来稍微推动一下,时钟将上升到4.25 GHz左右(查看任务管理器中的“性能”选项卡).这是13%的时钟速度差异,因此我们的估计是正确的。@PeterCordes在许多假设中证明了这个答案是错误的,但作为对这个问题的一些盲目研究尝试,它仍然是有用的
我设置了一些快速基准测试,认为它可能与代码内存对齐有关,这真是一个疯狂的想法
但似乎@Adrian McCarthy在动态频率缩放方面做得对
无论如何,基准测试告诉我们,插入一些NOP可能会有助于解决这个问题,在块1中x+=31之后插入15个NOP将导致与块2几乎相同的性能。单指令循环体中插入15个NOP如何提高性能,真是令人惊讶
我也尝试过,最聪明的编译器可能会扔掉一些插入这样NOP的代码内存,但事实似乎并非如此。
编辑:多亏了@PeterCordes,我们清楚地看到优化从来没有像上面基准测试中预期的那样有效(因为全局变量需要添加访问内存的指令),新的基准测试清楚地表明,对于堆栈变量,块1和块2的性能是相同的。但是NOPs仍然可以帮助单线程应用程序使用循环访问全局变量,在这种情况下,您可能不应该使用全局变量,而只需在循环后将全局变量分配给局部变量
编辑2:由于快速基准宏使变量访问不稳定,从而阻止了重要的优化,因此实际上优化从未起作用。加载变量一次是合乎逻辑的,因为我们只是在循环中修改它,因此它是不稳定的或禁用的优化。因此,这个答案基本上是错误的,但至少它显示了NOP可以加速未优化的代码执行,如果它在现实世界中有任何意义(有更好的方法比如Buffter计数器)。< /P> < P>我将代码分解成C++和汇编。我只想测试循环,所以我没有返回总和。。我在Windows上运行,调用约定是rcx,rdx,r8,r9,
循环计数在rcx
中。代码正在添加imm
.code
public test1
align 16
test1 proc
sub rsp,16
mov qword ptr[rsp+0],0
mov qword ptr[rsp+8],0
tst10: add qword ptr[rsp+8],17
dec rcx
jnz tst10
add rsp,16
ret
test1 endp
public test2
align 16
test2 proc
sub rsp,16
mov qword ptr[rsp+0],0
mov qword ptr[rsp+8],0
tst20: add qword ptr[rsp+0],17
add qword ptr[rsp+8],-37
dec rcx
jnz tst20
add rsp,16
ret
test2 endp
end
.code
public test1
align 16
test1 proc
xor rdx,rdx
xor r8,r8
xor r9,r9
xor r10,r10
xor r11,r11
tst10: add rdx,17
dec rcx
jnz tst10
ret
test1 endp
public test2
align 16
test2 proc
xor rdx,rdx
xor r8,r8
xor r9,r9
xor r10,r10
xor r11,r11
tst20: add rdx,17
add r8,-37
dec rcx
jnz tst20
ret
test2 endp
public test3
align 16
test3 proc
xor rdx,rdx
xor r8,r8
xor r9,r9
xor r10,r10
xor r11,r11
tst30: add rdx,17
add r8,-37
add r9,47
dec rcx
jnz tst30
ret
test3 endp
public test4
align 16
test4 proc
xor rdx,rdx
xor r8,r8
xor r9,r9
xor r10,r10
xor r11,r11
tst40: add rdx,17
add r8,-37
add r9,47
add r10,-17
dec rcx
jnz tst40
ret
test4 endp
end
#define NUM_ITERATIONS 1000000000ll
#define X_INC 17
#define Y_INC -31
for (long i = 0; i < NUM_ITERATIONS; i++) { x+=X_INC; }
mov QWORD PTR [rbp-32], 0
.L13:
cmp QWORD PTR [rbp-32], 999999999
jg .L12
add QWORD PTR [rbp-24], 17
add QWORD PTR [rbp-32], 1
jmp .L13
.L12:
for (long i = 0; i < NUM_ITERATIONS; i++) {x+=X_INC; y+=Y_INC;}
mov QWORD PTR [rbp-80], 0
.L21:
cmp QWORD PTR [rbp-80], 999999999
jg .L20
add QWORD PTR [rbp-64], 17
sub QWORD PTR [rbp-72], 31
add QWORD PTR [rbp-80], 1
jmp .L21
.L20:
while (x < (X_INC * NUM_ITERATIONS)) { x+=X_INC; }
.L15:
movabs rax, 16999999999
cmp QWORD PTR [rbp-40], rax
jg .L14
add QWORD PTR [rbp-40], 17
jmp .L15
.L14:
register long i;
for (i = 0; i < NUM_ITERATIONS; i++) { x+=X_INC; }
mov ebx, 0
.L17:
cmp rbx, 999999999
jg .L16
add QWORD PTR [rbp-48], 17
add rbx, 1
jmp .L17
.L16:
orig_main:
...
call std::chrono::_V2::system_clock::now() # demangled C++ symbol name
mov rbp, rax # save the return value = start
call std::chrono::_V2::system_clock::now()
# end in RAX
#include <benchmark/benchmark.h>
static void TargetFunc(benchmark::State& state) {
uint64_t x2 = 0, y2 = 0;
// Code inside this loop is measured repeatedly
for (auto _ : state) {
benchmark::DoNotOptimize(x2 += 31);
benchmark::DoNotOptimize(y2 += 31);
}
}
// Register the function as a benchmark
BENCHMARK(TargetFunc);
# just the main loop, from gcc10.1 -O3
.L7: # do{
add rax, 31 # x2 += 31
add rdx, 31 # y2 += 31
sub rbx, 1
jne .L7 # }while(--count != 0)
static void TargetFunc(benchmark::State& state) {
uint64_t x2 = 0, y2 = 0;
// Code inside this loop is measured repeatedly
for (auto _ : state) {
x2 += 16;
y2 += 17;
asm volatile("" : "+r"(x2), "+r"(y2));
}
}