每个程序员都应该了解的内存知识(五): 代码优化
代码优化
多线程优化
尽量使用顺序读写
因为分支预测的关系, 顺序读写通常能够带来更好的性能.
共享变量
-
将只读变量和读写变量分离
有可能因为缓存行的原因导致读写变量的更新影响到读变量, 进而影响了运行速度
-
提升数据的局部性, 将一起使用的读写变量分组到一个结构中
-
缓存行Padding (谨慎使用)
package main import ( "sync" "runtime" ) type NoPad struct { a int64 b int64 } type Pad struct { a int64 _ [56]byte // 填充56字节,与int64加起来正好是64字节,即一个缓存行的大小 b int64 } func main() { // 创建没有填充的结构体 np := NoPad{0, 0} // 创建有填充的结构体 p := Pad{0, [56]byte{}, 0} var wg sync.WaitGroup wg.Add(2) // 更新NoPad结构体的两个字段 go func() { for i := 0; i < 1000000; i++ { np.a = int64(i) } wg.Done() }() go func() { for i := 0; i < 1000000; i++ { np.b = int64(i) } wg.Done() }() // 更新Pad结构体的两个字段 go func() { for i := 0; i < 1000000; i++ { p.a = int64(i) } wg.Done() }() go func() { for i := 0; i < 1000000; i++ { p.b = int64(i) } wg.Done() }() wg.Wait() runtime.GC() // 垃圾回收,仅为了在程序结束前清理资源 }
通过填充, 是个某个结构体能够填充一整个缓存行, 而不会影响其他的变量
-
如果一个变量被多个线程使用,但每次使用都是独立的,copy It
原子性优化
相比较普通的操作, 原子性操作要更慢, 如果在已经存在并发保护的情况下, 考虑不要使用原子操作
LL/SC
Load Link (LL)操作
LL操作会读取一个共享变量的值,并建立一个监视(monitor)或者链接(link)。这个链接会在相对应的Store Conditional(SC)操作之前保持活跃状态。这意味着,如果另一个处理器在LL和SC之间修改了该变量,SC操作就会失败。
Store Conditional (SC)操作
SC操作尝试写入一个新值到之前LL操作读取的那个共享变量。如果自LL操作以来该变量没有被其他处理器修改,那么SC将成功,否则它将失败。在失败的情况下,通常重试整个LL/SC序列。
LL/SC可以用来实现各种原子操作,如原子的加法、减法、比较和交换等。这些操作是构建更高级同步构造的基石,如锁、信号量和其他并发控制机制。
CAS
这是一个三进制操作,只有当当前值与第三参数值相同时,才将作为参数提供的值写入地址(第二参数)
这里稍微注意下ABA问题:
ABA问题发生在一个线程在CAS操作中读取了内存位置的值A,并且准备更新这个位置到某个新值B。如果在这个线程完成更新之前,另一个线程将该位置的值从A改为B,然后又改回A,这时原来的线程执行CAS操作时会发现该位置的值仍然是A,因此它会错误地认为这个值没有被其他线程更改过,从而成功地将值更新为B。
为了解决ABA问题,一种常见的方法是使用版本号或时间戳。每次变量更新时,除了变量的值,还会更新一个额外的版本号或时间戳。这样,即便变量的值被改回原值,版本号也会不同,从而CAS操作能够检测到中间的状态变化。在现代的处理器和编程语言中,通常会提供一些特殊的数据类型或原子操作,如原子引用计数(atomic reference counting)或者原子标记指针(atomic marked pointers),来帮助解决ABA问题。