【问题标题】:C++ - Apply std::exp to an std::vectorC++ - 将 std::exp 应用于 std::vector
【发布时间】:2020-03-18 02:07:37
【问题描述】:

有没有比简单的方法更快(从性能角度)的方法

std::vector<double> y;
y.reserve(x.size());
for(size_t i = 0; i < x.size(); ++i)
    y.push_back(std::exp(x[i]));

【问题讨论】:

  • 在我看来,这就像使用 GPGPU 而不是 CPU 可以非常有效地完成的操作。但是,您不能用标准 C++ 编写计算内核。

标签: c++ c++11 c++-standard-library exp


【解决方案1】:

如果您需要最接近的 ULP 的最大精度,这可能与您将获得的一样快。

如果你能接受一些近似误差,有much faster methods that use SIMD

【讨论】:

  • 应注意,链接解决方案专门适用于float 计算,而不是double。 SIMD 也不一定要牺牲准确性,因为压缩双精度存在_exp 指令。因此,如果编译器没有自动对其进行向量化,那么仍然可以通过为目标架构手动滚动 SIMD 计算或使用向量库来实现一些加速。
【解决方案2】:

push_back,令人惊讶的是,它有一点开销,因为它实际上并不知道你已经保留了足够的空间,所以它总是需要检查。因为这个检查可以改变循环迭代之间的控制流,push_back 排除了编译器的自动向量化。

考虑这两个函数,其中第一个使用push_back,而第二个就地修改副本(或移入值):

auto exp1(std::vector<double> const& xs) -> std::vector<double> {
    auto ys = std::vector<double>{};
    ys.reserve(xs.size());
    for(auto x : xs){ ys.push_back(std::exp(x)); }
}

auto exp2(std::vector<double> xs) -> std::vector<double> {
    for(auto & x : xs){ x = std::exp(x); }
    return xs;
}

We'll look at the assembly output,如果在 GCC 9.1 中编译时使用

gcc -std=c++17 -O3 -march=skylake-avx512

这是exp1的内部循环(嵌入了相当多的额外代码,因为你已经reserved,所以这些代码永远不会被执行):

.L45:
        add     rbx, 8
        vmovsd  QWORD PTR [r14], xmm0
        add     r14, 8
        cmp     r12, rbx
        je      .L44
.L18:
        vmovsd  xmm0, QWORD PTR [rbx]
        call    exp
        vmovsd  QWORD PTR [rsp], xmm0
        cmp     rbp, r14
        jne     .L45

这是exp2的:

.L53:
        vmovsd  xmm0, QWORD PTR [rbx]
        add     rbx, 8
        call    exp
        vmovsd  QWORD PTR [rbx-8], xmm0
        cmp     rbp, rbx
        jne     .L53

实际上,它们基本相同,因为exp 很复杂,GCC 不知道如何自动对其进行向量化。但是,考虑在内循环中发生更简单的事情的情况:

auto sq1(std::vector<double> const& xs) -> std::vector<double> {
    auto ys = std::vector<double>{};
    ys.reserve(xs.size());
    for(auto x : xs){ ys.push_back(x*x); }
}

auto sq2(std::vector<double> xs) -> std::vector<double> {
    for(auto & x : xs){ x *= x; }
    return xs;
}

这是sq1的内部循环:

.L89:
        vmovsd  QWORD PTR [rsi], xmm0
        add     rbx, 8
        add     rsi, 8
        mov     QWORD PTR [rsp+24], rsi
        cmp     rbp, rbx
        je      .L72
.L75:
        vmovsd  xmm0, QWORD PTR [rbx]
        mov     rsi, QWORD PTR [rsp+24]
        vmulsd  xmm0, xmm0, xmm0
        vmovsd  QWORD PTR [rsp+8], xmm0
        cmp     rsi, QWORD PTR [rsp+32]
        jne     .L89

这里是sq2。请注意,它使用 vmulpdymm 寄存器,并且一次跳转 32 个字节而不是一次 8 个字节。

.L11:
        vmovupd ymm0, YMMWORD PTR [rdx]
        add     rdx, 32
        vmulpd  ymm0, ymm0, ymm0
        vmovupd YMMWORD PTR [rdx-32], ymm0
        cmp     rdx, rcx
        jne     .L11

当然,这个内部循环 sn-p 有点误导:如果它的大小没有被 4 整除,它隐藏了大量用于处理 std::vector 其余部分的代码。不过,我的主要关键是,是的,你实际上可以做得比reserve + push_back 稍微好一点(当我第一次发现时,这让我很惊讶),如果我们不处理exp,情况会好得多特别是。

【讨论】:

    猜你喜欢
    • 2017-01-25
    • 2011-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-21
    • 2019-10-20
    相关资源
    最近更新 更多