内存和cache交换的最小单位是cacheline。比如cacheline 64字节,每一次缓存数据的单位都是以一个 CacheLine 64 字节为单位进行存储的。假如说要查询的数据在 L1 中不存在,那么 CPU 的做法是一次性从 L2 中把要访问的数据及其后面的 64 个字节全部缓存进来。假如下一次再执行的时候要访问的指令在上一次已经在 L1 中存在了,那么就直接访问 L1,就不必再从 L2 来读取了,通过一个代码示例来分析:
- #include<stdio.h>
- #include<stdlib.h>
- #include<string.h>
-
- int a[10000][10000];
-
- void bad_cache() {
- int i = 0,j =0;
- for(i = 0; i < 10000; i++) {
- for(j = 0; j< 10000; j++) {
- a[j][i] = 1;
- }
- }
- }
-
- void good_cache() {
- int i = 0,j =0;
- for(i = 0; i < 10000; i++) {
- for(j = 0; j< 10000; j++) {
- a[i][j] = 1;
- }
- }
- }
调用good_cache的性能数据:
- #simpleperf stat -e cache-references,cache-misses cache
-
- Performance counter statistics:
-
- 1,788,304,622 cache-references # 665.594 M/sec (100%)
- 7,106,477 cache-misses # 0.397386% miss rate (100%)
-
- Total test time: 2.686779 seconds.
可以看到共花费2.68s,cache-misses比例很低,而bad_cache的性能数据:
- #simpleperf stat -e cache-references,cache-misses cache
-
-
- Performance counter statistics:
-
- 1,611,937,854 cache-references # 126.103 M/sec (100%)
- 171,774,619 cache-misses # 10.656405% miss rate (100%)
-
- Total test time: 12.782758 seconds.
由于cache-misses率过高导致时间花费了12.78s时间。
多处理器架构下,多线程并行写入同一内存位置,由于缓存一致性问题会导致性能问题,这种现象称为cache伪共享。
2.1 结构体对齐cache line
内核代码中经常看到某个结构体对齐到cache line size。如果有很多结构体的数组,结构体内存对齐将有助于性能提升。
示例代码:
- #define ____cacheline_aligned __attribute__((__aligned__(64)))
- #define LOOP 10000 * 10000
- struct data {
- int32_t x;
- }/*____cacheline_aligned*/;
-
- typedef struct data Data;
-
- Data dArray[2];
-
- void f1() {
- int64_t i = 0;
- for(i = 0; i < LOOP; i++) {
- dArray[0].x = 2;
- }
- printf("f1 complete\n");
- }
-
- void f2() {
- int64_t i = 0;
- for(i= 0; i < LOOP; i++) {
- dArray[1].x = 1;
- }
- printf("f2 complete\n");
- }
-
- int main() {
- printf("sizeof(Data):%d\n", sizeof(Data));
- std::thread t1(f1);
- std::thread t2(f2);
- t1.join();
- t2.join();
- printf("complete\n");
- return 0;
- }
如果Data结构体不对齐到cache line,那么dArray[0]和dArray[1]会在同一个cacheline上面,两个线程同时修改结构体成员变量,由于缓存一致性机制,会导致缓存失效。
上面代码的cache-miss比例 6.2%
- Performance counter statistics:
-
- 1,217,787,043 cache-references # 1.374 G/sec (100%)
- 75,934,256 cache-misses # 6.235430% miss rate (100%)
将____cacheline_aligned打开以后,结构体对齐到64字节,这样dArray[0]和dArray[1]将分别占用不同的cache line,写cache失效后两者不会互相影响,cache-miss比例0.017%:
- 1,216,454,682 cache-references # 1.413 G/sec (100%)
- 205,538 cache-misses # 0.016896% miss rate (100%)
2.2 结构体成员分布在不同cache line
数据结构中频繁访问的成员可以单独占用一个cache line或者相关的成员在不同的cache line中错开,以提高访问效率。比如linux内核struct zone数据结构中zone->lock和zone->lru_lock两个频繁访问的锁,可以让他们在不同的cache line中,以提高获取锁的效率。
- ZONE_PADDING(_pad1_)
- /* free areas of different sizes */
- struct free_area free_area[MAX_ORDER];
-
- /* zone flags, see below */
- unsigned long flags;
-
- /* Write-intensive fields used from the page allocator */
- spinlock_t lock;
-
- ZONE_PADDING(_pad2_)
-
- /* Write-intensive fields used by page reclaim */
-
- /* Fields commonly accessed by the page reclaim scanner */
- spinlock_t lru_lock;
- atomic_long_t vm_zone_stat[NR_VM_ZONE_STAT_ITEMS] __cacheline_aligned_in_smp;
- atomic_long_t vm_numa_stat[NR_VM_NUMA_STAT_ITEMS] __cacheline_aligned_in_smp;
- atomic_long_t vm_node_stat[NR_VM_NODE_STAT_ITEMS] __cacheline_aligned_in_smp;
内核代码将vm_zone_stat等几个内存系统频繁访问的数组对齐到cacheline,确保该数组成员在一个cacheline中
参考文章: