从存储角度提升程序性能

这篇主要是利用局部性原理优化程序性能,以前也写过一篇优化程序性能,这篇主要是结合perf工具,量化比较程序性能的优化前后使用。

一 存储体系

1.1 存储层组成

计算机的存储是由多级存储组成的存储体系,原因是越快的存储成本越高,越慢的存储成本越低。利用多级存储,可以让计算机在拥有一个成本和底层最便宜的存储相当,但是却以接近顶层存储存储的告诉速度向程序提供数据读写。存储体系中每一层都会作为下一层存储的缓存,如下图


层次存储

1.2 存储访问速度

存储体系中各层存储中越接近CPU的位置速度越快,L1 缓存访问的速度为1-5个时钟周期,L2缓存访问速度为12个时钟周期,L3 缓存大约30个时钟周期;一般来说L1和L2是每个CPU核心都具有的,L3是多个CPU核心共享的。
2GHZ的cpu上速度如下:


存储体系访问速度差异

1.3 linux中缓存查看

不同的cpu 上的L1、L2 和L3的大小是不同的,在Linux上可以通过以下命令查询:

[root@localhost ~]# tree /sys/devices/system/cpu/cpu0/cache
/sys/devices/system/cpu/cpu0/cache
├── index0
│   ├── coherency_line_size
│   ├── id
│   ├── level
│   ├── number_of_sets
│   ├── physical_line_partition
│   ├── shared_cpu_list
│   ├── shared_cpu_map
│   ├── size
│   ├── type
│   └── ways_of_associativity

[root@localhost ~]# cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
[root@localhost ~]# cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64
[root@localhost ~]# cat /sys/devices/system/cpu/cpu0/cache/index0/type
Data
[root@localhost ~]# cat /sys/devices/system/cpu/cpu0/cache/index0/ways_of_associativity 
8
[root@localhost ~]# cat /sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_list 
0
[root@localhost ~]# cat /sys/devices/system/cpu/cpu0/cache/index0/physical_line_partition 
1
[root@localhost ~]# cat /sys/devices/system/cpu/cpu0/cache/index0/number_of_sets 
64

说明:
size: 缓存大小32K
coherency_line_size: cache line size 对齐大小,64 字节。
number_of_sets: 缓存0中的组数;
ways_of_associativity : 组中的行数;
shared_cpu_list :可以被哪些cpu共享;
type: Data 数据缓存(Instruction指令缓存、Unified 通用指令)

更简单的查询办法:

[root@localhost ~]# lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                4
On-line CPU(s) list:   0-3
Thread(s) per core:    1
Core(s) per socket:    4
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 60
Model name:            Intel(R) Xeon(R) CPU E3-1220 v3 @ 3.10GHz
Stepping:              3
CPU MHz:               3106.417
CPU max MHz:           3500.0000
CPU min MHz:           800.0000
BogoMIPS:              6186.09
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              8192K
NUMA node0 CPU(s):     0-3

二 cache和内存

2.1 缓存优点和局限性

上面我们知道,L1和L2缓存数据是以cache line为单位的,一般都是64字节为一个cache line。也就是每次缓存数据的时候是直接缓存64个字节,这样可以利用存储的局部性优势,但是也会有伪共享的问题,即如果缓存的数据不够64个字节,两个缓存数据同时存在一个缓存行中,一个数据更新后,整个缓存行失效。

还有一个问题就是当一个数据被多个cpu核心共享的时候,会有一致性问题。如果一个cpu修改了数据,需要同时让其他cpu共享的同一份数据失效,这样会造成cpu cache miss和一致性问题。
行业内通过MESI协议来保证Cache的一致性。

2.2 内存和缓存映射

我们知道程序在计算机中运行的时候,访问的是虚拟地址,通过MMU将虚拟地址转成实际内存地址,如果
PTE(页表项)不在高速缓存中,就同样需要将页表项调入到缓存中,这样开销就会有几十到几百个时钟周期。如果PTE在L1缓存中,开销就会下降到1-2个时钟周期,所以现在MMU中都会包含一个小的PTE缓存叫TLB(后备缓冲器)。
CPU请求数据的时候是这样的:
1) CPU产生一个虚拟地址。
2)MMU从TLB中取出PTE。
3)MMU通过PTE将虚拟地址转成物理地址。
4)高速缓存或内存将请求的数据返回给CPU。
如果TLB没有命中,则需要从L1缓存中去取PTE,覆盖TLB一个PTE。

三 利用高速缓存优化程序性能

cpu访问高速缓存比访问内存快的多,快100倍左右,所以代码中要善于利用高速缓存提升程序的性能,如何利用那,就是尽量让写的程序满足局部性原理,局部性原理包括两个部分,第一个是时间的局部性即最近访问的变量,在很多的时间内会再次访问;第二是空间局部性原理,即某个地址被程序访问,它周边的地址很可能会被再次访问。

  • 像我们经常在循环中使用同一个局部变量,就满足时间的局部性原理,这些变量常被寄存器保存。
  • 我们循环遍历数组的时候,满足空间局部性原理。
    一般来说,我们的循环代码越小,循环步长越小,循环的次数越多,局部性就越好。

四 测试程序的缓存利用

以下面个小程序为例,看看满足局部性原理和不满足局部性原理的性能差异

#include 
#include 
#include 
#include 

#define SIZE  2048

long timediff(clock_t t1,clock_t t2)
{
  // CLOCKS_PER_SEC 1秒钟有多少个时钟
   return ( (double) t2 -t1)/CLOCKS_PER_SEC*1000;
}

int main(int argc,char ** argv)
{
    //栈上分配,不能分配大了,会内存错误的
    char arrs[SIZE][SIZE];
  // cpu时钟作为计数单元更准确
    clock_t start = clock();
    for (int i = 0; i< SIZE; i++) {
      for (int j = 0; j< SIZE;j++) {
          // arrs[i][j] =0;
          arrs[j][i] = 0;
      }
    }
   clock_t end = clock();
   //耗时多少ms
   printf("\nCost time:%ld  \n",timediff(start,end));
}

如果使用循环体内注释掉的代码: arrs[i][j] =0; 耗时10ms;如果使用循环体内没有注释掉的代码: arrs[j][i] = 0;耗时50ms。
性能相差5倍,如何得知是利用缓存局部性原理得到的性能提升那,我们可以通过perf工具来查看下两次执行情况:

[root@localhost testcode]# perf stat -e cache-references,cache-misses,instructions,cycles,L1-dcache-load-misses,L1-dcache-loads ./a.out

Cost time:10  

 Performance counter stats for './a.out':

            33,108      cache-references                                            
            10,248      cache-misses              #   30.953 % of all cache refs    
        55,486,651      instructions              #    1.46  insn per cycle         
        37,963,618      cycles                                                      
           112,745      L1-dcache-load-misses     #    0.62% of all L1-dcache hits  
        18,164,912      L1-dcache-loads                                             

       0.011468219 seconds time elapsed

       0.010431000 seconds user
       0.001043000 seconds sys

性能差的情况:

[root@localhost testcode]# perf stat -e cache-references,cache-misses,instructions,cycles,L1-dcache-load-misses,L1-dcache-loads ./a.out

Cost time:50  

 Performance counter stats for './a.out':

         4,228,333      cache-references                                            
            31,962      cache-misses              #    0.756 % of all cache refs    
        55,664,205      instructions              #    0.31  insn per cycle         
       177,039,427      cycles                                                      
         4,306,660      L1-dcache-load-misses     #   23.64% of all L1-dcache hits  
        18,215,744      L1-dcache-loads                                             

       0.051881676 seconds time elapsed

       0.050861000 seconds user
       0.000997000 seconds sys

说明:
①cache-references 表示总的缓存读取次数
②cache-misses 表示没有命中缓存次数
②/ ① 即缓存的没有命中率,

③ L1-dcache-load-misses: L1缓存没有命中次数
④ L1-dcache-loads : 总的L1缓存读取次数
可以看出,第一执行的总的缓存命中率比第二次总的缓存命中率要高的多,而且1个时钟周期要执行的指令条数也更多。

你可能感兴趣的:(从存储角度提升程序性能)