0


几个影响 cpu cache 性能因素及 cache 测试工具介绍

===============================》内核新视界文章汇总《===============================

文章目录

1 cache 性能及影响因素

1.1 内存访问和性能比较

有如下测试代码:

#include<stdio.h>#include<stdlib.h>#include<time.h>#include<sys/time.h>intmain(int argc,char*argv[]){long length =64*1024*1024;int*arr =malloc(64*1024*1024*sizeof(int));long i=0, j=0;structtimeval tv1, tv2;structtimezone tz;unsignedlong sec, usec;for(i =0; i < length; i++) arr[i]= i;// 循环1gettimeofday(&tv1,&tz);for(j =0; j <10; j++){//每取一次arr[i], 通过 cache_line 顺便把后面 15 个 arr[i] 都取过来了for(i =0; i < length; i++) arr[i]*=3;}gettimeofday(&tv2,&tz);if(tv2.tv_usec < tv1.tv_usec){
        usec =1000000+ tv2.tv_usec - tv1.tv_usec;
        sec = tv2.tv_sec - tv1.tv_sec -1;}else{
        usec = tv2.tv_usec - tv1.tv_usec;
        sec = tv2.tv_sec - tv1.tv_sec;}printf("time %ld.%06ld s\n", sec, usec);// 循环2gettimeofday(&tv1,&tz);for(j =0; j <10; j++){for(i =0; i < length; i +=16) arr[i]*=3;}gettimeofday(&tv2,&tz);if(tv2.tv_usec < tv1.tv_usec){
        usec =1000000+ tv2.tv_usec - tv1.tv_usec;
        sec = tv2.tv_sec - tv1.tv_sec -1;}else{
        usec = tv2.tv_usec - tv1.tv_usec;
        sec = tv2.tv_sec - tv1.tv_sec;}printf("time %ld.%06ld s\n", sec, usec);return0;}

该测试代码预先分配大量内存,并与一个数组指针管理,使其可以通过数组索引访问该片申请内存,接着对该内存域进行预读写,使其在测试中不受 pagefault 影响测试结果。
该测试主要的测试点:

  1. 第一个循环按照字节顺序读写相同固定数量的数据。
  2. 第二个循环跨 cacheline 读取比第一次少 16 倍的数据量。

理论不考虑 cache 则读写时间应该是

第二次 * 16 = 第一次

,实际测试结果如下:

(1)HUAWEI Kunpeng 920

# ./a.out
time 2.522369 s
time 0.339671 s

#perfstat --./a.out
time 2.623479 s
time 0.307353 s

 Performance counter stats for'./a.out':3,154.14 msec task-clock                #    0.992 CPUs utilized
                 7      context-switches          #    0.002 K/sec
                 1      cpu-migrations            #    0.000 K/sec
             6,024      page-faults               #    0.002 M/sec
     8,200,710,113      cycles                    #    2.600 GHz
    15,203,228,361      instructions              #    1.85  insn per cycle
   <not supported>      branches
            76,472      branch-misses

       3.180782182 seconds time elapsed

       3.084959000 seconds user
       0.069436000 seconds sys

(2)Phytium S2500(飞腾 S2500)

# ./a.out
time 5.085658 s
time 1.352258 s

#perfstat --./a.out
time 5.535788 s
time 0.826428 s

 Performance counter stats for'./a.out':6,930.59 msec task-clock                #    0.999 CPUs utilized
                50      context-switches          #    0.007 K/sec
                25      cpu-migrations            #    0.004 K/sec
            14,731      page-faults               #    0.002 M/sec
    14,552,534,159      cycles                    #    2.100 GHz
    15,281,585,654      instructions              #    1.05  insn per cycle
   <not supported>      branches
           254,832      branch-misses

       6.934533427 seconds time elapsed

       6.481427000 seconds user
       0.450099000 seconds sys

可以看到两台机器第一次循环花费时间都远小于第二次的 16 这个倍数。

原因是第一次循环读写会从内存按照 cacheline 大小加载连续内存到 cache 中,如果后续读写数据在 cache 中则可以高效读写数据,而不会从内存中读取。

而第二次循环跨 cacheline 读写数据,每次均有可能从内存读写数据。

从上述测试中可以看到,cache 读 cpu 读写性能影响非常大。并且从测试也可以看到鲲鹏机器的 cache 读写速率高于飞腾机器。

1.2 cache line 对性能的影响

1.1

中的测试能够看到 cache 对性能的影响,而 cache 的 cacheline 的大小对性能提升也有着直接作用。
cache line 对 cache 性能影响尤为重要, 程序对 cacheline 的优化会大幅提升运行性能, 这里对

1.1

中的例子进行测试, 将步长 16 从 1 依次成倍增长测试不同步长,测试相同次数获取时延:

脚本如下:

# ./example2.sh#set -xwork=./a.out
stride=1foriin`seq115`;do
    perf stat-e cache-misses -e cache-references -e cpu-cycles -e instructions -e L1-dcache-load-misses -e L1-dcache-loads -e L1-dcache-store-misses -e L1-dcache-stores  -- $work$stridestride=$(($stride*2))done

(1)HUAWEI Kunpeng 920

# ./example2.sh
stride =1, time 2.817894 s
stride =2, time 2.920336 s
stride =4, time 3.495400 s
stride =8, time 4.565321 s
stride =16, time 6.297579 s
stride =32, time 7.362044 s
stride =64, time 7.982766 s
stride =128, time 10.473204 s
stride =256, time 9.676618 s
stride =512, time 12.120638 s
stride =1024, time 13.391726 s
stride =2048, time 17.123795 s
stride =4096, time 23.674495 s
stride =8192, time 24.273880 s
stride =16384, time 28.081542 s

可以看到读写时间随读写的步进大小逐渐递增至最后趋近平稳。而在 16,1024 步进时有大幅变化。
上述现象说明 cacheline 大小对性能影响的变化。

当步进在 Dcache-L1 时,比如 64 字节,此时在 Dcache-L1 中读写数据整体性能会非常好。
当步进从 L2 跨度到从 Dcache-L2 读写数据时,此时读写性能将会有一个明显降低,同理 L3 时会继续有个明显变化幅度,直至最后趋于稳定时,只能每次都从内存读写数据。

1.3 L1 和 L2 缓存大小

1.2

测试中,当在 cacheline 范围内读写数据时性能最好,那么 cacheline 越大性能则会越好。

1.4 指令集并行性对 cache 性能的影响

有如下代码:

int steps =256*1024*1024;int a[2];// Loop 1for(int i=0; i<steps; i++){ a[0]++; a[0]++;}// Loop 2for(int i=0; i<steps; i++){ a[0]++; a[1]++;}

循环上 loop2 比 loop1 更快两个代码有如下依赖:

  1. loop1 依赖关系x = a[0] -> x++ -> a[0] = x -> y = a[0] -> y++ -> a[0] = y
  2. loop2 依赖关系 ​ x = a[0] -> x++ -> a[0] = x``````y = a[1] -> y++ -> a[1] = y

即机器具有有并行性,它可以同时访问 L1 中的两个内存位置,loop1 中处理器无法利用这种指令级并行性,但是 loop2 可以,代码如下:

#include<stdio.h>#include<stdlib.h>#include<time.h>#definebarrier() __asm__ __volatile__("":::"memory")staticlongtimediff(clock_t t1,clock_t t2){long elapsed;
    elapsed =((double)t2 - t1)/ CLOCKS_PER_SEC *1000;return elapsed;}intmain(int argc,char*argv[]){long i=0, j=0;clock_t start, end;int a[2]={0};unsignedlong steps =2000*1024*1024;// loop1
    start=clock();for(i =0; i < steps; i++){
        a[0]++;barrier();
        a[0]++;barrier();}
    end =clock();printf("%lu\n",timediff(start,end));// loop2
    start=clock();for(i =0; i < steps; i++){
        a[0]++;barrier();
        a[1]++;barrier();}
    end =clock();printf("%lu\n",timediff(start,end));return0;}

测试如下:
(1)HUAWEI Kunpeng 920

loop1 time 11.611784 s
loop2 time 6.526364 s

(2)Phytium S2500

loop1 time 15.546450 s
loop2 time 7.637472 s

(3)Intel® Xeon® Gold 5218

loop1 time 8.084105 s
loop2 time 6.649898 s

可以看到指令集并行性在能够并行时性能都会有明显提升,另外三台机器中并行性最好的是华为鲲鹏,然而最好的并行性和因特尔 5218 持平,非并行性下 intel 具有最好的读写性能。

1.5 缓存关联性对 cache 的影响

目前有三种映射缓存方式:

  1. 直接映射缓存每个内存块只能存储在缓存中的一个特定插槽中。一种简单的解决方案是将索引为 chunk_index 的块映射到缓存槽 (chunk_index % cache_slots)。映射到同一插槽的两个内存块不能同时存储在缓存中。
  2. N路组相联高速缓存每个内存块都可以存储在缓存中 N 个特定插槽中的任何一个中。例如,在 16 路缓存中,每个内存块可以存储在 16 个不同的缓存槽中。通常,具有相同最低位的索引的块将共享 16 个槽。
  3. 全关联缓存每个内存块都可以存储在缓存中的任何槽中。实际上,缓存的运行方式类似于哈希表。

直接映射缓存可能会发生冲突,当多个值竞争缓存中的同一个插槽时,它们会不断相互驱逐,命中率直线下降。

全关联高速缓存在硬件中实现起来既复杂又昂贵。

N 路组关联高速缓存是处理器高速缓存的典型解决方案,因为它们在实现简单性和良好的命中率之间取得了很好的平衡。

当前所有测试机器都是 N路组相联,需要计算每个 way,sets 使用 bit 再去推算每一组可能丢失命中的字节步长,这里没有计算,略过。

1.6 错误的 cacheline 共享 (缓存一致性)

在多核机器上,缓存会遇到另一个问题,缓存一致性,也就是说多线程,多核之间并行对 cache 访问一致性问题对性能同样会造成严重影响。
在测试机器中,有 L1 L2 独占的,也有超线程下共享 L1的。
这里有如下测试:

for(int j =0; j <100000000; j++){ 
    s_counter[position]= s_counter[position]+3;}

使用四个不同的线程参数 0,1,2,3调用上述代码和使用16,32,48,64调用上述代码,16,32,48,64耗时比第一组小很多,这就是缓存一致性引起,代码如下:

// example6.c#include<stdio.h>#include<stdlib.h>#include<time.h>#include<pthread.h>#include<unistd.h>#include<stdio.h>staticint s_counter[1024]__attribute__((aligned(64)));int aaa =1;staticvoidupdate_counter(int posion){for(int i =0; i <100000000; i++){
        s_counter[posion]= s_counter[posion]+3;}}staticvoid*thread_func1(void*param){if(aaa)update_counter(0);elseupdate_counter(16);returnNULL;}staticvoid*thread_func2(void*param){if(aaa)update_counter(1);elseupdate_counter(32);returnNULL;}staticvoid*thread_func3(void*param){if(aaa)update_counter(2);elseupdate_counter(48);returnNULL;}staticvoid*thread_func4(void*param){if(aaa)update_counter(3);elseupdate_counter(64);returnNULL;}intmain(int argc,char*argv[]){pthread_t tid1, tid2, tid3, tid4;if(argc !=1)
        aaa =0;pthread_create(&tid1,NULL, thread_func1,NULL);pthread_create(&tid2,NULL, thread_func2,NULL);pthread_create(&tid3,NULL, thread_func3,NULL);pthread_create(&tid4,NULL, thread_func4,NULL);pthread_join(tid1,NULL);pthread_join(tid2,NULL);pthread_join(tid3,NULL);pthread_join(tid4,NULL);return0;}

测试如下:
(1)HUAWEI Kunpeng 920

#gcc-O0 -g example6.c -lpthread[root@node-3 test_cache]# time ./a.out    // 0,1,2,3

real    0m1.889s
user    0m6.994s
sys     0m0.000s
[root@node-3 test_cache]# time ./a.out  1// 16,32,48,64

real    0m0.451s
user    0m1.439s
sys     0m0.000s

[root@node-3 test_cache]# time taskset -c 0./a.out

real    0m1.251s
user    0m1.240s
sys     0m0.000s
[root@node-3 test_cache]# time taskset -c 0./a.out  1

real    0m1.250s
user    0m1.239s
sys     0m0.000s

(1)Phytium S2500

#gcc-O0 -g example6.c -lpthread[root@node-1 test_cache]# time ./a.out

real    0m0.467s
user    0m1.829s
sys     0m0.011s
[root@node-1 test_cache]# time ./a.out 1

real    0m0.432s
user    0m1.720s
sys     0m0.001s

[root@node-1 test_cache]# time taskset -c 0./a.out

real    0m1.724s
user    0m1.722s
sys     0m0.001s
[root@node-1 test_cache]# time taskset -c 0./a.out 1

real    0m1.727s
user    0m1.714s
sys     0m0.011s

1.7 硬件设计

在某些处理器上,如果 L1 缓存访问来自不同 bank 的缓存行,则它们可以并行处理两个访问,如果它们属于同一 bank,则可以串行处理,这是“硬件怪异”的一个奇怪例子:

staticint A, B, C, D, E, F, G;staticvoidWeirdness(){for(int i =0; i <200000000; i++){<something>}}<something>          Time
A++; B++; C++; D++;719 ms
A++; C++; E++; G++;448 ms
A++; C++;518 ms

递增字段 A、B、C、D 比递增字段 A、C、E、G 花费的时间更长。

仅递增 A 和 C 比递增 A 和 C 以及 E 和 G花费的时间更长

2 cpu cache benchmark 工具

2.1 使用 llcbench 工具对 cache 进行性能测试

LLCbench (底层表征基准测试 Low-Level Characterization Benchmarks) 是一个基准测试工具, 集成了 MPBench, CacheBench 和 BLASBench 测试方法。这里只使用其中的 CacheBench 测试缓存性能。(llcbench 工具目前可能需要一些科学上网才能获取)
测试步骤:

#tar-xvf llcbench-20170104.tar.gz#cdllcbench;make;make linux-lam;make cache-bench;make cache-run#makecache-scripts#makecache-graph

测试大致原理: 对不同字节长度数组进行同样步长的循环读写测试统计时间(同

1

1.1

测试类似,不过 llcbench 工具更加标准和准确),生成的测试数据如下:
(1)HUAWEI Kunpeng 920

  1. 全局测试在这里插入图片描述
  2. numa 绑定最近节点: numactl -C 24 -m 1 make cache-run distances 10在这里插入图片描述
  3. numa 绑定最远节点: numactl -C 24 -m 3 make cache-run distances 24在这里插入图片描述 通过跳变位置可以看到每个 cache 级别的大小,以及内存读写速率位置。同样的 numa 对性能影响也是明显的(体现在图标最后一个坡段)。

2.2 使用 pts 工具对内存缓存带宽进行测试

pts (phoronix-test-suite) 的 cachebench 测试底层同样使用 llcbench 进行测试, 这里使用 pts 测试各平台内存缓存带宽。

测试步骤如下:

# ./install-sh    # 安装 pts
#phoronix-test-suite install pts/cachebench  # 下载 cachebench 测试用例#phoronix-test-suite run pts/cachebench   # 运行 cachebench 测试

(1)HUAWEI Kunpeng 920

  1. 读测试:在这里插入图片描述
  2. 写测试在这里插入图片描述
  3. 读-修改-写测试:在这里插入图片描述
  4. 测试报告: https://openbenchmarking.org/result/2302010-NE-KUNPENG3610

2.3 lmbench 对 L1, L2, L3 cache 时延及带宽测试

lmbench 是一个微型评测工具, 它衡量两个关键特征: 反应时间和带宽。

使用工具集中的 stream 测试带宽,对应的时延是在带宽跑满下的时延。

使用工具集中的 lat_mem_rd 测试时延,主要对应不同数据大小下的时延。

2.3.1 工具常规测试步骤
# 编译问题
#makeresults LDFLAGS=-ltirpc#yum-y install libtirpc libtirpc-devel#cp/usr/include/tirpc/rpc/* /usr/include/rpc/ -rf
# ln -s /usr/include/tirpc/netconfig.h /usr/include

# 测试
# cd src
# make results
# make rerun; make rerun; make rerun; cd results; make LIST=<OS>; make ps;

一个示例对应结果如下:
Memory latencies in nanoseconds - smaller is better
    (WARNING - may not be correct, check graphs)
------------------------------------------------------------------------------
Host                 OS   Mhz   L1 $   L2 $    Main mem    Rand mem    Guesses
--------- -------------   ---   ----   ----    --------    --------    -------
easystack Linux 5.15.79  4083 1.2130 2.5860 8.6590       161.1

显示结果分别为(单位纳秒):
L1: L1 cache 时延
L2: L2 cache 时延
Main mem: 顺序读内存时延
Rand mem: 随机访问内存时延
Guesses: 当 L2 不存在时, 可能会打印 No L2 cache?

对应分布图如下:
在这里插入图片描述
lmbench 常规测试可以很好反应各级别 cache 时延, 以及不同数据跨度下 cache 时延的变化, 不过 lmbench 测试对 arm64 平台测试有问题, 且每次测试时间过长, 这里直接使用 stream 和 lat_mem_rd 进行测试。

2.3.2 stream 带宽及时延测试

HUAWEI Kunpeng 920(同一个 numa 到不同 cpu 的时延及带宽 (node -> cpux))

#fori in $(seq 02495);do echo "cpu:$i -> node0"; numactl -C $i -m 0./stream -W 50-N 50-M 64M; done
cpu:0-> node0
STREAM copy latency:1.28 nanoseconds
STREAM copy bandwidth:12546.05 MB/sec
STREAM scale latency:1.31 nanoseconds
STREAM scale bandwidth:12207.16 MB/sec
STREAM add latency:1.94 nanoseconds
STREAM add bandwidth:12381.71 MB/sec
STREAM triad latency:2.18 nanoseconds
STREAM triad bandwidth:11019.52 MB/sec
cpu:24-> node0
STREAM copy latency:1.47 nanoseconds
STREAM copy bandwidth:10851.14 MB/sec
STREAM scale latency:1.51 nanoseconds
STREAM scale bandwidth:10610.51 MB/sec
STREAM add latency:2.03 nanoseconds
STREAM add bandwidth:11812.86 MB/sec
STREAM triad latency:2.34 nanoseconds
STREAM triad bandwidth:10247.19 MB/sec
cpu:48-> node0
STREAM copy latency:1.75 nanoseconds
STREAM copy bandwidth:9152.87 MB/sec
STREAM scale latency:2.54 nanoseconds
STREAM scale bandwidth:6298.64 MB/sec
STREAM add latency:2.35 nanoseconds
STREAM add bandwidth:10191.17 MB/sec
STREAM triad latency:3.81 nanoseconds
STREAM triad bandwidth:6303.67 MB/sec
cpu:72-> node0
STREAM copy latency:1.84 nanoseconds
STREAM copy bandwidth:8699.05 MB/sec
STREAM scale latency:2.83 nanoseconds
STREAM scale bandwidth:5661.04 MB/sec
STREAM add latency:2.56 nanoseconds
STREAM add bandwidth:9362.28 MB/sec
STREAM triad latency:4.14 nanoseconds
STREAM triad bandwidth:5792.24 MB/sec
2.3.2 lat_mem_rd 不同数据长度的时延测试

HUAWEI Kunpeng 920(同一个 numa 到不同 cpu 的时延 (node -> cpux))

#fori in $(seq 02495);do echo "cpu:$i -> node0"; numactl -C $i -m 0./lat_mem_rd -W 10-N 10-t 64M; done
cpu:0-> node0
"stride=640.000491.5520.000981.5520.001951.5520.002931.552...0.109381.5520.117191.5520.125003.1040.140623.1040.156253.1030.171883.104...0.468753.1040.500003.1060.562507.3890.625009.3850.6875010.2380.7500011.6800.8125011.7350.8750013.077
2.3.2.1 lat_mem_rd 替代实验

使用 memory_latency.c 对不同数据长度进行相同步长测试:

#include<sys/types.h>#include<stdlib.h>#include<stdio.h>#include<sys/mman.h>#include<sys/time.h>#include<unistd.h>#defineONEp =(char**)*p;#defineFIVEONE ONE ONE ONE ONE#defineTENFIVE FIVE#defineFIFTYTEN TEN TEN TEN TEN#defineHUNDREDFIFTY FIFTYstaticvoidusage(){printf("Usage: ./mem-lat -b xxx -n xxx -s xxx\n");printf("   -b buffer size in KB\n");printf("   -n number of read\n\n");printf("   -s stride skipped before the next access\n\n");printf("Please don't use non-decimal based number\n");}intmain(int argc,char* argv[]){unsignedlong i, j, size, tmp;unsignedlong memsize =0x800000;/* 1/4 LLC size of skylake, 1/5 of broadwell */unsignedlong count =1048576;/* memsize / 64 * 8 */unsignedint stride =64;/* skipped amount of memory before the next access */unsignedlong sec, usec;structtimeval tv1, tv2;structtimezone tz;unsignedint*indices;while(argc-->0){if((*argv)[0]=='-'){/* look at first char of next */switch((*argv)[1]){/* look at second */case'b':
                argv++;
                argc--;
                memsize =atoi(*argv)*1024;break;case'n':
                argv++;
                argc--;
                count =atoi(*argv);break;case's':
                argv++;
                argc--;
                stride =atoi(*argv);break;default:usage();exit(1);break;}}
        argv++;}char* mem =mmap(NULL, memsize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON,-1,0);// trick3: init pointer chasing, per stride=8 byte
    size = memsize / stride;
    indices =malloc(size *sizeof(int));for(i =0; i < size; i++)
        indices[i]= i;// trick 2: fill mem with pointer referencesfor(i =0; i < size -1; i++)*(char**)&mem[indices[i]*stride]=(char*)&mem[indices[i+1]*stride];*(char**)&mem[indices[size-1]*stride]=(char*)&mem[indices[0]*stride];registerchar**p =(char**) mem;
    tmp = count /100;gettimeofday(&tv1,&tz);for(i =0; i < tmp;++i){
        HUNDRED;//trick 1}gettimeofday(&tv2,&tz);char**touch = p;if(tv2.tv_usec < tv1.tv_usec){
        usec =1000000+ tv2.tv_usec - tv1.tv_usec;
        sec = tv2.tv_sec - tv1.tv_sec -1;}else{
        usec = tv2.tv_usec - tv1.tv_usec;
        sec = tv2.tv_sec - tv1.tv_sec;}printf("Buffer size: %ld KB, stride %d, time %ld.%06ld s, latency %.2f ns\n",
            memsize/1024, stride, sec, usec,(sec *1000000+ usec)*1000.0/(tmp *100));munmap(mem, memsize);free(indices);}

mem-lat.sh

#set -xwork=./a.out
buffer_size=1stride=64foriin`seq115`;do
    taskset -ac0$work-b$buffer_size-s$stridebuffer_size=$(($buffer_size*2))done

HUAWEI Kunpeng 920

# ./mem-lat.sh
Buffer size:1 KB, stride 64, time 0.001627 s, latency 1.55 ns
Buffer size:2 KB, stride 64, time 0.001627 s, latency 1.55 ns
Buffer size:4 KB, stride 64, time 0.001637 s, latency 1.56 ns
Buffer size:8 KB, stride 64, time 0.001627 s, latency 1.55 ns
Buffer size:16 KB, stride 64, time 0.001626 s, latency 1.55 ns
Buffer size:32 KB, stride 64, time 0.001627 s, latency 1.55 ns
Buffer size:64 KB, stride 64, time 0.001664 s, latency 1.59 ns
Buffer size:128 KB, stride 64, time 0.003489 s, latency 3.33 ns
Buffer size:256 KB, stride 64, time 0.003542 s, latency 3.38 ns
Buffer size:512 KB, stride 64, time 0.003605 s, latency 3.44 ns
Buffer size:1024 KB, stride 64, time 0.004646 s, latency 4.43 ns
Buffer size:2048 KB, stride 64, time 0.004625 s, latency 4.41 ns
Buffer size:4096 KB, stride 64, time 0.005523 s, latency 5.27 ns
Buffer size:8192 KB, stride 64, time 0.006047 s, latency 5.77 ns
Buffer size:16384 KB, stride 64, time 0.007000 s, latency 6.68 ns

本文转载自: https://blog.csdn.net/maybeYoc/article/details/131984365
版权归原作者 内核新视界 所有, 如有侵权,请联系我们删除。

“几个影响 cpu cache 性能因素及 cache 测试工具介绍”的评论:

还没有评论