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

2
4
6
8

我也很想修改我的.asm函数,以便通过更长的 vector 运行-因为在这里简化了,我只使用了一个具有四个元素的 vector ,这样我就可以对这个 vector 求和而不用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;
}

.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

我也想作一些其他澄清:
  • 我的CPU的每个内核有八个256位寄存器(ymm0-ymm7)还是共有八个?
  • 所有其他寄存器(如rax,rbx等)是总计还是每个核心?
  • 因为仅使用SIMD协处理器和一个内核就可以每个周期处理4个双倍数,所以我可以在其余CPU上每个周期执行另一个指令吗?例如,我可以在一个内核的每个周期内增加5倍吗? (4个含SIMD +1个)
  • 如果我在不执行循环的情况下执行以下操作,该怎么办?
    #pragma openmp parallel forfor (size_t i = 0; i < reductions; i++)addVec(C + i, A + i, B + i)
    这将派生coreNumber + hyperThreading线程,并且每个线程在四个double上执行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
    

    还是仅仅因为这会超过您告诉我的六个端口?
    .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
    

    最佳答案

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

    您正在使用Intel语法,其中目标应位于源之前。因此,在您原始的.asm代码中,您应该进行更改

    vaddpd ymm0, ymm2, ymm3
    


     vaddpd ymm3, ymm2, ymm0
    

    一种查看方式是使用内部函数,然后查看反汇编。
    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;
    }
    

    在Linux上使用g++ -S -O3 -mavx -masm=intel -mabi=ms foo.cpp从GCC中反汇编得出:
    vmovapd ymm0, YMMWORD PTR [rdx]
    mov     rax, rcx
    vaddpd  ymm0, ymm0, YMMWORD PTR [r8]
    vmovapd YMMWORD PTR [rcx], ymm0
    vzeroupper
    ret
    
    vaddpd ymm0, ymm0, YMMWORD PTR [rdx]指令将负载和加法融合到一个融合的微操作中。当我将该函数与您的代码一起使用时,它将得到2,4,6,8。

    您可以在l1-memory-bandwidth-50-drop-in-efficiency-using-addresses-which-differ-by-4096处找到将两个数组xy求和的源代码,并将它们写出到z数组中。这将使用内部函数并将其展开八次。使用gcc -Sobjdump -d取消该代码。另一个几乎相同的事情,也是用汇编语言编写的,位于obtaining-peak-bandwidth-on-haswell-in-the-l1-cache-only-getting-62。在文件triad_fma_asm.asm中,将pi: dd 3.14159行更改为pi: dd 1.0。这两个示例都使用单个浮点,因此,如果要加倍,则必须进行必要的更改。

    您其他问题的答案是:
  • 处理器的每个内核都是物理不同的单元,具有自己的一组寄存器。
    每个内核具有16个通用寄存器(例如rax,rbx,r8,r9等)和几个专用寄存器(例如RFLAGS)。在32位模式下,每个内核都有8个256位寄存器,在64位模式下,则有16个256位寄存器。当AVX-512可用时,将有32个512位寄存器(但在32位模式下只有8个)。

  • 请注意,每个内核都具有far more registers,而不是您可以直接编程的逻辑内核。
  • 参见1.以上
  • 从2006年开始,通过Haswell的
  • Core2处理器每个时钟最多可以处理4 µop。但是,使用两种称为微操作融合和宏操作融合的技术,使用Haswell可以在每个时钟周期实现六个微操作。

  • 微操作融合可以融合例如一个负载和一个附加负载,即所谓的融合微操作,但每个微操作仍需要自己的端口。宏运算融合可以融合例如标量加法和跳入仅需要一个端口的微型运算。宏操作融合本质上是一对二的。

    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
    

    因此,实际上,Haswell的每个内核都可以在一个时钟周期内处理16个 double (每个FMA进行4个乘法和4个加法),2个256负载,1个256位存储以及1个64位加法和分支。在这个问题obtaining-peak-bandwidth-on-haswell-in-the-l1-cache-only-getting-62中,我(理论上)使用六个端口在一个时钟周期内获得了五个微运算。但是,实际上在Haswell上很难做到这一点。

    对于您的特定操作,它读取两个数组并写入一个数组,因此每个时钟周期受两次读取的约束,因此每个时钟周期只能发出一个FMA。因此,最好的办法是每个时钟周期增加四倍。
  • 如果正确并行化代码并且处理器具有四个物理核心,则可以在一个时钟周期内实现64个双浮点运算(2FMA * 4核心)。从理论上讲,这对于某些操作而言是最好的,但对于您所讨论的操作而言,则不是最好的。

  • 但是,让我告诉您一个小 secret ,即英特尔不希望人们谈论太多。 Most operations are memory bandwidth bound,并不能从并行化中受益良多。这包括您问题中的操作。因此,尽管英特尔每隔几年就会不断推出新技术(例如,AVX,FMA,AVX512,将内核数量增加一倍),这每次都会使性能翻倍,从而声称在实践中获得摩尔定律,但平均 yield 是线性的,而不是指数级的现在已经有好几年了。

    08-07 14:38