小彭老师并行课

编译器优化与SIMD指令集

https://www.bilibili.com/video/BV12S4y1K721?spm_id_from=333.999.0.0

汇编语言

16位汇编,我的笔记:https://zhuanlan.zhihu.com/p/445370044

这里 YMM XMM 等都是浮点数寄存器, 比如 YMM/XMM0 就一共128位宽,可以塞下4个float或者2个double。那么他们在运算加法的时候就可以两个double一起来算,效率就更高了。因为高性能计算常用浮点数。

注:
指令集是为了增强CPU在某些方面(如多媒体)的功能而特意开发出的一组程序代码集合。
SSE(StreamingSIMD Extensions,单指令多数据留扩展)数据集:包含70条指令,其中有50条SIMD(单指令多数据技术)浮点运算指令,12条MMX整数运算增强指令,8条优化内存中连续数据块的传输指令。理论上这些指令对目前流行的图像处理、浮点处理、3D运算、视频处理、音频处理等多媒体应用起到全面强化的作用。
参考:https://blog.csdn.net/xz4385478/article/details/81201735



从上面这张图我们可以看到不同位宽之间的关系。64位寄存器 r 开头,和 32 位 e 开头的寄存器公用低32位;同理 ax 又和 eax 公用低16位。看图也知道 r15b r15w r15d r15 关系也差不多。

汇编语言大致分为两种,一种是 Intel 模式的汇编,它写寄存器就直接写eax,mov edx, eax就表示把 eax 赋值给 edx;而 AT&T 汇编则恰恰相反,是往右移动的;且表示长度也有所区分等等,详细见下图。GCC用的就是 AT&T 这一种。

我们的返回值是通过 eax 传出去的:

-fomit-frame-pointer 参数是让生成的汇编更简介;而 -fverbose-asm 则是让生成的汇编带有提示(比如图中的 main.cpp:2: )。编译器会自动设定寄存器优化,不需要我们去指定。

关于 lea 指令

https://www.zhihu.com/question/40720890/answer/110774673
lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数,例如:
lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。



上图想要线性访问,我们首先把 32 位的esi变成了64位的rsi(因为int的b是32位,而指针a是64位)。想要避免就可以用size_t,所以就更高效。并且当数组长度超过 INT_MAX 更需要用 size_t 了。推荐始终使用 size_t 表示数组大小和索引:

SIMD

对于浮点数,这里的addss的第二个 s 代表 single 即单精度浮点数。addss 就代表这两个数相加到 xmm0,而0是可以作为返回值传出的:



我们在上图来看 addps 和 addss 的区别。

省流助手: 如果你看到编译器生成的汇编里,有大量 ss 结尾的指令则说明矢量化失败;如果看到大多数都是 ps 结尾则说明矢量化成功。

编译器优化

代数化简

常量折叠

不是万能的



constexpr:强迫编译器在编译期求值

内联

关于 PLT:https://www.cnblogs.com/pannengzhi/p/2018-04-09-about-got-plt.html

如果看的到定义,在同一个文件里,则编译器可以优化:

指针

编译器宁愿慢也不愿意出错!

这里的 __restrict 是GCC特有的关键字,C语言里 restrict 是标准但是不在C++里:



注:参考Effective modern cpp P259
volatile 的用处是告诉编译器,正在处理的是特种内存(这种内存的位置实际上是用于与外部设备(如外部传感器、显示器、打印机和网络端口等)通信,而非用于读取或写入常规内存(即 RAM))。它的意思是通知编译器“不要对在此内存上的操作做任何优化”。
比如:

volatile int x;
x = 10;  // 不会被优化!
x = 20;

合并写入


矢量化

对齐


因为所有的 64 位电脑都支持 xmm,但是编译器不敢保证支持 ymm(AVX指令集)。所以只能保证 128 位的 xmm 是可以用的,但是不敢保证 256 位的 ymm 是可以用的。

如果还是想用 ymm 可以(gcc而言)用 -march=native 来检测:

因为编译器和标准库是绑定的,所以可以自动优化成对标准库的调用(同理还有 memcpy 之类的):


但是上图的缺点是,如果是 1023 而非 1024 的话,就可能覆盖掉别人的数据了。也就是必须是 4 的整数倍。

有什么解决的办法呢?边界特判法:先取出前面的4的倍数个数,再对最后3个数做特殊处理。



循环

调用次数多的地方称为热的地方,少的地方为冷的地方。所以循环的地方较热,更需要被编译器重点优化。


优化:会把热的代码尽量移到冷的地方去。

还有SIMD优化失败的例子,罪魁祸首就是调用了外部函数。解决方案是放在同一个文件里(即可以看到函数的定义,这样编译器看得到这个函数就可以选择内联了)。

结论:不管是编译器还是 CPU,都喜欢顺序的连续访问。

结构体

三个 float 没有矢量化成功。

结论:计算机喜欢 2 的整数幂,2, 4, 8, 16, 32, 64, 128... 结构体大小若不是 2 的整数幂,往往会导致 SIMD 优化失败。

技巧:添加一个辅助对齐的变量,对齐到 16 字节。


结构体的内存布局:AOS 与 SOA





测试结果:

STL



数学运算



数学函数请加 std:: 前缀!请勿用全局的数学函数,他们是 C 语言的遗产。始终用 std::sin, std::pow 等。

优化手法总结

  1. 函数尽量写在同一个文件内
  2. 避免在 for 循环内调用外部函数
  3. 非 const 指针加上 __restrict 修饰
  4. 试着用 SOA 取代 AOS
  5. 对齐到 16 或 64 字节
  6. 简单的代码,不要复杂化
  7. 试试看 #pragma omp simd
  8. 循环中不变的常量挪到外面来
  9. 对小循环体用 #pragma unroll
  10. -ffast-math 和 -march=native (GCC 编译器)

这是通用的:


这个貌似不是跨平台的,只能针对 GCC 开启这两个:

C++11开始的多线程编程

0. 时间

usleep 貌似是 Linux 特有的。
image

总之要精度高的话就用 steady_clock,参考:
https://en.cppreference.com/w/cpp/chrono
image

image
image
image
image

CMake Threads 包

image

image
image
参考:https://zh.cppreference.com/w/cpp/thread/jthread

image

数据结构

image
image
image
image
image
image

但是上面的情形,每次循环都要上锁和解锁,非常低效,可以用访问者模式:
image

posted @   hebh  阅读(145)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示