-1

简单的 C 代码,只有一个双精度加法。

void test(double *a, double *b, long n) {
    for (long j = 0; j < n; j++)
    for (long i = 0; i < n; i++) {
        b[i] = b[i] + a[j];
    }
}

在编译器资源管理器中获取 ASM 结果:https ://godbolt.org/z/tJ-d39

有一个addpd和两个addsd。两者都是双精度相关的。

另一个类似的 rust 代码,得到了更多的双精度添加工具:https ://godbolt.org/z/c49Wuh

pub unsafe fn test(a: &mut [f64], b: &mut [f64], n: usize) {
    for j in 0..n {
        for i in 0..n {
            *b.get_unchecked_mut(i) = *b.get_unchecked_mut(i) + *a.get_unchecked_mut(j);
        }
    }
}
4

2 回答 2

6

在 C++ 的 GCC 输出中,前 2 个来自使用 (Packed Double) 的自动矢量化+ 使用(Scalar Double)addpd的标量清理。addsd如果您想将其编译为 C,-xc请在编译器选项中使用。

addsd对于输入数组重叠的情况,底部的额外内容位于单独的纯标量循环中。


这两个标量addsd指令是必要的,因为您没有向编译器承诺输入数组不重叠(与double *restrict a),并且您没有承诺大小是偶数个doubles。

因此,要使用 SIMD 自动矢量化,我们需要检查重叠。如果长度不是 SIMD 向量的整数,我们需要进行清理。

这也是为什么函数中有这么多整数指令,而不仅仅是 2 个简单的嵌套循环的原因。

您的 Rust/LLVM 输出是相同的,但对主 SIMD 循环进行了循环展开(LLVM 默认执行此操作)。因此,标量清理循环可能需要运行 1 次以上的迭代,因为 1 次 SIMD 循环迭代不仅仅执行 2 个元素。


不幸的是,GCC/clang 没有优化你的函数来总结a[0..n-1]然后循环b一次,将总数添加到每个元素中。这将是合法的-ffast-math(否则不是因为 FP 数学不是严格关联的),但不幸的是编译器无论如何都不会这样做。您必须在源代码中自己完成。

这是一个重大的错过优化,从复杂性O(n^2)开始。O(n)但它是编译器不会为你做的,即使是-ffast-math.

于 2019-07-12T09:01:11.980 回答
6

尝试在不优化的情况下进行编译,您只会得到一条addsd指令。C 代码中的两个额外添加是由于自动矢量化。特别是如果您查看反汇编的第 34 和 37 行,您将看到向量内存访问。addpd是矢量化代码的主要补充,两个saddsd用于处理边界条件。

Rust 代码中的额外指令是由于循环展开。

正如@Peter Cordes 所指出的,gcc 在优化时默认不会进行循环展开-O3,而 LLVM(Rust 编译器所基于的)会这样做。因此 C 代码和 Rust 代码之间的区别。

于 2019-07-12T09:01:47.400 回答