深入理解计算机系统:优化程序性能

  • 编写高效程序
    • 选择合适的算法和数据结构
    • 编写编译器能够有效优化从而转换成高效可执行代码的程序
    • 对于运算量非常大的计算,将任务分解为多个子任务,使其在多处理器和多核上并行执行

优化编译器的能力和局限性

妨碍优化的因素

  • 必须考虑两个指针可能指向同一个地址(存储器别名的使用)
    • pxpy 指向不同的地址时,*px*py 的值能正确交换
    • pxpy 指向相同的地址时,它们指向的值会变为零
    • 正确写法应该先检查 pxpy 的相等性
void swap(int* px, int* py)
{
  *px = *px + *py;
  *py = *px - *py;
  *px = *px - *py;
}
  • 函数调用
    • 当函数存在副作用(比如修改全局变量的值、打印输出等)时,func1()func2() 并不等价
int f();

int func1() 
{ 
  return f() + f() + f() + f();  // 4 次调用
}

int func2()
{
  return 4 * f(); // 1 次调用
}

优化程序性能示例

循环展开技术
void prefix_sum1(double a[], double p[], long n)
{
  long i;
  p[0] = a[0];
  for (i = 1; i < n; i++)
    p[i] = p[i-1] + a[i];
}

void prefix_sum2(double a[], double p[], long n)
{
  long i;
  p[0] = a[0];
  for (i = 1; i < n; i += 2) {
    p[i] = p[i-1] + a[i];
    p[i+1] = p[i] + a[i+1];
  }

  if (i < n) p[i] = p[i-1] + a[i];
}

示例

  • 向量抽象数据类型 vec_rec 的实现
typedef int data_t;

typedef struct {
  long len;
  data_t* data;
} vec_rec, *vec_ptr;

vec_ptr new_vec(long len)
{
  vec_ptr result = (vec_ptr)malloc(sizeof(vec_rec));

  if (!result) return NULL;

  result->len = len;

  if (len > 0) {
    data_t* data = (data_t *)calloc(len, sizeof(data_t));
    if (!data) {
      free((void*) result);
      return NULL;
    }
    return->data = data;
  }
  else
    result->data = NULL;

  return result;
}

// 获取 v[index],并将其存入 dest
int get_vec_element(vec_ptr v, long index, data_t* dest)
{
  if (index < 0 || index >= v->len) return 0;

  *dest = v->data[index];
  return 1;
}

long vec_length(vec_ptr v)
{
  return v->len;
}
  • 测试用例:合并运算
#define IDENT 0
#define OP    +
// #define IDENT 1
// #define OP    *

void combine1(vec_ptr v, data_t* dest)
{
  long i;
  *dest = IDENT;

  for (i = 0; i < vec_length(v); i++) { // 多次调用返回相同值的函数 vec_length(),明显降低效率
    data_t val;
    get_vec_element(v, i, &val);
    *dest = *dest OP val;
  }
}
代码移动技术
  • 识别要执行多次但结果不会改变的计算
void combine2(vec_ptr v, data_t* dest)
{
  long i;
  long length = vec_length(v);
  *dest = IDENT;

  for (i = 0; i < length; i++) { // 对 combine1() 的改进
    data_t val;
    get_vec_element(v, i, &val);
    *dest = *dest OP val;
  }
}
减少函数调用
data_t* get_vec_start(vec_ptr v)
{
  return v->data;
}

void combine3(vec_ptr v, data_t* dest)
{
  long i;
  long length = vec_length(v);
  data_t* data = get_vec_start(v);
  *dest = IDENT;

  for (i = 0; i < length; i++) { // 对 combine2() 的改进
    *dest = *dest OP data[i];
  }
}
在临时变量中存放结果,而不是每次读写传入的参数
void combine4(vec_ptr v, data_t* dest)
{
  long i;
  long length = vec_length(v);
  data_t* data = get_vec_start(v);
  data_t acc;
  *dest = IDENT;

  for (i = 0; i < length; i++) { // 对 combine2() 的改进
    acc = acc OP data[i];
  }

  *dest = acc;
}
循环展开
  • 增加每次迭代计算的元素的数量,减少循环的迭代次数
    • 减少循环索引计算和条件分支
    • 减少计算中关键路径上的操作数量
void combine5(vec_ptr v, data_t* dest)
{
  long i;
  long length = vec_length(v);
  long limit = length - 2;
  data_t* data = get_vec_start(v);
  data_t acc = IDENT;

  for (i = 0; i < limit; i += 3)
    acc = ((acc OP data[i]) OP data[i+1]) OP data[i+2];

  for (; i < length; i++)
    acc = acc OP data[i];

  *dest = acc;
}
重新结合变换
void combine7(vec_ptr v, data_t* dest)
{
  long i;
  long length = vec_length(v);
  long limit = length - 2;
  data_t* data = get_vec_start(v);
  data_t acc = IDENT;

  for (i = 0; i < limit; i += 3)
    acc = acc OP (data[i] OP (data[i+1] OP data[i+2]));

  for (; i < length; i++)
    acc = acc OP data[i];

  *dest = acc;
}
  • 用命令行展选项 -funroll-loops 调用 GCC,可以让编译器正确执行循环展开
提高并行性
void combine6(vec_ptr v, data_t* dest)
{
  long i;
  long length = vec_length(v);
  long limit = length - 1;
  data_t* data = get_vec_start(v);
  data_t acc0 = IDENT;
  data_t acc1 = IDENT;

  for (i = 0; i < limit; i += 2) {
    acc0 = acc0 OP data[i];
    acc1 = acc1 OP data[i+1]
  }

  for (; i < length; i++)
    acc0 = acc0 OP data[i];

  *dest = acc0 + acc1;
}
其他方式
  • 对于随机数组,minmax2() 的效率高于 minmax1()
void minmax1(int a[], int b[], int n)
{
  int i;
  for (i = 0; i < n; i++)
    if (a[i] > b[i]) {
      int tmp = a[i];
      a[i] = b[i];
      b[i] = tmp;
    }
}

void minmax2(int a[], int b[], int n)
{
  int i;
  for (i = 0; i < n; i++) {
    int min = a[i] < b[i] ? a[i] : b[i];
    int max = a[i] < b[i] ? b[i] : a[i];
    a[i] = min;
    b[i] = max;
  }
}

编写高速缓存友好的代码

  • 程序通常把大部分时间用在少量的核心函数上
  • 核心函数通常把大部分时间用在核心循环上

局部性

  • 程序总是倾向于访问最近访问过的数据项或附近的数据项
int sumvec(int v[N])
{
  int i, sum = 0;
  for (i = 0; i < N; i++)
    sum += v[i];
  return sum;
}

// 行优先,步长为 1 的引用模式,具有良好的空间局部性
int sumarraycols(int a[M][N])
{
  int i, j, sum = 0;
  for (i = 0; i < M; i++)
    for (j = 0; j < N; j++)
     sum += a[i][j];
  return sum;
}

// 列优先,步长为 N 的引用模式,不具有良好的空间局部性
int sumarraycols(int a[M][N])
{
  int i, j, sum = 0;
  for (j = 0; j < N; j++)
    for (i = 0; i < M; i++)
     sum += a[i][j];
  return sum;
}

时间局部性

  • 对局部变量的反复引用是好的

空间局部性

  • 重新排列循环以提高空间局部性
  • 例一:两个 n n n 阶方阵相乘 C n × n = A n × n B n × n C_{n\times n} = A_{n\times n}B_{n\times n} Cn×n=An×nBn×n
// ijk
for (i = 0; i < n; i++) {
  for (j = 0; j < n; j++) {
    sum = 0.0;
    for (k = 0; k < n; k++)
      sum += A[i][k] * B[k][j];
    C[i][j] += sum;
  }
}

// ikj,步长为 1 的访问模式
for (i = 0; i < n; i++) {
  for (k = 0; k < n; k++) {
    r = A[i][k];  // 局部变量
    for (j = 0; j < n; j++)
      C[i][j] += r * B[k][j];
  }
}

// kij,步长为 1 的访问模式
for (k = 0; k < n; k++) {
  for (i = 0; i < n; i++) {
    r = A[i][k];  // 局部变量
    for (j = 0; j < n; j++)
      C[i][j] += r * B[k][j];
  }
}
  • 例二:遍历三维矩阵
    • 当最右边的索引变化最快时,其引用模式步长为 1
int sumarray3d(int a[N][N][N])
{
  int i, j, k, sum = 0;
  for (i = 0; i < N; i++)
    for (j = 0; j < N; j++)
      for (k = 0; k < N; k++)
        sum += a[i][j][k];
  return sum;
}
  • 例三:遍历内含数组的结构的数组
    • 结构体内各成员的排列方式
#define N 1000
#define M 3
typedef struct {
  int vel[M];
  int acc[M];
} point;

point p[N];

// 步长为 1 的访问模式
void clear1(point* p)
{
  int i, j;
  for (i = 0; i < N; i++) {
    for (j = 0; j < M; j++)
      p[i].vel[j] = 0;
    for (j = 0; j < M; j++)
      p[i].acc[j] = 0;
  }
}

void clear2(point* p)
{
  int i, j;
  for (i = 0; i < N; i++) {
    for (j = 0; j < M; j++) {
      p[i].vel[j] = 0;
      p[i].acc[j] = 0;
    }
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值