我有以下代码:
#include <iostream>
#include <chrono>
#define ITERATIONS "10000"
int main()
{
/*
======================================
The first case: the MOV is outside the loop.
======================================
*/
auto t1 = std::chrono::high_resolution_clock::now();
asm("mov $100, %eax\n"
"mov $200, %ebx\n"
"mov $" ITERATIONS ", %ecx\n"
"lp_test_time1:\n"
" add %eax, %ebx\n" // 1
" add %eax, %ebx\n" // 2
" add %eax, %ebx\n" // 3
" add %eax, %ebx\n" // 4
" add %eax, %ebx\n" // 5
"loop lp_test_time1\n");
auto t2 = std::chrono::high_resolution_clock::now();
auto time = std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
std::cout << time;
/*
======================================
The second case: the MOV is inside the loop (faster).
======================================
*/
t1 = std::chrono::high_resolution_clock::now();
asm("mov $100, %eax\n"
"mov $" ITERATIONS ", %ecx\n"
"lp_test_time2:\n"
" mov $200, %ebx\n"
" add %eax, %ebx\n" // 1
" add %eax, %ebx\n" // 2
" add %eax, %ebx\n" // 3
" add %eax, %ebx\n" // 4
" add %eax, %ebx\n" // 5
"loop lp_test_time2\n");
t2 = std::chrono::high_resolution_clock::now();
time = std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
std::cout << '\n' << time << '\n';
}
第一种情况
我用它编译了
gcc version 9.2.0 (GCC)
Target: x86_64-pc-linux-gnu
gcc -Wall -Wextra -pedantic -O0 -o proc proc.cpp
它的输出是
14474
5837
我也用 Clang 编译了它,结果相同。
那么,为什么第二种情况更快(几乎加速了 3 倍)?它实际上与一些微架构细节有关吗?如果重要的话,我有一个 AMD 的 CPU:“AMD A9-9410 RADEON R5,5 个计算核心 2C+3G”。
最佳答案
循环内的
mov $200, %ebx
通过 ebx
打破循环携带的依赖链,允许乱序执行与链重叠5 在多个迭代中添加
指令。
如果没有它,add
指令链将在 add
(1 个周期)关键路径的延迟上成为循环的瓶颈,而不是吞吐量(4 个周期)挖掘机,改进自
压路机上 2 个循环)。您的CPU是Excavator core .
自 Bulldozer 以来,AMD 拥有高效的loop
指令(仅 1 uop),这与 Intel CPU 不同,在 Intel CPU 中,loop
会在每 7 个周期 1 次迭代时成为任一循环的瓶颈。 ( https://agner.org/optimize/ 用于说明表、微架构指南以及有关此答案中所有内容的更多详细信息。)
随着 loop
和 mov
在前端(和后端执行单元)中占用插槽,远离 add
,而是 3x 4 倍加速看起来是正确的。
参见this answer了解 CPU 如何查找和利用指令级并行性 (ILP) 的介绍。
参见Understanding the impact of lfence on a loop with two long dependency chains, for increasing lengths有关重叠独立 dep 链的一些深入细节。
<小时/>顺便说一句,10k 次迭代并不多。在那段时间里,您的 CPU 甚至可能不会从空闲速度提升。或者可能会在第二个循环的大部分时间里跳到最大速度,但在第一个循环中不会跳到最大速度。所以要小心这样的微基准。
此外,您的内联汇编是不安全的,因为您忘记在 EAX、EBX 和 ECX 上声明 clobbers。你在不告诉编译器的情况下就踏入了它的寄存器。通常,您应该始终在启用优化的情况下进行编译,但如果这样做,您的代码可能会中断。
关于performance - 为什么在展开的 ADD 循环内重新初始化寄存器会使其运行速度更快,即使循环内有更多指令?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58884297/