草庐IT

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

coder 2024-02-10 原文

我在这里做错了什么?我得到 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/

    有关c++ - 使用 AVX2 在程序集 x86_64 中添加两个 vector 加上技术说明的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

      我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

    3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

      很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

    5. ruby - 在 Ruby 中使用匿名模块 - 2

      假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

    6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

      我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

    7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

    8. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

      我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

    9. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

      我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

    10. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

      我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

    随机推荐