【发布时间】: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