C++ 优化内存读取速度

如何解决C++ 优化内存读取速度

我正在创建一个具有 1024 * 1024 * 1024 个元素的 int(32 位)向量,如下所示:

std::vector<int> nums;
for (size_t i = 0; i < 1024 * 1024 * 1024; i++) {
   nums.push_back(rand() % 1024);
}

此时保存了 4 GB 的随机数据。然后我只是像这样总结向量中的所有元素:

uint64_t total = 0;
for (auto cn = nums.begin(); cn < nums.end(); cn++) {
   total += *cn;
}

这大约需要 0.18 秒,这意味着数据的处理速度约为 22.2 GB/s。我在内存带宽高得多的 M1 上运行它,大约 60GB/s。有没有办法让上面的代码在单核上运行得更快?

编辑: 手动 SIMD 版本:

int32x4_t simd_total = vmovq_n_s32(0); 
for (auto cn = nums.begin(); cn < nums.end()-3; cn +=4) { 
    const int32_t v[4] = {cn[0],cn[1],cn[2],cn[3]} 
    simd_total = vaddq_s32(simd_total,vld1q_s32(v)); 
} 
return vaddvq_s32(simd_total); 

SIMD 版本与非手动 SIMD 版本具有相同的性能。

编辑 2: 好的,所以我将向量元素更改为 uint32_t,并将结果类型更改为 uint32_t(如@Peter Cordes 所建议的):

uint32_t sum_ints_32(const std::vector<uint32_t>& nums) {
    uint32_t total = 0;
    for (auto cn = nums.begin(); cn < nums.end(); cn++) {
        total += *cn;
    }
    return total;
}

这运行得更快(~45 GB/s)。这是拆解:

0000000100002218 <__Z11sum_ints_32RKNSt3__16vectorIjNS_9allocatorIjEEEE>:
   100002218:   a940200c    ldp x12,x8,[x0]
   10000221c:   eb08019f    cmp x12,x8
   100002220:   54000102    b.cs    100002240 <__Z11sum_ints_32RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x28>  // b.hs,b.nlast
   100002224:   aa2c03e9    mvn x9,x12
   100002228:   8b090109    add x9,x9
   10000222c:   f1006d3f    cmp x9,#0x1b
   100002230:   540000c8    b.hi    100002248 <__Z11sum_ints_32RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x30>  // b.pmore
   100002234:   52800000    mov w0,#0x0                    // #0
   100002238:   aa0c03e9    mov x9,x12
   10000223c:   14000016    b   100002294 <__Z11sum_ints_32RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x7c>
   100002240:   52800000    mov w0,#0x0                    // #0
   100002244:   d65f03c0    ret
   100002248:   d342fd29    lsr x9,x9,#2
   10000224c:   9100052a    add x10,#0x1
   100002250:   927ded4b    and x11,x10,#0x7ffffffffffffff8
   100002254:   8b0b0989    add x9,x12,x11,lsl #2
   100002258:   9100418c    add x12,#0x10
   10000225c:   6f00e400    movi    v0.2d,#0x0
   100002260:   aa0b03ed    mov x13,x11
   100002264:   6f00e401    movi    v1.2d,#0x0
   100002268:   ad7f8d82    ldp q2,q3,[x12,#-16]
   10000226c:   4ea08440    add v0.4s,v2.4s,v0.4s
   100002270:   4ea18461    add v1.4s,v3.4s,v1.4s
   100002274:   9100818c    add x12,#0x20
   100002278:   f10021ad    subs    x13,x13,#0x8
   10000227c:   54ffff61    b.ne    100002268 <__Z11sum_ints_32RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x50>  // b.any
   100002280:   4ea08420    add v0.4s,v1.4s,v0.4s
   100002284:   4eb1b800    addv    s0,v0.4s
   100002288:   1e260000    fmov    w0,s0
   10000228c:   eb0b015f    cmp x10,x11
   100002290:   540000a0    b.eq    1000022a4 <__Z11sum_ints_32RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x8c>  // b.none
   100002294:   b840452a    ldr w10,[x9],#4
   100002298:   0b000140    add w0,w10,w0
   10000229c:   eb08013f    cmp x9,x8
   1000022a0:   54ffffa3    b.cc    100002294 <__Z11sum_ints_32RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x7c>  // b.lo,b.ul,b.last
   1000022a4:   d65f03c0    ret

我还重写了 Manual-SIMD 版本:

uint32_t sum_ints_simd_2(const std::vector<uint32_t>& nums) {
    uint32x4_t  simd_total = vmovq_n_u32(0);
    for (auto cn = nums.begin(); cn < nums.end()-3; cn +=4) {
        const uint32_t v[4] = { cn[0],cn[3] };
        simd_total = vaddq_u32(simd_total,vld1q_u32(v));
    }
    return vaddvq_u32(simd_total);
}

它的运行速度仍然比非手动 SIMD 版本慢 2 倍,并导致以下反汇编:

0000000100002464 <__Z15sum_ints_simd_2RKNSt3__16vectorIjNS_9allocatorIjEEEE>:
   100002464:   a9402408    ldp x8,[x0]
   100002468:   d1003129    sub x9,#0xc
   10000246c:   6f00e400    movi    v0.2d,#0x0
   100002470:   eb09011f    cmp x8,x9
   100002474:   540000c2    b.cs    10000248c <__Z15sum_ints_simd_2RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x28>  // b.hs,b.nlast
   100002478:   6f00e400    movi    v0.2d,#0x0
   10000247c:   3cc10501    ldr q1,[x8],#16
   100002480:   4ea08420    add v0.4s,v0.4s
   100002484:   eb09011f    cmp x8,x9
   100002488:   54ffffa3    b.cc    10000247c <__Z15sum_ints_simd_2RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x18>  // b.lo,b.last
   10000248c:   4eb1b800    addv    s0,v0.4s
   100002490:   1e260000    fmov    w0,s0
   100002494:   d65f03c0    ret

为了达到与自动矢量化版本相同的速度,我们可以在手动 SIMD 版本中使用 uint32x4x2 而不是 uint32x4:

uint32_t sum_ints_simd_3(const std::vector<uint32_t>& nums) {
    uint32x4x2_t simd_total;
    simd_total.val[0] = vmovq_n_u32(0);
    simd_total.val[1] = vmovq_n_u32(0);
    for (auto cn = nums.begin(); cn < nums.end()-7; cn +=8) {
        const uint32_t v[4] = { cn[0],cn[3] };
        const uint32_t v2[4] = { cn[4],cn[5],cn[6],cn[7] };
        simd_total.val[0] = vaddq_u32(simd_total.val[0],vld1q_u32(v));
        simd_total.val[1] = vaddq_u32(simd_total.val[1],vld1q_u32(v2));
    }
    return vaddvq_u32(simd_total.val[0]) + vaddvq_u32(simd_total.val[1]);
}

为了获得更快的速度,我们可以利用 uint32x4x4(大约 53 GB/s):

uint32_t sum_ints_simd_4(const std::vector<uint32_t>& nums) {
    uint32x4x4_t simd_total;
    simd_total.val[0] = vmovq_n_u32(0);
    simd_total.val[1] = vmovq_n_u32(0);
    simd_total.val[2] = vmovq_n_u32(0);
    simd_total.val[3] = vmovq_n_u32(0);
    for (auto cn = nums.begin(); cn < nums.end()-15; cn +=16) {
        const uint32_t v[4] = { cn[0],cn[7] };
        const uint32_t v3[4] = { cn[8],cn[9],cn[10],cn[11] };
        const uint32_t v4[4] = { cn[12],cn[13],cn[14],cn[15] };
        simd_total.val[0] = vaddq_u32(simd_total.val[0],vld1q_u32(v2));
        simd_total.val[2] = vaddq_u32(simd_total.val[2],vld1q_u32(v3));
        simd_total.val[3] = vaddq_u32(simd_total.val[3],vld1q_u32(v4));
    }
    return vaddvq_u32(simd_total.val[0])
        + vaddvq_u32(simd_total.val[1])
        + vaddvq_u32(simd_total.val[2])
        + vaddvq_u32(simd_total.val[3]);
}

这让我们得到以下反汇编:

0000000100005e34 <__Z15sum_ints_simd_4RKNSt3__16vectorIjNS_9allocatorIjEEEE>:
   100005e34:   a9402408    ldp x8,[x0]
   100005e38:   d100f129    sub x9,#0x3c
   100005e3c:   6f00e403    movi    v3.2d,#0x0
   100005e40:   6f00e402    movi    v2.2d,#0x0
   100005e44:   6f00e401    movi    v1.2d,#0x0
   100005e48:   6f00e400    movi    v0.2d,#0x0
   100005e4c:   eb09011f    cmp x8,x9
   100005e50:   540001c2    b.cs    100005e88 <__Z15sum_ints_simd_4RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x54>  // b.hs,b.nlast
   100005e54:   6f00e400    movi    v0.2d,#0x0
   100005e58:   6f00e401    movi    v1.2d,#0x0
   100005e5c:   6f00e402    movi    v2.2d,#0x0
   100005e60:   6f00e403    movi    v3.2d,#0x0
   100005e64:   ad401504    ldp q4,q5,[x8]
   100005e68:   ad411d06    ldp q6,q7,[x8,#32]
   100005e6c:   4ea38483    add v3.4s,v4.4s,v3.4s
   100005e70:   4ea284a2    add v2.4s,v5.4s,v2.4s
   100005e74:   4ea184c1    add v1.4s,v6.4s,v1.4s
   100005e78:   4ea084e0    add v0.4s,v7.4s,v0.4s
   100005e7c:   91010108    add x8,#0x40
   100005e80:   eb09011f    cmp x8,x9
   100005e84:   54ffff03    b.cc    100005e64 <__Z15sum_ints_simd_4RKNSt3__16vectorIjNS_9allocatorIjEEEE+0x30>  // b.lo,b.last
   100005e88:   4eb1b863    addv    s3,v3.4s
   100005e8c:   1e260068    fmov    w8,s3
   100005e90:   4eb1b842    addv    s2,v2.4s
   100005e94:   1e260049    fmov    w9,s2
   100005e98:   0b080128    add w8,w9,w8
   100005e9c:   4eb1b821    addv    s1,v1.4s
   100005ea0:   1e260029    fmov    w9,s1
   100005ea4:   0b090108    add w8,w8,w9
   100005ea8:   4eb1b800    addv    s0,v0.4s
   100005eac:   1e260009    fmov    w9,s0
   100005eb0:   0b090100    add w0,w9
   100005eb4:   d65f03c0    ret

疯狂的东西

解决方法

-march=native 有帮助吗? IDK 如果有任何 SIMD 功能 Apple clang 不会在第一代 AArch64 MacOS CPU 上利用,但 clang 可能只是一般地采用基线 AArch64。

如果使用 uint32_t 求和,是否可以更快,以便编译器在添加之前不必加宽每个元素?这意味着每条 SIMD 指令只能处理相同大小的累加器一半的内存数据。

https://godbolt.org/z/7c19913jE 表明 Thomas Matthews 的展开建议确实让 clang11 -O3 -march=apple-a13 展开它制作的 SIMD 向量化 asm 循环。源更改通常不是胜利,例如 对于 x86-64 clang -O3 -march=haswell 来说更糟糕,但它在这里确实有帮助。


另一种可能性是单个内核无法使内存带宽饱和。但发布的基准测试结果 by Anandtech for example 似乎排除了这一点:他们发现即使是单个内核也可以达到 59GB/s,尽管这可能运行了优化 memcpy 函数。

(他们说单个 Firestorm 内核几乎可以使内存控制器饱和这一事实令人震惊,这是我们以前在设计中从未见过的。这听起来有点奇怪;台式机/笔记本电脑英特尔CPU 非常接近,unlike their "server" chips。也许不像 Apple 那样

与现代 x86 相比,M1 具有相当低的内存延迟,因此这可能有助于单核能够跟踪传入的负载,以保持必要的延迟 x 带宽乘积,即使其内存带宽很高。

,

这里有一些技巧。

循环展开

uint64_t total = 0;
for (auto cn = nums.begin(); cn < nums.end(); cn += 4)
{
    total += cn[0];
    total += cn[1];
    total += cn[2];
    total += cn[3];
}

注册预取

uint64_t total = 0;
for (auto cn = nums.begin(); cn < nums.end(); cn += 4)
{
    const uint64 n0 = cn[0];
    const uint64 n1 = cn[1];
    const uint64 n2 = cn[2];
    const uint64 n3 = cn[3];
    total += n0;
    total += n1;
    total += n2;
    total += n3;
}

您应该在高优化级别为每一个打印汇编语言并比较它们。

此外,您的处理器可能有一些您可以使用的专用指令。例如,ARM 处理器可以通过一条指令从内存中加载多个寄存器。

另外,查找 SIMD 指令或在互联网上搜索“C++ SIMD 读取内存”。

我与编译器(在嵌入式系统上)争论过,发现编译器的优化策略可能优于或等于指令专业化或其他技术(使用测试点和示波器执行计时)。

您必须记住,与具有多核的系统或专用(嵌入式)系统相比,您在单核机器上的任务很可能会更频繁地被替换。

,

考虑尽可能多地预先计算并使用内置 STL 函数,这将在尝试 SIMD 或汇编方法之前产生尽可能多的最佳代码。如果它仍然太慢,请尝试 SIMD/汇编版本:

避免在未预留的 push_back 上调用 std::vector:这会导致系统在达到容量限制时分配更多空间。由于您事先知道数组的大小,因此请提前预留空间:(对于非内置类型,也可以考虑 emplace_back)。

此外,STL 函数可以将样板代码减少到两个函数调用。

另外,avoid rand().

const std::size_t GB = 1024 * 1024 * 1024;
std::vector<int> nums(4 * GB);
std::generate(std::begin(nums),std::end(nums),[](){ return rand() % 1024; });

//...

const auto sum = std::accumulate(std::begin(nums),0);

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

相关推荐


使用本地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-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res