performance - 为什么在展开的 ADD 循环内重新初始化寄存器会使其运行速度更快,即使循环内有更多指令?

标签 performance assembly x86 cpu-architecture

我有以下代码:

#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/ 用于说明表、微架构指南以及有关此答案中所有内容的更多详细信息。)

随着 loopmov 在前端(和后端执行单元)中占用插槽,远离 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/

相关文章:

linux - 基于用户输入的汇编调用子程序

mysql - 为什么 LEFT JOIN 和 GROUP BY 会影响性能?

c - gcc 的原子操作和代码生成

c++ - 获取调用者的返回地址

c++ - 代码对齐会显着影响性能

c++ - 实现线程作为库

performance - 我应该创建多个 OpenCL 内核以避免条件语句吗?

java - 迭代大量对象

jquery - 查找嵌套表格单元格内容 - jquery 查询性能

assembly - Intel X86-64 组装教程或书籍