小彭老师并行课
编译器优化与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 等。
优化手法总结
- 函数尽量写在同一个文件内
- 避免在 for 循环内调用外部函数
- 非 const 指针加上 __restrict 修饰
- 试着用 SOA 取代 AOS
- 对齐到 16 或 64 字节
- 简单的代码,不要复杂化
- 试试看 #pragma omp simd
- 循环中不变的常量挪到外面来
- 对小循环体用 #pragma unroll
- -ffast-math 和 -march=native (GCC 编译器)
这是通用的:
这个貌似不是跨平台的,只能针对 GCC 开启这两个:
C++11开始的多线程编程
0. 时间
usleep 貌似是 Linux 特有的。
总之要精度高的话就用 steady_clock,参考:
https://en.cppreference.com/w/cpp/chrono
CMake Threads 包
参考:https://zh.cppreference.com/w/cpp/thread/jthread
数据结构
但是上面的情形,每次循环都要上锁和解锁,非常低效,可以用访问者模式:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)