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

细节:

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

std::accumulate(begin, end, starValue)

为了预测运行时间,我使用了带有LLVM机器代码分析器(llvm-mca)插件的Compiler Explorer(https://godbolt.org/z/5HDzSF),因为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的挂钟时间之间没有相关性。例如,在上面的代码中,总周期为2806,运行时间为14ms。当我切换到startValue 0ULL时,总周期为2357,但运行时为117ms。

最佳答案

TL:DR:LLVM-MCA分析了这些注释之间的整个代码块,就好像它们是循环的主体一样,并向您显示了所有这些指令的100次迭代的周期数。

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

如果取消选中Godbolt编译器资源管理器上的“//”框,或修改asm语句以添加"nop # LLVM-MCA-END"之类的nop,则可以在asm窗口中找到这些行,并查看LLVM-MCA的内容这是“循环”。

LLVM MCA模拟指定的汇编指令序列,并计算在指定的目标体系结构上每次迭代执行所需的周期数。 LLVM MCA进行了许多简化,例如(超出我的脑袋):(1)假定所有条件分支均落空,(2)假定所有内存访问均属于Write Back内存类型,并且所有命中L1高速缓存,(3)假定前端工作最佳,并且(4)call指令不遵循被调用过程,它们只是掉线了。我还无法记忆起其他假设。

本质上,LLVM MCA(如Intel IACA)仅适用于后端计算绑定(bind)的简单循环。在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,但是我认为这一步对于大型 vector 而言并不重要,并且在大多数情况下都会失败。最后的跳转jne也是最后一条指令,因此MCA会假定下一条指令再次是最上面的一条。对于足够多的迭代,此假设非常好。

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

关于c++ - (如何)可以使用LLVM机器代码分析器预测代码片段的运行时间?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/54107384/

10-11 23:08