c++ - AVX 的乘加矢量化比 SSE 慢

标签 c++ performance optimization sse avx

我有一段代码在竞争激烈的锁下运行,所以它需要尽可能快。代码非常简单——它是对一堆数据的基本乘加,如下所示:

for( int i = 0; i < size; i++ )
{
    c[i] += (double)a[i] * (double)b[i];
}
在启用 SSE 支持的 -O3 下,代码正在按照我的预期进行矢量化。但是,打开 AVX 代码生成后,我的速度降低了大约 10-15%,而不是加速,我不知道为什么。
这是基准代码:
#include <chrono>
#include <cstdio>
#include <cstdlib>

int main()
{
    int size = 1 << 20;

    float *a = new float[size];
    float *b = new float[size];
    double *c = new double[size];

    for (int i = 0; i < size; i++)
    {
        a[i] = rand();
        b[i] = rand();
        c[i] = rand();
    }

    for (int j = 0; j < 10; j++)
    {
        auto begin = std::chrono::high_resolution_clock::now();

        for( int i = 0; i < size; i++ )
        {
            c[i] += (double)a[i] * (double)b[i];
        }

        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count();

        printf("%lluus\n", duration);
    }
}
这是在 SSE 下生成的程序集:
0x100007340 <+144>:  cvtps2pd (%r13,%rbx,4), %xmm0
0x100007346 <+150>:  cvtps2pd 0x8(%r13,%rbx,4), %xmm1
0x10000734c <+156>:  cvtps2pd (%r15,%rbx,4), %xmm2
0x100007351 <+161>:  mulpd  %xmm0, %xmm2
0x100007355 <+165>:  cvtps2pd 0x8(%r15,%rbx,4), %xmm0
0x10000735b <+171>:  mulpd  %xmm1, %xmm0
0x10000735f <+175>:  movupd (%r14,%rbx,8), %xmm1
0x100007365 <+181>:  addpd  %xmm2, %xmm1
0x100007369 <+185>:  movupd 0x10(%r14,%rbx,8), %xmm2
0x100007370 <+192>:  addpd  %xmm0, %xmm2
0x100007374 <+196>:  movupd %xmm1, (%r14,%rbx,8)
0x10000737a <+202>:  movupd %xmm2, 0x10(%r14,%rbx,8)
0x100007381 <+209>:  addq   $0x4, %rbx
0x100007385 <+213>:  cmpq   $0x100000, %rbx           ; imm = 0x100000 
0x10000738c <+220>:  jne    0x100007340               ; <+144> at main.cpp:26:20
运行 SSE 基准的结果:
1411us
1246us
1243us
1267us
1242us
1237us
1246us
1242us
1250us
1229us
启用 AVX 的生成程序集:
0x1000070b0 <+144>:  vcvtps2pd (%r13,%rbx,4), %ymm0
0x1000070b7 <+151>:  vcvtps2pd 0x10(%r13,%rbx,4), %ymm1
0x1000070be <+158>:  vcvtps2pd 0x20(%r13,%rbx,4), %ymm2
0x1000070c5 <+165>:  vcvtps2pd 0x30(%r13,%rbx,4), %ymm3
0x1000070cc <+172>:  vcvtps2pd (%r15,%rbx,4), %ymm4
0x1000070d2 <+178>:  vmulpd %ymm4, %ymm0, %ymm0
0x1000070d6 <+182>:  vcvtps2pd 0x10(%r15,%rbx,4), %ymm4
0x1000070dd <+189>:  vmulpd %ymm4, %ymm1, %ymm1
0x1000070e1 <+193>:  vcvtps2pd 0x20(%r15,%rbx,4), %ymm4
0x1000070e8 <+200>:  vcvtps2pd 0x30(%r15,%rbx,4), %ymm5
0x1000070ef <+207>:  vmulpd %ymm4, %ymm2, %ymm2
0x1000070f3 <+211>:  vmulpd %ymm5, %ymm3, %ymm3
0x1000070f7 <+215>:  vaddpd (%r14,%rbx,8), %ymm0, %ymm0
0x1000070fd <+221>:  vaddpd 0x20(%r14,%rbx,8), %ymm1, %ymm1
0x100007104 <+228>:  vaddpd 0x40(%r14,%rbx,8), %ymm2, %ymm2
0x10000710b <+235>:  vaddpd 0x60(%r14,%rbx,8), %ymm3, %ymm3
0x100007112 <+242>:  vmovupd %ymm0, (%r14,%rbx,8)
0x100007118 <+248>:  vmovupd %ymm1, 0x20(%r14,%rbx,8)
0x10000711f <+255>:  vmovupd %ymm2, 0x40(%r14,%rbx,8)
0x100007126 <+262>:  vmovupd %ymm3, 0x60(%r14,%rbx,8)
0x10000712d <+269>:  addq   $0x10, %rbx
0x100007131 <+273>:  cmpq   $0x100000, %rbx           ; imm = 0x100000 
0x100007138 <+280>:  jne    0x1000070b0               ; <+144> at main.cpp:26:20
运行 AVX 基准测试的结果:
1532us
1404us
1480us
1464us
1410us
1383us
1333us
1362us
1494us
1526us
请注意,使用两倍于 SSE 的指令生成的 AVX 代码并不重要 - 我已经尝试过手动较小的展开(以匹配 SSE),但 AVX 仍然较慢。
对于上下文,我使用的是 macOS 11 和 Xcode 12,以及 Mac Pro 6.1(垃圾桶)和 Intel Xeon CPU E5-1650 v2 @ 3.50GHz。

最佳答案

更新:对齐并没有多大帮助/根本没有帮助。也可能存在另一个瓶颈,例如在压缩浮点数 ->双转换?另外,vcvtps2pd (%r13,%rbx,4), %ymm0只有一个 16 字节的内存源,所以只有存储是 32 字节的。我们没有任何 32 字节的拆分加载。 (我在仔细查看代码之前写了下面的答案。)

那是一个 IvyBridge CPU。您的数据是否按 32 对齐?如果不是,那么众所周知的事实是,32 字节加载或存储上的缓存行拆分是那些旧微体系结构的严重瓶颈。那些支持 Intel AVX 的早期 CPU 具有全宽 ALU,但它们运行 32 字节加载并在来自同一 uop1 的执行单元中存储为 2 个单独的数据周期,从而使缓存行拆分成为一个额外的特殊(和额外慢)的情况. ( https://www.realworldtech.com/sandy-bridge/7/ )。不像 Haswell (和 Zen 2)及更高版本,具有 32 字节数据路径2。
慢到 GCC 的默认设置 -mtune=generic代码生成会even split 256-bit AVX loads and stores在编译时不知道要对齐。 (这是一种矫枉过正的方式,尤其是在较新的 CPU 上,和/或当数据实际上已对齐但编译器不知道时,或者在常见情况下数据已对齐时,但该功能仍需要偶尔工作时未对齐的数组,让硬件处理这种特殊情况,而不是在普通情况下运行更多指令甚至检查这种特殊情况。)
但是您正在使用 clang,它在这里制作了一些不错的代码(展开 4 倍),可以在对齐数组或像 Haswell 这样的较新 CPU 上表现良好。不幸的是,它使用索引寻址模式,违背了展开的大部分目的(特别是对于 Intel Sandybridge/Ivy Bridge),因为负载和 ALU uop 将分开并分别通过前端。 Micro fusion and addressing modes . (Haswell 可以为 SSE 案例保留其中一些微融合,但不能为 AVX,例如商店。)
您可以使用 aligned_alloc ,或者也许做一些与 C++17 对齐的事情 new获得与 delete 兼容的对齐分配.
普通 new可能会给您一个 16 对齐的指针,但未对齐 32。我不知道 MacOS,但在 Linux glibc 的大型分配分配器上,通常会在页面开头保留 16 个字节用于簿记,因此您通常会得到比 16 大的任何对齐距离 16 字节的大分配。

脚注 2:在加载端口中花费第二个周期的单 uop 仍然只生成一次地址。这允许另一个 uop(例如存储地址 uop)在第二个数据周期发生时使用 AGU。因此它不会干扰完全流水线化的地址处理部分。
SnB/IvB 只有 2 个 AGU/加载端口,因此通常每个时钟最多可以执行 2 个内存操作,其中最多一个是存储。但是对于 32 字节的加载和存储,每 2 个数据周期只需要一个地址(并且存储数据已经是来自存储地址的另一个端口的单独 uop),这允许 SnB/IvB 实现每个时钟 2 + 1 加载 + 存储,持续,用于 32 字节加载和存储的特殊情况。 (这使用了大部分前端带宽,因此这些负载通常需要微融合作为另一条指令的内存源操作数。)
另见我在 How can cache be that fast? 上的回答在电子.SE。
脚注 1: Zen 1(和推土机系列)将所有 32 字节操作解码为 2 个单独的 uops,因此没有特殊情况。一半的负载可以跨缓存线分配,这与来自 xmm 的 16 字节负载完全一样。加载。

关于c++ - AVX 的乘加矢量化比 SSE 慢,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65297855/

相关文章:

.net - 如何解决VB.NET 1.1应用程序的性能下降问题?

performance - Hibernate,使用参数查询 SQL。表现不佳

c++ - c++一致性是否使用decltype来帮助模板推导?

c++ - 在此范围内未声明的变量 数组线性搜索

c++ - 如何从 .fxo 文件创建 ID3D11VertexShader?

Java 线程创建开销

c++ - C++中使用双指针的动态二维数组

Java 优化 - 局部变量与对象属性

PHP 代码缓存和优化

sql - 优化数百万行的自连接