在程序集x86_64中添加两个向量与AVX2以及技术说明

Ner*_*rva 5 c++ assembly simd openmp computer-architecture

我在这做错了什么?我得到4个零而不是:

2
4
6
8
Run Code Online (Sandbox Code Playgroud)

我也想修改我的.asm函数,以便在这里运行更长的向量,因为我只使用了一个带有四个元素的向量,这样我就可以在没有带有SIMD 256位寄存器的循环的情况下对该向量求和.

的.cpp

#include <iostream>
#include <chrono>

extern "C" double *addVec(double *C, double *A, double *B, size_t &N);

int main()
{
    size_t N = 1 << 2;
    size_t reductions = N / 4;

    double *A = (double*)_aligned_malloc(N*sizeof(double), 32);
    double *B = (double*)_aligned_malloc(N*sizeof(double), 32);
    double *C = (double*)_aligned_malloc(N*sizeof(double), 32);

    for (size_t i = 0; i < N; i++)
    {
        A[i] = double(i + 1);
        B[i] = double(i + 1);
    }

    auto start = std::chrono::high_resolution_clock::now();

        double *out = addVec(C, A, B, reductions);

    auto finish = std::chrono::high_resolution_clock::now();

    for (size_t i = 0; i < N; i++)
    {
        std::cout << out[i] << std::endl;
    }

    std::cout << "\n\n";

    std::cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish - start).count() << " ns\n";

    std::cin.get();

    _aligned_free(A);
    _aligned_free(B);
    _aligned_free(C);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

.ASM

.data
; C -> RCX
; A -> RDX
; B -> r8
; N -> r9
.code
    addVec proc
        ;xor rbx, rbx
        align 16
        ;aIn:
            vmovapd ymm0, ymmword ptr [rdx]
            ;vmovapd ymm1, ymmword ptr [rdx + rbx + 4]
            vmovapd ymm2, ymmword ptr [r8]
            ;vmovapd ymm3, ymmword ptr [r8 + rbx + 4]

            vaddpd ymm0, ymm2, ymm3

            vmovapd ymmword ptr [rcx], ymm3
        ;inc rbx
        ;cmp rbx, qword ptr [r9]
        ;jl aIn
        mov rax, rcx    ; return the address of the output vector
    ret
    addVec endp
end
Run Code Online (Sandbox Code Playgroud)

另外,我想补充一些其他说明:

  1. 我的CPU每个核心有8个256位寄存器(ymm0-ymm7)还是总共8个?
  2. 所有其他寄存器如rax,rbx等...总共或每个核心?
  3. 由于我只需要使用SIMD协处理器和一个内核就可以处理每个周期4个双倍,我可以在每个周期与CPU的其余部分执行另一条指令吗?那么例如我可以在一个核心的每个周期添加5个双倍?(4个SIMD + 1)
  4. 如果我在没有在汇编函数中放置循环的情况下执行以下操作会怎么样?:

    #pragma openmp parallel for

    for (size_t i = 0; i < reductions; i++)

    addVec(C + i, A + i, B + i)

    这会分叉coreNumber + hyperThreading线程,每个线程执行SIMD添加四个双?那么每个循环总共4*coreNumber加倍?我不能在这里添加hyperThreading吗?


我可以更新吗?:

.data
;// C -> RCX
;// A -> RDX
;// B -> r8
.code
    addVec proc
        ; One cycle 8 micro-op
            vmovapd ymm0, ymmword ptr [rdx]     ; 1 port
            vmovapd ymm1, ymmword ptr [rdx + 32]; 1 port
            vmovapd ymm2, ymmword ptr [r8]      ; 1 port
            vmovapd ymm3, ymmword ptr [r8 + 32] ; 1 port
            vfmadd231pd ymm0, ymm2, ymm4        ; 1 port
            vfmadd231pd ymm1, ymm3, ymm4        ; 1 port
            vmovapd ymmword ptr [rcx], ymm0     ; 1 port
            vmovapd ymmword ptr [rcx + 32], ymm1; 1 port

        ; Return the address of the output vector
        mov rax, rcx                            ; 1 port ?
    ret
    addVec endp
end
Run Code Online (Sandbox Code Playgroud)

或者只是这个'因为我会超过你告诉我的六个端口?

.data
;// C -> RCX
;// A -> RDX
;// B -> r8
.code
    addVec proc
        ;align 16
        ; One cycle 5 micro-op ?
        vmovapd ymm0, ymmword ptr [rdx]     ; 1 port
        vmovapd ymm1, ymmword ptr [r8]      ; 1 port
        vfmadd231pd ymm0, ymm1, ymm2        ; 1 port
        vmovapd ymmword ptr [rcx], ymm0     ; 1 port

        ; Return the address of the output vector
        mov rax, rcx                        ; 1 port ?
    ret
    addVec endp
end
Run Code Online (Sandbox Code Playgroud)

Z b*_*son 8

您的代码得到错误结果的原因是您的程序集中的语法向后.

您正在使用英特尔语法,其中目标应位于源之前.所以在你原来的.asm代码中你应该改变

vaddpd ymm0, ymm2, ymm3
Run Code Online (Sandbox Code Playgroud)

 vaddpd ymm3, ymm2, ymm0
Run Code Online (Sandbox Code Playgroud)

一种看待这种情况的方法是使用内在函数,然后查看反汇编.

extern "C" double *addVec(double * __restrict C, double * __restrict A, double * __restrict B, size_t &N) {
    __m256d x = _mm256_load_pd((const double*)A);
    __m256d y = _mm256_load_pd((const double*)B);
    __m256d z = _mm256_add_pd(x,y);
    _mm256_store_pd((double*)C, z);
    return C;
}
Run Code Online (Sandbox Code Playgroud)

在Linux上使用GCC来解释g++ -S -O3 -mavx -masm=intel -mabi=ms foo.cpp:

vmovapd ymm0, YMMWORD PTR [rdx]
mov     rax, rcx
vaddpd  ymm0, ymm0, YMMWORD PTR [r8]
vmovapd YMMWORD PTR [rcx], ymm0
vzeroupper
ret
Run Code Online (Sandbox Code Playgroud)

vaddpd ymm0, ymm0, YMMWORD PTR [rdx]指令将加载和加法融合到一个融合微操作中.当我在你的代码中使用该函数时,它获得2,4,6,8.

可以看到总结两个阵列的源代码xy并写出到一个数组zL1-存储器带宽-50-滴在-使用-地址-其中-不同逐4096效率.这使用内在函数并展开八次.使用gcc -S或解密代码objdump -d.另一个执行几乎完全相同的事情并且在汇编中编写的源代码是获取峰值带宽 - 仅在l1-cache-only-getting-62.在文件triad_fma_asm.asm中将行更改pi: dd 3.14159pi: dd 1.0.这两个示例都使用单个浮点,因此如果您想要加倍,则必须进行必要的更改.

您的其他问题的答案是:

  1. 处理器的每个核心都是物理上不同的单元,具有自己的寄存器集.每个核心有16个通用寄存器(例如RAX,RBX,R8,R9,...)和一些特殊用途的寄存器(例如RFLAGS).在32位模式下,每个内核有8个256位寄存器,在64位模式下有16个256位寄存器.当AVX-512可用时,将有32个512位寄存器(但在32位模式下只有8个).

请注意,每个内核的寄存器数量远远多于您可以直接编程的逻辑寄存器.

  1. 见上文

  2. 自2006年以来,通过Haswell的Core2处理器每个时钟最多可以处理4个μop.然而,使用称为微操作融合和宏操作融合的两种技术,Haswell可以在每个时钟周期实现6个微操作.

微操作融合可以将例如负载和加法融合到一个所谓的融合微操作中,但是每个微操作仍然需要其自己的端口.宏操作融合可以融合例如标量添加和跳转到一个只需要一个端口的微操作.宏观操作融合基本上是两个一个.

Haswell有八个端口.您可以使用这样的七个端口在一个时钟周期内获得六个微操作.

256-load + 256-FMA    //one fused µop using two ports
256-load + 256-FMA    //one fused µop using two ports
256-store             //one µop using two ports
64-bit add + jump     //one µop using one port
Run Code Online (Sandbox Code Playgroud)

因此实际上Haswell的每个内核可以处理16个双精度数(每个FMA增加4个乘法和4个),两个256负载,一个256位存储,以及一个64位的加法和分支,在一个时钟周期内完成.在这个问题中,获得峰值带宽一直在l1-cache-only-getting-62,我在理论上使用六个端口在一个时钟周期内获得了五个微操作.然而,在Haswell的实践中,这很难实现.

对于您的特定操作,它读取两个数组并写入一个,它受每个时钟周期的两次读取限制,因此每个时钟周期只能发出一个FMA.所以它能做的最好的是每个时钟周期四个双打.

  1. 如果你正确地并行你的代码,您的处理器有四个物理核心,那么你可以达到在一个时钟周期内64个浮点运算(2FMA*4cores).这对于某些操作来说是理论上最好的,但对于你的问题中的操作则不是.

但是,让我告诉你英特尔不希望人们谈论太多的秘密.大多数操作都受内存带宽限制,并且无法从并行化中获益.这包括您问题中的操作.因此,尽管英特尔每隔几年就会推出新技术(例如AVX,FMA,AVX512,内核数量翻倍),每次声称摩尔定律在实践中得到的性能翻倍,平均效益是线性的而不是指数的它已经有好几年了.