#语句
#减少memset使用
对于一个C语言结构体Msg,初始化有以下方式:
-
Msg m = {0};
-
memset_s 非常耗时
-
复合字面量 可由编译器优化成更好的初始化形式
1 2 3 4#define INI_STRUCTRUE(sPtr) do{ \ static const __typeof__(*(sPtr)) tmp = {0}; \ *(sPtr) = tmp; \ } while(0)- 大对象优化成memset
1 2# 跳转memset代码执行 bl memset - 对212字节以内小对象优化成每次64位写,对内存分块写入
1 2 3 4 5# stp是ARM架构下的存储指令,表示“存储配对”,将两个8字节的值存储到内存 # xzr是ARM中的零寄存器,表示常量值0 stp xzr, xzr, [x1] stp xzr, xzr, [x1, #16] ...
- 大对象优化成memset
-
对于大结构体,可采用**数据标志位****的方式或字符串结束符标识方法:
1 2 3 4struct Msg{ ... bool msgIsValid; };
#消除重复计算
使用局部变量存储表达式或者函数结果,特别是循环的条件。
|
|
|
|
|
|
#满足精度条件下,整数运算代替浮点运算
|
|
#表达式
#利用短路,简单判断放前面
&&的短路:
|
|
||的短路:
|
|
#控制语句
#优先高命中分支
编译器对高概率指令进行联排,有利于指令cache的命中率,对cache友好。
|
|
编译器认为==的概率低,所以
|
|
#合理运用分支预测
|
|
这样写后==的放在正常指令后,人为告诉编译器这是高概率分支,CPU提前装载高概率的指令。
#switch替代if...else...
在分支少的时候,他们的效率相同。在分支多的时候(gcc规定为18个分支)switch会被优化成跳转表,前提是switch的case是连续的。
- 1,2,3 √
- 2,5,7 ×
建议if...else...超过3个分支使用switch替代,代码更加clean.
#合理的循环体变量
使用unsigned时要注意是否会因为溢出造成死循环。
|
|
unsigned char和unsigned short会多一个取低位和前置归零操作:
|
|
其汇编指令可能为:
|
|
因为U8和U16会被扩展成U32,这样编译出来的指令会多出and指令,建议改为:
|
|
#多层循环应外小内大,行优先
只有内外次数差很多的时候才考虑这个优化
一个循环至少包括三条指令:
- 自增:递增循环变量。
1 2# i++ add w19, w19, 1 - 比较:检查循环变量是否满足退出条件。
1 2# i < imm w19, imm - 跳转:如果未满足条件,跳转到循环开始的地方继续执行。
1bne .L2
外层循环1000次,内层循环10次,总指令数:
- 外层循环的指令数:
1000 * 3 = 3000 - 内层循环的指令数:
10 * 3 * 1000 = 30000
外层循环10次,内层循环1000次,总指令数:
- 外层循环的指令数:
10 * 3 = 30 - 内层循环的指令数:
1000 * 3 * 10 = 30000
两种循环方式差了至少2970条指令
- 当内外循环次数差距大的时候,外层放小循环,内层放大循环
- 确保内层循环遍历的是内存中连续存放的数据,cache友好
#循环合并
|
|
一个循环至少3条指令,那么第一种情况:
- 一个循环体:
3 * n - 一共两个循环体:
2 * 3 * n
第二种改进方法:
3 * n
这个优化需权衡:
- 如果
A和B都是很简单的操作,小指令,循环合并会带来更大的性能提升。 - 操作独立且可并行的情况下:
A和B之间没有依赖关系,能够同时执行。 - 同时可能导致循环体超大,降低cache命中率等
#循环展开
通过减少循环控制开销(如比较和跳转),它通常在编译器无法自动进行优化时,手动展开循环能够显著提升性能。
|
|
展开后:
|
|
- 减少控制指令:减少每次迭代中的比较和跳转开销,从而提升性能。
- 提高流水线效率:当多次操作并行执行时,CPU 更容易在流水线中安排指令,从而提高吞吐量。
- 更好的缓存局部性:当每次迭代处理更多数据时,CPU 更容易利用数据缓存来减少内存访问延迟。
展开因子是指每次迭代中展开的次数,通常选择为 2、4、8 等。
- 因子过小优化效果可能不明显,没有显著减少循环控制指令
- 过大可能导致循环体太大,降低指令cache命中
#数据结构
#数据对齐自然边界
若为跨CPU/模块的消息数据结构定义,需采用紧凑排列,同时尽量让被访问数据对齐到自然边界。
|
|
- arr数组从偏移2开始,每个int元素都未4字节对齐
- 在x86架构会有性能惩罚,在RISC架构可能直接崩溃
优化为:
|
|
#临近定义同时访问的数据结构
|
|
这样每次访问key和value时,间隙了0x90的长度,都会导致D-Cache缺失,需要从内存中重新装载,效率低下。
|
|
使得每次访问的K-V都是连续存放的,提高了cache命中率。
#避免对整个字符串清零(同为减少memset调用)
|
|
#函数
#避免过多参数
不同的CPU传参方式有差异,在ARM64架构中,参数通过寄存器X0-X7传递,超过8个参数则会将超过的参数入栈
|
|
在调用到参数的时候还需要从栈中取出:
|
|
#复杂类型避免值传递
值传递会有调用memset进行拷贝的指令:
|
|
#合理使用inline或宏替代短函数
内联和宏都cache友好,函数则需要bl到另一个指令块
热点函数使用inline,兼具可调试,类型检测,安全性更高
#避免冗余合法性检查
信任边界定义:
- 进程与进程之间的消息通信
- 跨so的API接口
- 应用程序与OS内核
#内存
#CPU一致性与伪共享
多个CPU对某块内存同时读写,一个CPU写了Cache,则与内存中的数据不一致,需更新:
- 直写模式 Write through 写Cache时同时写内存
- 写回模式 Write back 写Cache时标记dirty,只有Cache需刷新了才写回内存
L3缓存和内存是多处理器共享的,多核多处理器的一致性方法:总线监听
写Cache时广播到总线,其他CPU嗅探到总线后,把自己的Cache副本标记为无效。
- 由于这个机制,多个线程就无法读写相同的Cache line,从而导致访存效率降低,称为伪共享
- 两个核操作两个不同的data,但在同一个Cache line中,即使data不同,但会导致另一个核的Cache失效
#位操作
位操作具有天然的并行性。CPU寄存器可以看作是32bits或64bits的集合,每次计算都可以看作是同时操作了这么多的位。
使用bitops.h库,会比自己写位操作好
#C++
#引用传参
不需要进行拷贝操作
#避免临时对象
构造和析构函数调用有开销
#unique_ptr替代shared_ptr
前者内存使用无任何开销,性能也与裸指针非常接近 后者
- 引用计数,控制块需要额外内存
- 控制块计数器为原子变量,增减比普通变量慢
#使用emplace函数
#容器遍历采用前置++--
普通循环变量前置后置加减没有区别
但迭代器有区别
|
|