gpt4 book ai didi

c++ - 使用 AVX2 在程序集 x86_64 中添加两个 vector 加上技术说明

转载 作者:塔克拉玛干 更新时间:2023-11-03 00:14:46 25 4
gpt4 key购买 nike

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

2
4
6
8

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

.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 个 double,所以我可以使用 CPU 的其余部分在每个周期执行另一条指令吗?例如,我可以用一个核心每个周期增加 5 个双倍吗? (4 与 SIMD + 1)
  • 如果我执行以下操作而不在我的汇编函数中放置循环怎么办?:
    #pragma openmp parallel forfor (size_t i = 0; i < reductions; i++)addVec(C + i, A + i, B + i)
    这是要 fork coreNumber + hyperThreading 线程,并且每个线程都对四个 double 进行 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]指令将负载和添加融合到一个融合的微操作中。当我在您的代码中使用该函数时,它会得到 2、4、6、8。

    您可以找到对两个数组求和的源代码 xy并写出到数组 zl1-memory-bandwidth-50-drop-in-efficiency-using-addresses-which-differ-by-4096 .这使用内在函数并展开八次。使用 gcc -S 反汇编代码或 objdump -d .另一个几乎做同样事情并用汇编编写的来源是 obtaining-peak-bandwidth-on-haswell-in-the-l1-cache-only-getting-62 .在文件 triad_fma_asm.asm换线 pi: dd 3.14159pi: 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 处理器每个时钟最多可以处理四个微操作。但是,使用两种称为微操作融合和宏操作融合的技术,可以使用 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 四个乘法和四个加法)、两个 256 加载、一个 256 位存储和一个 64 位加法和分支。在这个问题中, obtaining-peak-bandwidth-on-haswell-in-the-l1-cache-only-getting-62 ,我使用六个端口在一个时钟周期内(理论上)获得了五个微操作。但是,在 Haswell 上的实践中,这很难实现。

    对于读取两个数组并写入一个数组的特定操作,它受每个时钟周期两次读取的约束,因此每个时钟周期只能发出一个 FMA。所以它所能做的最好的事情是每个时钟周期四次 double 。
  • 如果您正确地并行化您的代码并且您的处理器有四个物理内核,那么您可以在一个时钟周期内实现 64 个双浮点运算(2FMA*4 个内核)。这对于某些操作来说是理论上最好的,但对于您问题中的操作来说却不是。

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

    关于c++ - 使用 AVX2 在程序集 x86_64 中添加两个 vector 加上技术说明,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26820662/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com