gpt4 book ai didi

c++ - 在 gcc 上使用 addcarry 得到奇怪的优化结果

转载 作者:行者123 更新时间:2023-11-30 05:12:07 24 4
gpt4 key购买 nike

考虑以下对 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) 明显比其他选项快

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

注意:欢迎使用其他编译器/其他架构进行实验(需要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](左侧是“fast "一个,右边那个是“慢”的)。

Valgrin results

最佳答案

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

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

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

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

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

关于c++ - 在 gcc 上使用 addcarry 得到奇怪的优化结果,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44698727/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com