Dav*_*Far 5 c c++ performance assembly llvm-mca
我使用llvm-mca来计算一堆代码的总周期,认为它们会预测它的运行时间.但是,动态测量运行时几乎没有相关性.那么:为什么由llvm-mca计算的总周期不能准确预测运行时?我可以用llvm-mca以更好的方式预测运行时吗?
细节:
我想知道,对于不同类型的下面的代码的运行时间begin(和end)迭代器,对startValue正在0.0或0ULL:
std::accumulate(begin, end, starValue)
Run Code Online (Sandbox Code Playgroud)
为了预测运行时,我使用Compiler Explorer(https://godbolt.org/z/5HDzSF)及其LLVM机器码分析器(llvm-mca)插件,因为llvm-mca是"一个使用可用信息的性能分析工具"在LLVM(例如调度模型)中静态测量性能".我使用了以下代码:
using vec_t = std::vector<double>;
vec_t generateRandomVector(vec_t::size_type size)
{
std::random_device rnd_device;
std::mt19937 mersenne_engine {rnd_device()};
std::uniform_real_distribution dist{0.0,1.1};
auto gen = [&dist, &mersenne_engine](){
return dist(mersenne_engine);
};
vec_t result(size);
std::generate(result.begin(), result.end(), gen);
return result;
}
double start()
{
vec_t vec = generateRandomVector(30000000);
vec_t::iterator vectorBegin = vec.begin();
vec_t::iterator vectorEnd = vec.end();
__asm volatile("# LLVM-MCA-BEGIN stopwatchedAccumulate");
double result = std::accumulate(vectorBegin, vectorEnd, 0.0);
__asm volatile("# LLVM-MCA-END");
return result;
}
Run Code Online (Sandbox Code Playgroud)
但是,我看到llvm-mca计算机的总周期与运行相应的std :: accumulate的挂钟时间之间没有相关性.例如,在上面的代码中,Total Cycles是2806,运行时间是14ms.当我切换到startValue时0ULL,Total Cycles是2357,但运行时间是117ms.
TL:DR:LLVM-MCA 分析了这些注释之间的整个代码块,就好像它是循环的主体一样,并向您展示了所有这些指令的 100 次迭代的循环计数。
但是除了实际的(微小的)循环外,大多数指令都是循环设置和循环后的 SIMD 水平总和,实际上只运行一次。(这就是为什么周期数是数千,而不是 400 = 100 倍vaddpdSkylake 上0.0带有double累加器的版本的 4 周期延迟。)
如果您取消选中 Godbolt 编译器资源管理器上的“//”框,或修改 asm 语句以添加 nop like "nop # LLVM-MCA-END",您将能够在 asm 窗口中找到这些行并查看 LLVM-MCA 正在查看的内容“环形”。
LLVM MCA 模拟指定的汇编指令序列,并计算在指定的目标架构上执行每次迭代估计需要的周期数。LLVM MCA 进行了许多简化,例如(在我的脑海中):(1)它假设所有条件分支都通过,(2)它假设所有内存访问都是写回内存类型,并且全部命中L1 缓存,(3) 它假设前端工作在最佳状态,以及 (4)call指令没有跟随到被调用的过程中,它们只是失败。还有其他假设我现在想不起来了。
本质上,LLVM MCA(如 Intel IACA)仅对后端计算绑定的简单循环准确。在 IACA 中,虽然支持大多数指令,但有一些指令没有详细建模。例如,假设预取指令仅消耗微体系结构资源,但基本为零延迟,并且对存储器层次结构的状态没有影响。然而,在我看来,MCA 完全忽略了这些指令。无论如何,这与您的问题并不是特别相关。
现在回到你的代码。在您提供的编译器资源管理器链接中,您没有将任何选项传递给 LLVM MCA。因此默认目标架构生效,即工具运行的任何架构。这恰好是SKX。您提到的循环总数是针对 SKX 的,但不清楚您是否在 SKX 上运行代码。您应该使用该-mcpu选项来指定架构。这与-march您传递给 gcc 的无关。另请注意,将核心周期与毫秒进行比较是没有意义的。您可以使用该RDTSC指令以核心周期来衡量执行时间。
请注意编译器如何内联对std::accumulate. 显然,这段代码从405行开始,最后一条指令std::accumulate在444行,一共38条指令。LLVM MCA 估计与实际性能不匹配的原因现在已经很清楚了。该工具假设所有这些指令都在循环中执行大量迭代。显然,情况并非如此。从 420-424 只有一个循环:
.L75:
vaddpd ymm0, ymm0, YMMWORD PTR [rax]
add rax, 32
cmp rax, rcx
jne .L75
Run Code Online (Sandbox Code Playgroud)
只有此代码应该是 MCA 的输入。在源代码层面,真的没有办法告诉 MCA 只分析这段代码。您必须手动内联std::accumulate并将LLVM-MCA-BEGIN和LLVM-MCA-END标记放置在其中的某个位置。
当传递0ULL而不是0.0to 时std::accumulate,LLVM MCA 的输入将从汇编指令 402 开始并在 441 结束。请注意,MCA 不支持的任何指令(例如vcvtsi2sdq)将完全从分析中省略。实际上处于循环中的代码部分是:
.L78:
vxorpd xmm0, xmm0, xmm0
vcvtsi2sdq xmm0, xmm0, rax
test rax, rax
jns .L75
mov rcx, rax
and eax, 1
vxorpd xmm0, xmm0, xmm0
shr rcx
or rcx, rax
vcvtsi2sdq xmm0, xmm0, rcx
vaddsd xmm0, xmm0, xmm0
.L75:
vaddsd xmm0, xmm0, QWORD PTR [rdx]
vcomisd xmm0, xmm1
vcvttsd2si rax, xmm0
jb .L77
vsubsd xmm0, xmm0, xmm1
vcvttsd2si rax, xmm0
xor rax, rdi
.L77:
add rdx, 8
cmp rsi, rdx
jne .L78
Run Code Online (Sandbox Code Playgroud)
请注意,jns目标地址在块中某处的代码中有一个条件跳转,。MCA 只会假设跳跃会失败。如果在实际运行代码时情况并非如此,MCA 将不必要地增加 7 条指令的开销。还有另一个跳跃,jb但我认为这个跳跃对于大向量并不重要,并且大部分时间都会失败。最后一个跳转,jne,也是最后一条指令,所以 MCA 会假设下一条指令又是最上面的一条。对于足够多的迭代,这个假设是完全正确的。
总的来说,很明显第一个代码比第二个代码小得多,所以它可能要快得多。您的测量确实证实了这一点。您也不需要使用微架构分析工具来了解原因。第二个代码只是做了更多的计算。因此,您可以很快得出结论,0.0就所有架构的性能和代码大小而言,传递都更好。
| 归档时间: |
|
| 查看次数: |
215 次 |
| 最近记录: |