为什么C / RUST中的一个加法计算在结果ASM中有3个双精度浮点加法工具?

com*_*lip -1 c assembly simd rust auto-vectorization

简单的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];
    }
}
Run Code Online (Sandbox Code Playgroud)

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

有一addpd和二addsd。两者都是与双精度有关的。

另一个类似的锈代码,获得了更多的双精度添加工具: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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 6

在C ++的GCC输出中,前2个来自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


Jmb*_*Jmb 6

尝试在不进行优化的情况下进行编译,您只会得到一条addsd指令。C代码中的两个额外加法是由于自动矢量化。特别是,如果您查看反汇编的第34和37行,则会看到向量存储器访问。该addpd是主要额外增加了量化代码和两个addsds为有处理边界条件。

Rust代码中的额外说明归因于循环展开。

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

  • 默认情况下,GCC不会在-O3处进行循环展开。-funroll-loops是-fprofile-use的一部分启用,也可以手动启用。Clang / LLVM确实会展开循环。普通的Rust编译器基于LLVM,它会根据调整选项将细小循环/小循环展开4或2倍或更多倍,这就是OP在Rust中看到的样子,但是*在*中没有发生您正在谈论的代码。(当行程数是小的恒定迭代次数时,GCC仍将*完全*展开某些循环。尤其是如果跨迭代进行恒定传播可以大大简化循环主体) (3认同)