在较高的优化级别上,AVX2 simd相对于标量的性能相对较差

如何解决在较高的优化级别上,AVX2 simd相对于标量的性能相对较差

我正在学习和使用SIMD函数,并编写了一个简单的程序,该程序将其可以在 1秒中运行的向量加法指令的数量与正常的标量加法进行了比较。 我发现SIMD在较低的优化级别上表现相对较好,而在较高的优化级别上则始终表现较差,并且我想知道原因。我同时使用MSVC和gcc,这是同一回事。以下结果来自 Ryzen 7 CPU。我也在 Intel 平台上进行了测试,故事几乎相同。

#include <iostream>
#include <numeric>
#include <chrono>
#include <iterator>
#include <thread>
#include <atomic>
#include <vector>
#include <immintrin.h>
int main()
{
    const auto threadLimit = std::thread::hardware_concurrency() - 1; //for running main() 
    for (auto i = 1; i <= threadLimit; ++i)
    {
        std::cerr << "Testing " << i << " threads: ";
        std::atomic<unsigned long long> sumScalar {};
        std::atomic<unsigned long long> loopScalar {};
        std::atomic<unsigned long long> sumSimd {};
        std::atomic<unsigned long long> loopSimd {};
        std::atomic_bool stopFlag{ false };
        std::vector<std::thread> threads;
        threads.reserve(i);
        {
            for (auto j = 0; j < i; ++j)
                threads.emplace_back([&]
                    {
                        uint32_t local{};
                        uint32_t loop{};
                        while (!stopFlag)
                        {
                            ++local;
                            ++loop;  //removed this(see EDIT)
                        }
                        sumScalar += local;
                        loopScalar += loop;
                    });
            std::this_thread::sleep_for(std::chrono::seconds{ 1 });
            stopFlag = true;
            for (auto& thread : threads)
                thread.join();
        }
        threads.clear();
        stopFlag = false;
        {
            for (auto j = 0; j < i; ++j)
                threads.emplace_back([&]
                    {
                        const auto oneVec = _mm256_set1_epi32(1);
                        auto local = _mm256_set1_epi32(0);
                        uint32_t inc{};
                        while (!stopFlag)
                        {
                            local = _mm256_add_epi32(oneVec,local);
                            ++inc; //removed this(see EDIT)
                        }
                        sumSimd += std::accumulate(reinterpret_cast<uint32_t*>(&local),reinterpret_cast<uint32_t*>(&local) + 8,uint64_t{});
                        loopSimd += inc;
                    });
            std::this_thread::sleep_for(std::chrono::seconds{ 1 });
            stopFlag = true;
            for (auto& thread : threads)
                thread.join();
        }
        std::cout << "Sum: "<<sumSimd <<" / "<<sumScalar <<"("<<100.0*sumSimd/sumScalar<<"%)\t"<<"Loop: "<<loopSimd<<" / "<<loopScalar<<"("<< 100.0*loopSimd/loopScalar<<"%)\n";
    // SIMD/Scalar,higher value means SIMD better
    }
}

有了g++ -O0 -march=native -lpthread,我得到了:

Testing 1 threads: Sum: 1004405568 / 174344207(576.105%)        Loop: 125550696 / 174344207(72.0131%)
Testing 2 threads: Sum: 2001473960 / 348079929(575.004%)        Loop: 250184245 / 348079929(71.8755%)
Testing 3 threads: Sum: 2991335152 / 521830834(573.238%)        Loop: 373916894 / 521830834(71.6548%)
Testing 4 threads: Sum: 3892119680 / 693704725(561.063%)        Loop: 486514960 / 693704725(70.1329%)
Testing 5 threads: Sum: 4957263080 / 802362140(617.834%)        Loop: 619657885 / 802362140(77.2292%)
Testing 6 threads: Sum: 5417700112 / 953587414(568.139%)        Loop: 677212514 / 953587414(71.0174%)
Testing 7 threads: Sum: 6078496824 / 1067533241(569.396%)       Loop: 759812103 / 1067533241(71.1746%)
Testing 8 threads: Sum: 6679841000 / 1196224828(558.41%)        Loop: 834980125 / 1196224828(69.8013%)
Testing 9 threads: Sum: 7396623960 / 1308004474(565.489%)       Loop: 924577995 / 1308004474(70.6861%)
Testing 10 threads: Sum: 8158849904 / 1416026963(576.179%)      Loop: 1019856238 / 1416026963(72.0224%)
Testing 11 threads: Sum: 8868695984 / 1556964234(569.615%)      Loop: 1108586998 / 1556964234(71.2018%)
Testing 12 threads: Sum: 9441092968 / 1655554694(570.268%)      Loop: 1180136621 / 1655554694(71.2835%)
Testing 13 threads: Sum: 9530295080 / 1689916907(563.951%)      Loop: 1191286885 / 1689916907(70.4938%)
Testing 14 threads: Sum: 10444142536 / 1805583762(578.436%)     Loop: 1305517817 / 1805583762(72.3045%)
Testing 15 threads: Sum: 10834255144 / 1926575218(562.358%)     Loop: 1354281893 / 1926575218(70.2948%)

有了g++ -O3 -march=native -lpthread,我得到了:

Testing 1 threads: Sum: 2933270968 / 3112671000(94.2365%)       Loop: 366658871 / 3112671000(11.7796%)
Testing 2 threads: Sum: 5839842040 / 6177278029(94.5375%)       Loop: 729980255 / 6177278029(11.8172%)
Testing 3 threads: Sum: 8775103584 / 9219587924(95.1789%)       Loop: 1096887948 / 9219587924(11.8974%)
Testing 4 threads: Sum: 11350253944 / 10210948580(111.158%)     Loop: 1418781743 / 10210948580(13.8947%)
Testing 5 threads: Sum: 14487451488 / 14623220822(99.0715%)     Loop: 1810931436 / 14623220822(12.3839%)
Testing 6 threads: Sum: 17141556576 / 14437058094(118.733%)     Loop: 2142694572 / 14437058094(14.8416%)
Testing 7 threads: Sum: 19883362288 / 18313186637(108.574%)     Loop: 2485420286 / 18313186637(13.5718%)
Testing 8 threads: Sum: 22574437968 / 17115166001(131.897%)     Loop: 2821804746 / 17115166001(16.4872%)
Testing 9 threads: Sum: 25356792368 / 18332200070(138.318%)     Loop: 3169599046 / 18332200070(17.2898%)
Testing 10 threads: Sum: 28079398984 / 20747150935(135.341%)    Loop: 3509924873 / 20747150935(16.9176%)
Testing 11 threads: Sum: 30783433560 / 21801526415(141.199%)    Loop: 3847929195 / 21801526415(17.6498%)
Testing 12 threads: Sum: 33420443880 / 22794998080(146.613%)    Loop: 4177555485 / 22794998080(18.3266%)
Testing 13 threads: Sum: 35989535640 / 23596768252(152.519%)    Loop: 4498691955 / 23596768252(19.0649%)
Testing 14 threads: Sum: 38647578408 / 23796083111(162.412%)    Loop: 4830947301 / 23796083111(20.3014%)
Testing 15 threads: Sum: 41148330392 / 24252804239(169.664%)    Loop: 5143541299 / 24252804239(21.208%)

编辑:删除loop变量后,在两种情况下仅保留local(请参见代码编辑),结果仍然相同。

EDIT2:上面的结果是在Ubuntu上使用GCC 9.3。我在Windows(mingw)上切换到GCC 10.2,它显示了很好的缩放比例,如下所示(结果是原始代码)。几乎可以得出结论,这是MSVC和GCC旧版本的问题吗?

Testing 1 threads: Sum: 23752640416 / 3153263747(753.272%)      Loop: 2969080052 / 3153263747(94.159%)
Testing 2 threads: Sum: 46533874656 / 6012052456(774.01%)       Loop: 5816734332 / 6012052456(96.7512%)
Testing 3 threads: Sum: 66076900784 / 9260324764(713.548%)      Loop: 8259612598 / 9260324764(89.1936%)
Testing 4 threads: Sum: 92216030528 / 12229625883(754.038%)     Loop: 11527003816 / 12229625883(94.2548%)
Testing 5 threads: Sum: 111822357864 / 14439219677(774.435%)    Loop: 13977794733 / 14439219677(96.8044%)
Testing 6 threads: Sum: 122858189272 / 17693796489(694.357%)    Loop: 15357273659 / 17693796489(86.7947%)
Testing 7 threads: Sum: 148478021656 / 19618236169(756.837%)    Loop: 18559752707 / 19618236169(94.6046%)
Testing 8 threads: Sum: 156931719736 / 19770409566(793.771%)    Loop: 19616464967 / 19770409566(99.2213%)
Testing 9 threads: Sum: 143331726552 / 20753115024(690.652%)    Loop: 17916465819 / 20753115024(86.3315%)
Testing 10 threads: Sum: 143541178880 / 20331801415(705.993%)   Loop: 17942647360 / 20331801415(88.2492%)
Testing 11 threads: Sum: 160425817888 / 22209102603(722.343%)   Loop: 20053227236 / 22209102603(90.2928%)
Testing 12 threads: Sum: 157095281392 / 23178532051(677.762%)   Loop: 19636910174 / 23178532051(84.7202%)
Testing 13 threads: Sum: 156015224880 / 23818567634(655.015%)   Loop: 19501903110 / 23818567634(81.8769%)
Testing 14 threads: Sum: 145464754912 / 23950304389(607.361%)   Loop: 18183094364 / 23950304389(75.9201%)
Testing 15 threads: Sum: 149279587872 / 23585183977(632.938%)   Loop: 18659948484 / 23585183977(79.1172%)

解决方法

reinterpret_cast<uint32_t*>(&local)循环后,使GCC9可以在循环内local 存储/重新加载,从而创建存储转发瓶颈。 >

此问题已在GCC10中修复;不需要提交未优化的错误。请勿将指针投射到__m256i本机上;即使GCC经常使它工作,它也违反了严格混叠,因此it's Undefined Behaviour不带-fno-strict-aliasing。 (You can point __m256i* at any other type,but not vice versa。)

gcc9.3(正在使用)正在循环内存储/重新加载向量,但将标量保存在inc eax的寄存器中!

向量循环因此会限制向量​​存储转发加vpaddd的延迟,并且恰好比标量循环慢了8倍。他们的瓶颈无关紧要,接近1倍的总速度只是巧合。

(标量循环大概在Zen1或Skylake上以每次迭代1个周期运行,并且vpaddd的7个循环存储转发加1听起来是正确的。)


它是由reinterpret_cast<uint32_t*>(&local) 间接引起的,这可能是因为GCC试图宽恕严格混叠的未定义行为违规,或者只是因为您将指针指向本地完全没有。

这不是正常现象,也不是预期结果,但是内循环内部的原子负载和lambda的组合会混淆GCC9导致此错误。 (请注意,GCC9和10正在从循环内部的线程函数arg重新加载stopFlag地址,即使是标量,因此将其保存在寄存器中已经有些失败了。)>

在正常的用例中,每次检查停止标志都会进行更多的SIMD工作,并且通常不会在迭代中保持向量状态。通常,您会有一个非原子的arg来告诉您要做多少工作,而不是您在内部循环中检查的停止标志。因此,这个错漏的错误很少是问题。 (除非它即使没有原子标记也会发生?)


可重现的on Godbolt,显示了-DUB_TYPEPUN-UUB_TYPEPUN的对比,其中我使用#ifdef来使用您的不安全(和错选触发)版本与安全版本一种带有来自Fastest method to calculate sum of all packed 32-bit integers using AVX512 or AVX2的手动矢量随机播放的图片。 (该手动hsum在添加之前不会扩展,因此可能会溢出并包装。但这不是重点;在不严格的条件下使用不同的手动shuffle或_mm256_store_si256到单独的数组中,可能会得到所需的结果-混淆未定义的行为。)

标量循环为:

# g++9.3 -O3 -march=znver1
.L5:                                      # do{
        inc     eax                         # local++
.L3:
        mov     rdx,QWORD PTR [rdi+8]      # load the address of stopFlag from the lambda
        movzx   edx,BYTE PTR [rdx]         # zero-extend *&stopFlag into EDX
        test    dl,dl
        je      .L5                       # }while(stopFlag == 0)

使用您的-O3 -march=znver1(即我的源代码版本中的reinterpret_cast)和g ++ 9.3,-DUB_TYPEPUN,向量循环:

# g++9.3 -O3 -march=znver1  with your pointer-cast onto the vector

 # ... ymm1 = _mm256_set1_epi32(1)
.L10:                                               # do {
        vpaddd  ymm1,ymm0,YMMWORD PTR [rsp-32]       # memory-source add with set1(1)
        vmovdqa YMMWORD PTR [rsp-32],ymm1             # store back into stack memory
.L8:
        mov     rax,QWORD PTR [rdi+8]                  # load flag address
        movzx   eax,BYTE PTR [rax]                     # load stopFlag
        test    al,al
        je      .L10                                # }while(stopFlag == 0)

... auto-vectorized hsum,zero-extending elements to 64-bit for vpaddq

但是有了一个安全的__m256i水平和,根本没有指针指向local的情况下,local停留在寄存器中。

#      ymm1 = _mm256_set1_epi32(1)
.L9:
        vpaddd  ymm0,ymm1,ymm0             # local += set1(1),staying in a register,ymm0
.L8:
        mov     rax,QWORD PTR [rdi+8]       # same loop overhead,still 3 uops (with fusion of test/je)
        movzx   eax,BYTE PTR [rax]
        test    al,al
        je      .L9

... manually-vectorized 32-bit hsum

在我的Intel Skylake i7-6700k上,使用g ++ 10.1 -O3 -march = skylake,Arch GNU / Linux,energy_performance_preference = balance_power(最大时钟= 3.9),对于每个线程数量,我都能获得预期的800 +-1% GHz,且任何数量的核心都处于活动状态。

标量循环和向量循环具有相同的uops数量,并且没有不同的瓶颈,因此它们以相同的周期/迭代运行。 (4,如果它可以使那些地址->停车标志负载的值链在飞行中,则可能每个周期以1次迭代运行)。

Zen1可能会有所不同,因为vpaddd ymm是2 oups。但是它的前端足够宽,可能每次迭代仍以1个周期运行该循环,因此您可能还会看到800%。

在未注释++loop的情况下,我获得〜267%的“ SIMD速度”。在SIMD循环中增加一个inc,它会变成5微妙,并且可能会对Skylake产生一些讨厌的前端影响。


-O0基准测试通常是没有意义的,它具有不同的瓶颈(通常是将所有内容保存在内存中/从内存中重新加载),并且SIMD内部函数通常在-O0有很多额外的开销。尽管在这种情况下,甚至-O3都在SIMD循环的存储/重新加载上遇到了瓶颈。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-