高效利用CPU Cache

2022-12-17  本文已影响0人  谭英智

本文动机

提高程序性能,一般是通过算法复杂度来提升性能;

如果在复杂度无法再提升后,可以通过更底层次的优化来提升性能。

例如利用CPU Cache的硬件特性,来加快代码运行的速度

CPU Cache知识背景

CPU在访问内存的时候,会以cache line的形式(64bytes)加载数据到CPU cache。

当CPU访问的数据是连贯的

这样的取数形式,会达到加速的效果

否则,性能会下降

Tips

Tip:遍历数据,该使用数组还是链表?

如果是遍历一个集合的数据,是使用数组更优还是链表更优?

通过构造一个数组,并遍历其中的元素;构造一个相同大小的链表,并遍历其中的元素

对比他们之间的性能差别

遍历数组:

void processVector(benchmark::State& state) {
  for (auto _ : state) {
    std::vector<int> v;
    for(int i=0; i< size; ++i){
      v.push_back(i);
    }
    for(int i=0; i< size; i++) {
      for(auto& item: v){
        benchmark::DoNotOptimize(v);
      }
      state.SetBytesProcessed(size);
    }
    
  }
}
// Register the function as a benchmark
BENCHMARK(processVector);

遍历链表:

void processList(benchmark::State& state) {
  for (auto _ : state) {
    std::list<int> v;
    for(int i=0; i< size; ++i){
      v.push_back(i);
    }
    for(int i=0; i< size; i++) {
      for(auto& item: v){
        benchmark::DoNotOptimize(v);
      }
      state.SetBytesProcessed(size);
    }
  }
}
// Register the function as a benchmark
BENCHMARK(processList);

结果如下,遍历数组的性能是遍历链表的3倍

cache-vector-or-list
分析:

Tip:经常被一起访问的数据,应该尽量在内存中靠近彼此

定义一个类的时候,一般会有多个成员变量,成员变量跟成员变量之间的亲疏关系是不一样的。

通过构造两个亲密关系的成员变量,一个类彼此靠近,一个类彼此远离。

查看他们共同访问时,性能有什么差别

#include <vector>

class A {
public:
  int a;
  int b;
  int padding[16];
};
class B {
public:
  int a;
  int padding[16];
  int b;
};
#define kArrSize 100

彼此靠近的类:

static void closeMemory(benchmark::State& state) {
  for (auto _ : state) {
    A arr[kArrSize];
    for(auto& item: arr) {
      item.a = 10;
      item.b = 20;
      benchmark::DoNotOptimize(item);
    }
  }
}
BENCHMARK(closeMemory);

彼此远离的类:

static void farMemory(benchmark::State& state) {
  for (auto _ : state) {
    B arr[kArrSize];
    for(auto& item: arr) {
      item.a = 10;
      item.b = 20;
      benchmark::DoNotOptimize(item);
    }
  }
}
BENCHMARK(farMemory);

彼此靠近的类,访问速度是彼此远离的类的1.6倍

cache-close-or-far
分析:

Tip:使用对象数组而不是对象指针数组

当程序组织对象时,有两个选择,一是以指针方式放入容器中,一是把对象直接放入容器中。

那么这两种方式,在性能上,哪个更高呢?

#include <vector>
#include <random>
#include <algorithm>
class A {
public:
  int a;
  int b;
};

#define kArrSize 100000

构造元素是对象的数组,并进行遍历

static void arrOfObjectMemory(benchmark::State& state) {
  for (auto _ : state) {
    A arr[kArrSize];
    std::array<A*, kArrSize> point_arr;
    for(int i=0; i<kArrSize; ++i){
      point_arr[i] = &arr[i];
    }
    for(auto& item: arr) {
      item.a = 10;
      item.b = 20;
      benchmark::DoNotOptimize(item);
    }
  }
}
BENCHMARK(arrOfObjectMemory);

构造元素是对象指针的数组,并进行遍历

static void arrOfPointMemory(benchmark::State& state) {
  for (auto _ : state) {
    A arr[kArrSize];
    std::array<A*, kArrSize> point_arr;
    for(int i=0; i<kArrSize; ++i){
      point_arr[i] = &arr[i];
    }
    for(auto& item: point_arr) {
      item->a = 10;
      item->b = 20;
      benchmark::DoNotOptimize(item);
    }
  }
}
BENCHMARK(arrOfPointMemory);

遍历对象数组是对象指针数组的2.4倍

cache-array-of-object
分析:

Tip:通过cache line大小来优化对象大小

如果有一个对象数组,对象的大小是48bytes(不是cache line大小64bytes),那么随机访问数组中的元素,性能会不会比以64 bytes大小对齐的对象性能下降呢?

#include <benchmark/benchmark.h>

#include <vector>
#include <random>
#include <algorithm>
class A {
public:
  int a;
  int number[4];
  int b;
};

class  alignas(64) B {
public:
  int a;
  int number[4];
  int b;
};

#define kArrSize 10000

构造48 bytes的对象数组,并遍历

static void _48bytesClass(benchmark::State& state) {
  for (auto _ : state) {
    A arr[kArrSize];
    int i = 0;
    srand(1);
    for(int j=0; j<10000; ++j){
      for(int i=0; i< kArrSize; ++i) {
        int index = rand()%(kArrSize-3);
        arr[index].a = i;
        arr[index].b = i*2;
        benchmark::DoNotOptimize(arr);
      }
    }
    
  }
}
BENCHMARK(_48bytesClass);

构造64 bytes的对象数组,并遍历

static void _64bytesClass(benchmark::State& state) {
  for (auto _ : state) {
    B arr[kArrSize];
    int i = 0;
    srand(1);
    for(int j=0; j<10000; ++j){
      for(int i=0; i< kArrSize; ++i) {
        int index = rand()%(kArrSize-3);
        arr[index].a = i;
        arr[index].b = i*2;
        benchmark::DoNotOptimize(arr);
      }
    }
  }
}
BENCHMARK(_64bytesClass);

结果64 bytes的对象随机访问比48 bytes的对象性能快1.2倍

cache-align-random
分析:

Tip:优化多维数组访问顺序

CPU cache是行优先的,对于列优先的数据结构,如果更好的利用cache呢?

使用列优先访问二维数组:

static void multipy(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  for (auto _ : state) {
    int in_matrix1[size][size] = {0};
    int in_matrix2[size][size] = {0};

    for(int i=0; i<size; ++i){
      for(int j=0; j<size; ++j) {
        in_matrix1[i][j] = i+j;
        in_matrix2[i][j] = i+j;
      }
    }
    
    int result[size][size] = {0};
    for(int m=0; m<loop_size; ++m){
      int i, j, k; 
      for (i = 0; i < size; i++) { 
          for (j = 0; j < size; j++) { 
              result[i][j] = 0; 
              for (k = 0; k < size; k++) {
                 result[i][j] += in_matrix1[i][k] *  
                               in_matrix2[k][j];
              benchmark::DoNotOptimize(result[i][j]); 
      
            } 
        } 
      } 
      
    }
    state.SetBytesProcessed(loop_size);
    
    
  }
}
// Register the function as a benchmark
BENCHMARK(multipy);

使用行优先访问二维数组

static void opt_multipy(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  for (auto _ : state) {
    int in_matrix1[size][size] = {0};
    int in_matrix2[size][size] = {0};

    for(int i=0; i<size; ++i){
      for(int j=0; j<size; ++j) {
        in_matrix1[i][j] = i+j;
        in_matrix2[i][j] = i+j;
      }
    }
    int result[size][size] = {0};
    for(int m=0; m<loop_size; ++m){
      int i, j, k; 
      for (i = 0; i < size; i++) { 
          for (j = 0; j < size; j++) { 
              result[i][j] = 0; 
          }
          for (k = 0; k < size; k++) {
              for (j = 0; j < size; j++) {
                  result[i][j] += in_matrix1[i][k] *  
                              in_matrix2[k][j]; 
                  benchmark::DoNotOptimize(result[i][j]);
      
            } 
        } 
      } 
      // Make sure the variable is not optimized away by compiler
      
      
    }
    state.SetBytesProcessed(loop_size);
    
  }
}
// Register the function as a benchmark
BENCHMARK(opt_multipy);

行优先的算法是列优先的性能的2倍

cache-metrics
分析:

Tip:避免在类中发生默认padding

一个类中会有不同类型的成员变量,如果成员变量没有什么亲疏关系,如果调整他们的布局,会不会发生微妙的性能差别?

#include <vector>
#include <random>
#include <algorithm>
class A {
public:
  int my_int;
  double my_double;
  int my_second_int;
};

class B {
public:
  double my_double;
  int my_int;
  int my_second_int;
};

#define kArrSize 10000

构造顺序为 int double int的类

static void paddingClass(benchmark::State& state) {
  for (auto _ : state) {
    A arr[kArrSize];
    for(int j=0; j<10000; ++j){
      for(int i=0; i< kArrSize; ++i) {
        auto& item = arr[i];
        item.my_int = i;
        item.my_second_int = i+2;
        benchmark::DoNotOptimize(item);
      }
    }
  }
}
BENCHMARK(paddingClass);

构造顺序为double int int的类

static void noPaddingClass(benchmark::State& state) {
  for (auto _ : state) {
    B arr[kArrSize];
    for(int j=0; j<10000; ++j){
      for(int i=0; i< kArrSize; ++i) {
        auto& item = arr[i];
        item.my_int = i;
        item.my_second_int = i+2;
        benchmark::DoNotOptimize(item);
      }
    }
  }
}
BENCHMARK(noPaddingClass);

结果顺序为double int int的类的遍历快1.4倍

cache-padding
分析:

Tip:使用栈分配内存而不是堆分配内存

构造使用堆内存和使用栈内存,观察哪种方式性能更优

使用堆内存

#include <iostream>
#include <string.h>
#define size  100
#define loop_size 10000
static void heap(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  for (auto _ : state) {
    for(int i=0; i<loop_size; ++i){
      void* p = malloc(size);
      memset(p, 1, size);
      benchmark::DoNotOptimize(p);
      free(p);
    }
    state.SetBytesProcessed(loop_size);
  }
}
// Register the function as a benchmark
BENCHMARK(heap);

使用栈内存

static void stack(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  for (auto _ : state) {
    for(int i=0; i<loop_size; ++i){
      char p[size];
      memset(p, 1, size);
      benchmark::DoNotOptimize(p);
    }
    state.SetBytesProcessed(loop_size);
  }
}
// Register the function as a benchmark
BENCHMARK(stack);

使用栈内存性能是使用堆内存的6倍

cache-stack
分析:

Tip:当数据还在cache中,尽量重复使用,而不是多次加载

在循环一个数组时,是遍历数组一次做一件事?还是遍历一次数组做多件事效率高?

我们来看下面的实验

遍历两次数组,一次获取最大值,一次获取最小值

#include <iostream>
#include <string.h>
#include <algorithm> 
#include <vector>
#define size  10000
#define loop_size 10000
static void noshare(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  std::vector<int> a;
  a.resize(size);
  for(int i=0; i<size; ++i) {
    a[i] = i;
  }
  std::random_shuffle(a.begin(), a.end());
  int min = a[0];
  int max = a[0];
  for (auto _ : state) {
    for(int i=0; i<loop_size; ++i){
      for (int i = 0; i < size; i++) {
        max = std::max(a[i], max);
        benchmark::DoNotOptimize(max);
      }
      for (int i = 0; i < size; i++) {
        min = std::min(a[i], min);
        benchmark::DoNotOptimize(min);
      } 
    }
    state.SetBytesProcessed(loop_size);
  }
}
// Register the function as a benchmark
BENCHMARK(noshare);

遍历一次数组,同时获取最大值和最小值

static void share(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  std::vector<int> a;
  a.resize(size);
  for(int i=0; i<size; ++i) {
    a[i] = i;
  }
  std::random_shuffle(a.begin(), a.end());
  int min = a[0];
  int max = a[0];
  for (auto _ : state) {
    for(int i=0; i<loop_size; ++i){
      for (int i = 0; i < size; i++) {
        max = std::max(a[i], max);
        benchmark::DoNotOptimize(max);
        min = std::min(a[i], min);
        benchmark::DoNotOptimize(min);
      }
    }
    state.SetBytesProcessed(loop_size);
  }
}
// Register the function as a benchmark
BENCHMARK(share);

结果只遍历数组一次的性能是遍历两次的性能的2倍

cache-share
分析:

Tip:尽量少去写内存

写内存的次数会不会影响性能?

构造一个冒泡排序,每次比较都交换元素

#include <iostream>
#include <string.h>
#include <algorithm> 
#include <vector>
#define size  100
#define loop_size 1000
static void sort_slow(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  std::vector<int> b;
  b.resize(size);
  for(int i=0; i<size; ++i) {
    b[i] = i;
  }
  std::random_shuffle(b.begin(), b.end());
  for (auto _ : state) {
    for(int i=0; i<loop_size; ++i){
       std::vector<int> a = b;
       for (int i = 0; i < size; i++) {
        for (int j = i+1; j < size; j++) {
            if (a[j] < a[i]) {
                std::swap(a[j], a[i]);
            }
        }
      }
    }
    state.SetBytesProcessed(loop_size);
  }
}
// Register the function as a benchmark
BENCHMARK(sort_slow);

冒泡排序,找到最终位置后,才交换一次元素

static void sort_fast(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  std::vector<int> b;
  b.resize(size);
  for(int i=0; i<size; ++i) {
    b[i] = i;
  }
  std::random_shuffle(b.begin(), b.end());
  for (auto _ : state) {
    for(int i=0; i<loop_size; ++i){
       std::vector<int> a = b;
       for (int i = 0; i < size; i++) {
        int min = a[i];
        int min_index = i;
        for (int j = i+1; j < size; j++) {
            if (a[j] < min) {
                min = a[j];
                min_index = j;
            }
        }
        std::swap(a[i], a[min_index]);
     }
    }
    state.SetBytesProcessed(loop_size);
  }
}
// Register the function as a benchmark
BENCHMARK(sort_fast);

交换少的性能是频繁交换的性能的1.4倍

cache-write-memory
分析:

Tip:使用软件预取

当程序随机访问内存时,是否可以通过软件预取,来加快访问内存的速度呢?

朴素的二分查找

#include <iostream>
#include <string.h>
#include <algorithm> 
#include <vector>
#define size  100000000
#define loop_size 100000
#define index_size 100
int binarySearch(std::vector<int>& array, int number_of_elements, int key) {
  int low = 0, high = number_of_elements-1, mid;
  while(low <= high) {
    mid = (low + high)/2;
    if(array[mid] < key)
        low = mid + 1; 
    else if(array[mid] == key)
          return mid;
    else if(array[mid] > key)
          high = mid-1;
  }
}
static void no_prefetch(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  srand(1);
  std::vector<int> a;
  a.resize(size);
  for(unsigned int i=0; i<size; ++i) {
    a[i] = i;
  }
  for (auto _ : state) {
    for(unsigned int i=0; i<size; ++i){
      int ret = binarySearch(a, size, rand()%size);
      benchmark::DoNotOptimize(ret);
    }
    state.SetBytesProcessed(size);
  }
}
// Register the function as a benchmark
BENCHMARK(no_prefetch);

预取功能的二分查找

int prefetch_binarySearch(std::vector<int>& array, int number_of_elements, int key) {
  int low = 0, high = number_of_elements-1, mid;
    while(low <= high) {
        mid = (low + high)/2;
        // low path
        __builtin_prefetch (&array[(mid + 1 + high)/2], 0, 1);
        // high path
        __builtin_prefetch (&array[(low + mid - 1)/2], 0, 1);
        if(array[mid] < key)
            low = mid + 1; 
        else if(array[mid] == key)
             return mid;
        else if(array[mid] > key)
             high = mid-1;
        }
}
static void preFetch(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  srand(1);
  std::vector<int> a;
  a.resize(size);
  for(unsigned int i=0; i<size; ++i) {
    a[i] = i;
  }

  for (auto _ : state) {
    for(unsigned int i=0; i<size; ++i){
      int ret = prefetch_binarySearch(a, size, rand()%size);
      benchmark::DoNotOptimize(ret);
    }
    state.SetBytesProcessed(size);
  }
}
// Register the function as a benchmark
BENCHMARK(preFetch);

预取的二分查找是朴素的性能的1.4倍

cache-prefetch
分析:
上一篇下一篇

猜你喜欢

热点阅读