【问题标题】:Strange optimization result with addcarry on gcc奇怪的优化结果与 gcc 上的 addcarry
【发布时间】:2017-06-22 11:55:06
【问题描述】:

考虑以下对addcarry 内在函数进行基准测试的代码:

// Preamble
#include <iostream>

// Addcarry wrapper
template <class C, class T>
C addcarry(C carry, T src0, T src1, T* dst)
{
    unsigned long long int d = 0;
    carry = __builtin_ia32_addcarryx_u64(carry, src0, src1, &d);
    *dst = d;
    return carry;
}

// Main function
int main(int argc, char* argv[])
{
    // Initialization
    unsigned long long int n = argc > 1 ? std::stoull(argv[1]) : 1ULL << 31;
    unsigned char carry = 0;
    unsigned long long int src1 = 0;
#ifdef NOSTOULL
    unsigned long long int dst = 0;
#else
    unsigned long long int dst = std::stoull("0");
#endif

    // Computation
    for (unsigned long long int src0 = 0; src0 < n; ++src0) {
        src1 = dst;
#ifdef NOWRAPPER
        carry = __builtin_ia32_addcarryx_u64(carry, src0, src1, &dst);
#else
        carry = addcarry(carry, src0, src1, &dst);
#endif
    }

    // Finalization
    return dst + carry;
}

我使用以下命令进行编译(其中[] 表示选项):

[g++6.3.0/g++7.1.0] -Wall -Wextra -pedantic -O3 -g [-DNOSTOULL]
[-DNOWRAPPER] addcarry_loop.cpp -o addcarry_loop -madx

然后我执行:

time ./addcarry_loop

并且我获得了以下真实/用户时间(经过多次运行):

(1) [-DNOSTOULL] [-DNOWRAPPER] => ~0m2.64s
(2) [          ] [-DNOWRAPPER] => ~0m2.61s
(3) [-DNOSTOULL] [           ] => ~0m2.48s
(4) [          ] [           ] => ~0m1.86s

我们可以注意到:

  • 选项 (1) 和 (2) 在统计上相似
  • 选项 (3)(带包装器功能)比选项 (1) 和 (2)(不带包装器功能)略快
  • 选项 (4) 明显快于其他选项

如何解释这些结果,尤其是最后一个(这对我来说毫无意义)。使用stoull 初始化变量并包装函数如何使代码更快?

注意:欢迎使用其他编译器/其他架构进行实验(需要adx 指令集)。

编辑:鉴于注释,我更新了代码,从主函数中提取循环:

// Preamble
#include <iostream>

// Addcarry wrapper
template <class C, class T>
C addcarry(C carry, T src0, T src1, T* dst)
{
    unsigned long long int d = 0;
    carry = __builtin_ia32_addcarryx_u64(carry, src0, src1, &d);
    *dst = d;
    return carry;
}

// Compute
unsigned long long int compute(unsigned long long int n)
{
    // Initialization
    unsigned char carry = 0;
    unsigned long long int src1 = 0;
#ifdef NOSTOULL
    unsigned long long int dst = 0;
#else
    unsigned long long int dst = std::stoull("0");
#endif

    // Computation
    for (unsigned long long int src0 = 0; src0 < n; ++src0) {
        src1 = dst;
#ifdef NOWRAPPER
        carry = __builtin_ia32_addcarryx_u64(carry, src0, src1, &dst);
#else
        carry = addcarry(carry, src0, src1, &dst);
#endif
    }

    // Finalization
    return dst + carry;
}

// Main function
int main(int argc, char* argv[])
{
    return compute(argc > 1 ? std::stoull(argv[1]) : 1ULL << 31);
}

循环对应的程序集似乎有点不同。在下图中,左侧对应空选项[] [],右侧对应[] [-DNOWRAPPER](左侧为“快”选项,右侧为“慢”选项)。

【问题讨论】:

  • 当你说这些东西在性能方面更快或相似时,这是多少次运行?这些运行的时间平均值和方差是多少,您使用什么样的统计检验(例如 t 检验)来显示它们不同(结果 p 值是多少)?我相信您存在性能差异,但您首先需要证明这一点。
  • 甚至比 AndyG 更重要的是,我对基准测试方法持怀疑态度。在 Godbolt 的 Compiler Explorer 上抛出这个并比较反汇编,很明显-DNOSTOULL 应该会产生明显的改进。它肯定会对生成的程序集产生重大影响。因此,无论有无,它的运行时间几乎相同似乎不太可能。 (不过,我可以确认,生成的代码使用包装变得更加优化,这确实很奇怪。还不知道为什么。我现在没有时间调查,尽管这已经激怒了我的好奇心。)
  • 我无法使用 g++ 6.3.0 重现您的结果。在这里,无论有没有-madx,我都得到了相同的构建,我的结果比你的慢:~3.5s。也许如果你看到生成的 asm 指令,你可以找到答案(你可以在这里发布 asm 吗?)。
  • @Cody:为什么?我用编译器资源管理器检查了编译后的代码,主循环编译相同,不管-DNOSTOULL是否存在(应该是,编译器不够聪明,无法优化整个循环)。跨度>
  • 您需要注意的一件事是,GCC 倾向于禁用 main 函数的许多优化,假设这永远不会成为实际代码中的瓶颈,因为它只是执行一次。因此,查看您在这里所拥有的确切内容的反汇编非常嘈杂,因此我稍微修改了代码以专注于重要位。我重命名了main,并将n 作为参数,所以我不需要初始化它,但编译器仍然无法围绕它进行优化。如您所见here-DNOSTOULL大大改进了生成的代码

标签: c++ gcc assembly benchmarking compiler-optimization


【解决方案1】:

我无法重现您的结果,我尝试过 gcc 6.3.0 和 gcc 7.1.0。在这里,您的所有变体都以相同的速度运行。但是,由于某种原因,您的拆卸与我的不同。看着你的反汇编,它有一些奇怪的东西。例如,在左侧,在 0x400d7d,有一个不必要的内存移动:它可以在循环之后被移出。当然,一个好的程序员可以在这里写出更好的代码(这种情况下最好的代码是完全去除循环,并为其应用数学公式)。

在我看来,编译器仍然不够好。他们变得越来越好(编译器开发人员干得好!),但有时他们生成的代码远非最佳。

这是我最后的经历:我写了一个霍夫曼解码器。 Clang 生成的代码运行速度几乎是 GCC 的一半。这是一个很大的区别。我检查了反汇编,clang 试图将两个 32 位变量“合并”到 64 位寄存器中(也许它非常努力地避免使用堆栈?)。我稍微修改了一下代码,然后clang代码突然变得比GCC的快了一点。

在创建像您这样的小循环时,每个小细节都很重要。稍微修改代码可能会导致巨大的速度差异。并且可能相同的编译代码在不同的处理器(Intel/AMD)上表现不同。

我的建议是这样的:在编写性能敏感循环时,尽量把循环放到一个单独的函数中。此函数不应包含任何序言或结语代码,仅包含循环。通过这种方式,您可以帮助编译器更好地优化(尽可能有效地使用寄存器)。使用这种技术,我的霍夫曼解码器速度提高了 20%。我并不是说你应该 100% 遵守这条规则,但它通常对我有用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-02
    • 2018-08-21
    • 2020-12-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多