使用 Rust SSE 内在函数进行浮点乘法没有加速

问题描述 投票:0回答:1

我正在尝试使用 Rust 中的内在函数进行实验,其中我制作了一个大的浮点数向量,然后记录将它们全部乘以一个常数所需的时间。接下来,我尝试使用 SSE 内在函数进行同样的操作。在一台具有 5000 万个浮点的相对较新的笔记本电脑上,无论哪种方式,它的时间约为 23 毫秒。当我用 AVX 做类似的事情时,它甚至更慢,比如 67ms。在启动计时器之前,我分配了输入和输出所需的所有内存。

这是我如何用简单的

f32
来做到这一点:

let mut rng = rand::thread_rng();
let x: Vec<f32> = (0..N).map(|_| rng.gen::<i16>() as f32).collect();

let mut z_native: Vec<f32> = (0..N).map(|_| 0.0).collect();

let t0 = Instant::now();
for i in 0..N {
    *z_native.get_unchecked_mut(i) = *x.get_unchecked(i) * std::f32::consts::PI;
}
println!("Native {:?}", t0.elapsed());

这是我使用 SSE 的方法:

let mut x_sse: Vec<__m128> = vec![];
let y_sse: __m128 = x86_64::_mm_set_ps1(std::f32::consts::PI);
let mut z_sse: Vec<__m128> = vec![];

for i in 0..(N/4) {
    x_sse.push(x86_64::_mm_set_ps(
        x[(i*4)+3], x[(i*4)+2],
        x[(i*4)+1], x[(i*4)+0],
    ));
    z_sse.push(x86_64::_mm_setzero_ps());
}

let t0 = Instant::now();
for i in 0..(N/4) {
    *z_sse.get_unchecked_mut(i) = x86_64::_mm_mul_ps(*x_sse.get_unchecked(i), y_sse);
}
println!("SSE {:?}", t0.elapsed());

关于它应该如何工作,我是否缺少一些东西?我知道内存对齐如何产生影响,但我读到

Vec
总是根据它包含的内容的大小对齐。我还在最后打印出从结果中随机选择的值,以确保它们不会以某种方式得到优化。谢谢!

assembly rust x86-64 sse avx2
1个回答
0
投票

该示例很难重现,因为我们不知道实际的编译标志。 这是我在计算机上观察到的情况;这与问题报告的内容非常不同。

for
上的
z_native
循环未运行
objdump
显示两个
std::sys::unix::time::Timespec::now()
调用之间几乎没有代码),因为存储的值随后不会被使用

也许

for
上的
z_sse
循环被认为是更多指令(我们显式地一一写出sse指令),那么编译器不会优化它。

如果我们在 std::hint::black_box()

 上使用 
z_native
,一旦计算完毕,我们就会强制编译器认为该数组有用,然后保留计算(
objdump
显示了两个
std::sys::unix::time::Timespec::now()
调用之间的循环代码).

在这种情况下,当没有特定于 cpu 的选项进行编译时(这似乎相当于

-C target-feature=+sse
),我得到了这个 native 循环:

    80f0:       41 0f 10 44 86 d0       movups -0x30(%r14,%rax,4),%xmm0
    80f6:       41 0f 10 4c 86 e0       movups -0x20(%r14,%rax,4),%xmm1
    80fc:       0f 59 c2                mulps  %xmm2,%xmm0
    80ff:       0f 59 ca                mulps  %xmm2,%xmm1
    8102:       41 0f 11 44 87 d0       movups %xmm0,-0x30(%r15,%rax,4)
    8108:       41 0f 11 4c 87 e0       movups %xmm1,-0x20(%r15,%rax,4)
    810e:       41 0f 10 44 86 f0       movups -0x10(%r14,%rax,4),%xmm0
    8114:       41 0f 10 0c 86          movups (%r14,%rax,4),%xmm1
    8119:       0f 59 c2                mulps  %xmm2,%xmm0
    811c:       0f 59 ca                mulps  %xmm2,%xmm1
    811f:       41 0f 11 44 87 f0       movups %xmm0,-0x10(%r15,%rax,4)
    8125:       41 0f 11 0c 87          movups %xmm1,(%r15,%rax,4)
    812a:       48 83 c0 10             add    $0x10,%rax
    812e:       48 3d 8c f0 fa 02       cmp    $0x2faf08c,%rax
    8134:       75 ba                   jne    80f0 <_ZN10my_project4main17h3b5b4aee6bb4180fE+0x120>

另一方面,sse循环看起来像这样:

    82d0:       41 0f 28 44 07 f0       movaps -0x10(%r15,%rax,1),%xmm0
    82d6:       0f 59 c1                mulps  %xmm1,%xmm0
    82d9:       41 0f 29 44 04 f0       movaps %xmm0,-0x10(%r12,%rax,1)
    82df:       41 0f 28 04 07          movaps (%r15,%rax,1),%xmm0
    82e4:       0f 59 c1                mulps  %xmm1,%xmm0
    82e7:       41 0f 29 04 04          movaps %xmm0,(%r12,%rax,1)
    82ec:       48 83 c0 20             add    $0x20,%rax
    82f0:       48 3d 10 c2 eb 0b       cmp    $0xbebc210,%rax
    82f6:       75 d8                   jne    82d0 <_ZN10my_project4main17h3b5b4aee6bb4180fE+0x300>

循环展开似乎不一样,但我无法解释为什么,并且 native 循环的持续时间大约是 sse 循环持续时间的两倍。

-C target-feature=+avx
编译时,情况非常相似。 native循环看起来像这样:

    8170:       c4 c1 7c 59 4c 86 a0    vmulps -0x60(%r14,%rax,4),%ymm0,%ymm1
    8177:       c4 c1 7c 59 54 86 c0    vmulps -0x40(%r14,%rax,4),%ymm0,%ymm2
    817e:       c4 c1 7c 59 5c 86 e0    vmulps -0x20(%r14,%rax,4),%ymm0,%ymm3
    8185:       c4 c1 7c 59 24 86       vmulps (%r14,%rax,4),%ymm0,%ymm4
    818b:       c4 c1 7c 11 4c 87 a0    vmovups %ymm1,-0x60(%r15,%rax,4)
    8192:       c4 c1 7c 11 54 87 c0    vmovups %ymm2,-0x40(%r15,%rax,4)
    8199:       c4 c1 7c 11 5c 87 e0    vmovups %ymm3,-0x20(%r15,%rax,4)
    81a0:       c4 c1 7c 11 24 87       vmovups %ymm4,(%r15,%rax,4)
    81a6:       48 83 c0 20             add    $0x20,%rax
    81aa:       48 3d 98 f0 fa 02       cmp    $0x2faf098,%rax
    81b0:       75 be                   jne    8170 <_ZN10my_project4main17h3b5b4aee6bb4180fE+0x120>

sse 循环看起来像这样:

    8360:       c4 c1 78 59 4c 07 f0    vmulps -0x10(%r15,%rax,1),%xmm0,%xmm1
    8367:       c4 c1 78 29 4c 04 f0    vmovaps %xmm1,-0x10(%r12,%rax,1)
    836e:       c4 c1 78 59 0c 07       vmulps (%r15,%rax,1),%xmm0,%xmm1
    8374:       c4 c1 78 29 0c 04       vmovaps %xmm1,(%r12,%rax,1)
    837a:       48 83 c0 20             add    $0x20,%rax
    837e:       48 3d 10 c2 eb 0b       cmp    $0xbebc210,%rax
    8384:       75 da                   jne    8360 <_ZN10my_project4main17h3b5b4aee6bb4180fE+0x310>

循环展开策略似乎又有所不同,但在 native 循环中使用 avx 寄存器似乎没有什么好处,因为它的持续时间大约是 sse 循环持续时间的两倍。

总而言之,显式 sse 循环总是比 native 循环更好。 然而,正如 @PeterCordes 在其评论中建议的那样,编译器应该发现 native 循环对于自动向量化来说是微不足道的,但很难理解为什么这种自动向量化(假设的微不足道)不如显式版本那么有效。

© www.soinside.com 2019 - 2024. All rights reserved.