BruceChen7 / gitblog

My blog
6 stars 1 forks source link

CPU Cache和Cache更新套路 #29

Open BruceChen7 opened 3 years ago

BruceChen7 commented 3 years ago

Reading

出现的原因

十年CPU的频率不断提升,而受制于制造工艺和成本限制,目前计算机的内存主要是DRAM并且在访问速度上没有质的突破。因此,CPU的处理速度和内存的访问速度差距越来越大,甚至可以达到上万倍。

多级cache

随着科技发展,热点数据的体积越来越大,单纯的增加一级缓存大小的性价比已经很低了。因此,就慢慢出现了在一级缓存(L1 Cache)和内存之间又增加一层访问速度和成本都介于两者之间的二级缓存(L2 Cache)。此外,又由于程序指令和程序数据的行为和热点分布差异很大,因此L1 Cache也被划分成L1i(i for instruction)和L1d(d for data)两种专门用途的缓存

什么是Cache Line

Cache Line可以简单的理解为CPU Cache中的最小缓存单位。目前主流的CPU Cache的Cache Line大小都是64Bytes。假设我们有一个512字节的一级缓存,那么按照64B的缓存单位大小来算,这个一级缓存所能存放的缓存个数就是512/64 = 8个。具体参见下图。

还有另一种理解:

The minimum amount of cache which can be loaded or stored to memory

image

缓存关联性

缓存设计的一个关键决定是确保每个主存块(chunk)能够存储在任何一个缓存槽里,或者只是其中一些(译者注:此处一个槽位就是一个缓存行)。有三种方式将缓存槽映射到主存块中:

一般输入的是N路关联的缓存。但是同样面对一个问题,如何获取对应内存的缓存行。

缓存更新的套路

有一种方案是先更新缓存,然后在更新数据库,这种方式是错误的。两个并发操作,一个更新,一个查询,更新操作先删除缓存,查询操作没有命中缓存,而是将老数据cache,然后更新操作更新了数据库,这样导致了数据库中的数据和缓存中的不一致,而且是长时间的不一致

Cache Aside Pattern

还是上面的问题:首先,没有了删除cache数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。为什么不是写完数据库后更新缓存?主要是怕两个并发的写操作导致脏数据(缓存的数据和数据库的数据长时间不一致)。其实,Cache Aside还是有并发问题的

比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据

但,这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大

Read/Write Through Pattern

与Cache Aside相比,可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache,下面的图中,lower memory可以理解磁盘,相当于内存是磁盘的缓存的逻辑。

Pasted image 20201123153456

Write Behind Caching Pattern 类似于linux中的page-cache的套路

伪共享导致的cache false-sharing

// 顺序访问
func BenchmarkMatrixCombination(b *testing.B) {
    matrixA := createMatrix(matrixLength)
    matrixB := createMatrix(matrixLength)

    for n := 0; n < b.N; n++ {
        for i := 0; i < matrixLength; i++ {
            for j := 0; j < matrixLength; j++ {
                matrixA[i][j] = matrixA[i][j] + matrixB[i][j]
            }
        }
    }
}

// 倒叙访问
// 性能大幅下降
func BenchmarkMatrixReversedCombination(b *testing.B) {
    matrixA := createMatrix(matrixLength)
    matrixB := createMatrix(matrixLength)

    for n := 0; n < b.N; n++ {
        for i := 0; i < matrixLength; i++ {
            for j := 0; j < matrixLength; j++ {
                matrixA[i][j] = matrixA[i][j] + matrixB[j][i]
            }
        }
    }
}
// 利用block来建立L1 cache
func BenchmarkMatrixReversedCombinationPerBlock(b *testing.B) {
    matrixA := createMatrix(matrixLength)
    matrixB := createMatrix(matrixLength)
    blockSize := 8

    for n := 0; n < b.N; n++ {
        for i := 0; i < matrixLength; i += blockSize {
            for j := 0; j < matrixLength; j += blockSize {
                for ii := i; ii < i+blockSize; ii++ {
                    for jj := j; jj < j+blockSize; jj++ {
                        matrixA[ii][jj] = matrixA[ii][jj] + matrixB[jj][ii]
                    }
                }
            }
        }
    }
}

// false-sharing
// https://taohuawu.club/cpu-caches-theory-and-application
type SimpleStruct struct {
    n int
}

// 两个变量靠的很近,位于两个核同一个缓存行中,那么一个go routine的改变var1, 那么另一个核上的缓存行会失效,另一个core读取var2的时候,看到var2失效了,会通过MESI协议获取新的缓存行。
func BenchmarkStructureFalseSharing(b *testing.B) {
    structA := SimpleStruct{}
    structB := SimpleStruct{}
    wg := sync.WaitGroup{}

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        wg.Add(2)
        go func() {
            for j := 0; j < M; j++ {
                structA.n += j
            }
            wg.Done()
        }()
        go func() {
            for j := 0; j < M; j++ {
                structB.n += j
            }
            wg.Done()
        }()
        wg.Wait()
    }
}

Cache分布

image

各级cache的访问

image