每个程序员都应该了解的内存知识(五): 代码优化

代码优化

多线程优化

尽量使用顺序读写

因为分支预测的关系, 顺序读写通常能够带来更好的性能.

共享变量

  1. 将只读变量和读写变量分离

    有可能因为缓存行的原因导致读写变量的更新影响到读变量, 进而影响了运行速度

  2. 提升数据的局部性, 将一起使用的读写变量分组到一个结构中

  3. 缓存行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() // 垃圾回收,仅为了在程序结束前清理资源
    }
    
    

    通过填充, 是个某个结构体能够填充一整个缓存行, 而不会影响其他的变量

  4. 如果一个变量被多个线程使用,但每次使用都是独立的,copy It

原子性优化

相比较普通的操作, 原子性操作要更慢, 如果在已经存在并发保护的情况下, 考虑不要使用原子操作

LL/SC

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问题。

posted @ 2024-03-29 20:00  pDJJq  阅读(9)  评论(0编辑  收藏  举报