1.局部性
内存和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时间。
2.cache伪共享(false sharing)
多处理器架构下,多线程并行写入同一内存位置,由于缓存一致性问题会导致性能问题,这种现象称为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;
3. 数组对齐到cacheline
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中
参考文章: