【问题标题】:(How) can I predict the runtime of a code snippet using LLVM Machine Code Analyzer?(如何)我可以使用 LLVM 机器代码分析器预测代码片段的运行时间吗?
【发布时间】:2019-06-04 01:34:26
【问题描述】:

我使用 llvm-mca 来计算一段代码的总周期,认为它们会预测其运行时间。然而,动态测量运行时间几乎没有相关性。那么:为什么 llvm-mca 计算的总周期不能准确预测运行时间?我可以使用 llvm-mca 以更好的方式预测运行时间吗?


详情:

我想知道以下代码对于不同类型的begin(和end)迭代器的运行时间,因为startValue0.00ULL

std::accumulate(begin, end, starValue)

为了预测运行时,我使用了 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;
}

但是,我发现 llvm-mca 的计算机总周期与运行相应 std::accumulate 的挂钟时间之间没有相关性。例如,在上面的代码中,Total Cycles 是 2806,运行时间是 14ms。当我切换到 startValue 0ULL 时,Total Cycles 是 2357,但运行时间是 117ms。

【问题讨论】:

  • 总 CPU 周期已经停止,直接对应于 1990 年代左右的 CPU 时间。现在有更多的事情会影响实际运行一段代码的时间,例如缓存、分支预测、外部影响等。这样的静态分析不仅需要代码,还需要代码将要运行的确切 CPU 模型,甚至有可能准确。
  • 好吧,好吧,在这种情况下,它可能与编译器必须进行大量转换的事实有关;似乎ull 版本实际上是在那里挑选的。 Those two calls to double conversion seem particularly bad,但这只是一个猜测。你用什么编译器构建这个来进行基准测试?
  • 如果您查看 godbolt.org/z/tTCyRW 的 llvm-mca 输出的底部,您会看到 error: invalid instruction mnemonic 'cvtsi2sdq'call instructions are not correctly modeled. Assume a latency of 100cy。也许这会导致错误的测量结果。但是,在我的原始示例中,测量的程序集中没有调用指令。我会将这个问题转发给 LLVM-MCA 的某个人...
  • @BartekBanachewicz:连续数组(如std::vector 或数组)对于数据布局来说是最好的。根据循环,对齐 SIMD 的数据会有所帮助。至于现代 x86 上循环的static analysis of performance 的资源,stackoverflow.com/tags/x86/info 有很多好东西的链接,尤其是 Agner Fog 的微架构指南 (agner.org/optimize),它确实详细介绍了英特尔和 AMD 的管道/OoO exec CPU 足够详细,可以准确预测很多情况下的性能。
  • 如果 CPU 以恒定频率运行,则总周期和时间呈线性相关。对于超标量/乱序流水线 CPU,其他事情,例如总(动态)指令是非常可变的,尤其是当缓存未命中可能会使流水线停滞数百个周期时。动态 CPU 频率(用于在空闲/低工作负载下省电)在 2000 年代成为了一件事情,而不是早在 90 年代。如果您的测试没有“预热”CPU 以使其首先达到最大涡轮增压,那么微基准测试可能会出现问题,但这不是问题所在。 (LLVM-MCA 正在分析循环外的代码。)

标签: c++ c performance assembly llvm-mca


【解决方案1】:

TL:DR:LLVM-MCA 分析了这些 cmets 之间的整个代码块,就好像它是一个循环的 body,并向您展示了所有 100 次迭代的循环计数这些说明。

除了实际的(微小的)循环之外,大多数指令都是循环设置和循环后的 SIMD 水平和,实际上只运行一次。 (这就是为什么循环计数是数千,而不是 400 = 100 倍 Skylake 上的 vaddpd 的 4 周期延迟,对于带有 double 累加器的 0.0 版本。)

如果您取消选中 Godbolt 编译器资源管理器上的“//”框,或修改 asm 语句以添加类似 "nop # LLVM-MCA-END" 的 nop,您将能够在 asm 窗口中找到这些行并查看 LLVM- MCA 将其视为“循环”。


LLVM MCA 模拟指定的汇编指令序列,并计算在指定目标架构上执行每次迭代估计需要的周期数。 LLVM MCA 进行了许多简化,例如(在我的脑海中):(1)它假设所有条件分支都通过,(2)它假设所有内存访问都是写回内存类型并且全部命中L1 缓存,(3) 它假设前端工作最佳,(4) call 指令没有被遵循到被调用的过程中,它们只是失败了。还有其他一些假设,我现在想不起来了。

基本上,LLVM MCA(如英特尔 IACA)仅适用于后端计算绑定的简单循环。在 IACA 中,虽然支持大多数指令,但有一些指令没有详细建模。例如,假设预取指令仅消耗微架构资源,但延迟基本为零,并且对存储器层次结构的状态没有影响。然而,在我看来,MCA 完全忽略了这些指示。无论如何,这与您的问题并不特别相关。

现在回到您的代码。在您提供的 Compiler Explorer 链接中,您没有将任何选项传递给 LLVM MCA。因此默认目标架构生效,即该工具运行的任何架构。这恰好是SKX。您提到的循环总数是针对 SKX 的,但不清楚您是否在 SKX 上运行代码。您应该使用-mcpu 选项来指定架构。这与您传递给 gcc 的 -march 无关。另请注意,将核心周期与毫秒进行比较是没有意义的。您可以使用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

只有这个代码应该是 MCA 的输入。在源代码层面,真的没有办法告诉 MCA 只分析这段代码。您必须手动内联 std::accumulate 并将 LLVM-MCA-BEGINLLVM-MCA-END 标记放置在其中的某个位置。

当将0ULL 而非0.0 传递给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

请注意,在目标地址位于块中某处的代码中存在条件跳转jns。 MCA 只会假设跳跃会失败。如果在代码的实际运行中不是这种情况,MCA 将不必要地增加 7 条指令的开销。还有另一个跳转,jb,但我认为这对大向量并不重要,并且大部分时间都会掉下来。最后一个跳转,jne,也是最后一条指令,所以 MCA 会假设下一条指令又是最上面的一条。对于足够多的迭代,这个假设是完全正确的。

总体而言,很明显第一个代码比第二个代码小得多,因此它可能要快得多。你的测量确实证实了这一点。您也不需要使用微架构分析工具来了解原因。第二个代码只是做了更多的计算。因此,您可以很快得出结论,通过0.0 在所有架构上的性能和代码大小方面都更好。

【讨论】:

  • 不仅double(与0.0一起选择)版本更快,它是actually correct。中间转换可以提供非常意想不到的结果。
  • @BartekBanachewicz 请注意,-ffast-math 已传递给 gcc 以生成代码。见What does gcc's ffast-math actually do?。我没有彻底检查代码,但我确实希望两个版本对于-ffast-math 都是正确的(是的,使用-ffast-math 时它们可能不会产生相同的结果,没关系)。
  • @HadiBrais:一种查看(在 Godbolt 上)开始/结束标记位置的简单方法:在 asm 语句中添加 nop", like nop # LLVM-MCA-END`。取消选中 Godbolt 上的“//”框也可以,这样您就可以看到注释行,但是拥有一个实际的 nop 会使“滚动到程序集”右键单击菜单选项起作用。
  • LLVM-MCA 对于简单的后端绑定计算循环甚至都不准确:因为它具有不正确的参数,例如 Skylake 的“width = 6”,所以它认为 Skylake 可以执行 6 个 nop(或其他混合指令)在 1 个周期内。与 IACA 相比,这通常会使答案明显错误。
猜你喜欢
  • 1970-01-01
  • 2023-03-07
  • 1970-01-01
  • 1970-01-01
  • 2019-04-20
  • 2020-12-30
  • 1970-01-01
  • 2016-04-16
  • 1970-01-01
相关资源
最近更新 更多