在带有AVX2的组件x86_64中添加两个矢量以及技术说明



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

2
4
6
8

我也很想修改我的.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 << "nn";
    std::cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish - start).count() << " nsn";
    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

此外,我还想得到一些其他澄清:

  1. 我的CPU的每个核心是否有八个256位寄存器(ymm0-ymm7),或者总共有八个
  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每个周期翻一番?我不能在这里添加超线程,对吗?


更新我可以这样做吗?:

.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;
}

使用g++ -S -O3 -mavx -masm=intel -mabi=ms foo.cpp与Linux上的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]指令将负载和加法融合为一个融合的micro-op。当我在你的代码中使用这个函数时,它会得到2,4,6,8。

您可以找到源代码,它将两个数组xy求和,并以l1-内存带宽-50高效使用地址-which-different-by-4096写入数组z。这使用内部函数并展开八次。用gcc -Sobjdump -d分解代码。另一个做着几乎相同的事情并以汇编形式编写的源代码是在获取峰值带宽时,即在获取缓存时。在文件triad_fma_asm.asm中,将行pi: dd 3.14159更改为pi: dd 1.0。这两个例子都使用了单浮点,所以如果你想要double,你必须做出必要的改变。

你其他问题的答案是:

  1. 处理器的每个核心都是一个物理上不同的单元,有自己的一组寄存器。每个核心有16个通用寄存器(例如rax、rbx、r8、r9…)和几个专用寄存器(例如RFLAGS)。在32位模式中,每个核心具有8个256位寄存器,在64位模式中具有16个256位的寄存器。当AVX-512可用时,将有32个512位寄存器(但在32位模式下只有8个)

请注意,每个核心都有比您可以直接编程的逻辑寄存器多得多的寄存器。

  1. 参见1。高于

  2. 从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个倍频(每个FMA有四个乘法和四个加法)、两个256加载、一个256位存储以及一个64位加法和分支。在这个问题中,获得峰值带宽-峰值带宽-缓存-获取-62,我(理论上)使用六个端口在一个时钟周期内获得了五个微操作。然而,在Haswell的实践中,这是很难实现的。

对于读取两个数组并写入一个数组的特定操作,它受每个时钟周期两次读取的约束,因此每个时钟周期只能发出一个FMA。因此,它能做的最好的事情是每个时钟周期四倍。

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

但让我告诉你一个小秘密,英特尔不想让人们谈论太多。大多数操作都有内存带宽限制,无法从并行化中获得太多好处。这包括您问题中的操作。因此,尽管英特尔每隔几年就会推出一项新技术(例如AVX、FMA、AVX512,将内核数量增加一倍),每次都会将性能提高一倍,声称在实践中获得了摩尔定律,但平均收益是线性的,而不是指数的,这已经持续了几年。

相关内容

最新更新