优化程序性能

这里所说的优化程序性能,不是说优化程序的算法,使得程序的性能从O(n^2)提升到O(n),而是说把程序的性能从2n提升n。也就是说,这里关系的不是程序渐进关系,而是关系前面的系数。

这里讲的优化方式,大多是针对有循环结构的,所以这里我们引入度量标准每元素的周期数(Cycles Per Element,PCE),作为一种表示程序性能并指导我们改进代码的方法。

妨碍编译器优化的两个因素

内存别名使用

内存别名使用指的是两个指针指向同一个内存位置的情况,在只执行安全的优化中,编译器必须假设不同的指针可能会指向内存中的同一个位置。所以编译器不会把下面的代码

void twiddle(long *xp, long *yp){
    *xp += *yp;
    *xp += *yp;
}

优化成下面的代码。

void twiddle(long *xp, long *yp){
    *xp += 2* *yp;
}

这是因为如果xp和yp如果指向同一个内存,那么前者使xp增加了3倍,而后者是xp增加两倍。

函数调用

函数调用可能有副作用,也就是函数可能维护了一个内部的状态,导致调用它的次数会改变程序的行为,如rand函数。所以,编译器一般会假设最糟糕的情况,并保存所有的函数调用不变。如编译器下面的代码

long func(){
    return f() + f() + f() + f();
}

优化成下面的代码。

long func(){
    return 4 * f();
}

常用的优化方法

代码移动

代码移动主要是正对要执行多次,但计算结果不会改变的计算。因而可以将计算移动到代码前面不会被多次求值的部分。如下面的例子。

void lower(char *s){
    size_t i;
    for (i = 0; i < strlen(s); i++)
        if (s[i] >= 'A' && s[i] <= 'Z')
            s[i] -= ('A' - 'a');
}

这里编译器无法优化对strlen的调用,所以这里每一次循环都会调用strlen,整个算法的复杂度就变为了O(n^2)。我们可以提前计算strlen的值,这样就可以把算法的复杂度降低到O(n)

降低计算强度

这里指的是使用开销小的运算来替代开销大的运算,如我们可以使用左移右移来替换乘除2的幂次方的运算。下面是一个例子。

for (i = 0; i < n; i++){
    int ni = n * i;
    for (j = 0; j < n; j++)
        a[ni + j] = b[j];
}

上面的代码可以优化成下面这种形式。

int ni = 0;
for (i = 0; i < n; i++){
    for (j = 0; j < n; j++)
        a[ni + j] = b[j];
    ni += n;
}

这里,利用了循环,把乘法使用加法替代。这种优化编译器可以完成。

重复利用部分计算结果

这个没什么好解释的,看看下面的例子。

/* Sum neighbors of i,j */ 
up = val[(i - 1) * n + j];
down = val[(i + 1) * n + j];
left = val[i * n + j - 1];
right = val[i * n + j + 1];
sum = up + down + left + right;

上面的代码可以优化为下面的代码。

long inj = i * n + j;
up = val[inj - n];
down = val[inj + n];
left = val[inj - 1];
right = val[inj + 1];
sum = up + down + left + right;

为了测试性能,我们先定义了一些抽象的数据结构。

#define data_t int
#define IDENT 0
#define OP +

typedef struct{
    size_t len;
    data_t *data;
} vec,*vec_ptr;

int get_vec_element(vec *v, size_t idx, data_t *val){
    if (idx >= v->len)
        return 0;
    *val = v->data[idx];
    return 1;
}

下面是测试的基准程序,即什么优化都不做的程序。

void combine1(vec_ptr v, data_t *dest){
    long int i;
    *dest = IDENT;
    for (i = 0; i < vec_length(v); i++){
        data_t val;
        get_vec_element(v, i, &val);
        *dest = *dest OP val;
    }
}

我们可以先对其做一些基础的优化。

void combine4(vec_ptr v, data_t *dest){
    long i;
    long length = vec_length(v);
    data_t *d = get_vec_start(v);
    data_t t = IDENT;
    for (i = 0; i < length; i++)
        t = t OP d[i];
    *dest = t;
}

下面讲一讲进一步的优化。

循环展开

讲循环展开,就不得不先讲一讲流水线。虽然在代码中乘法是一个操作,但是在实际的硬件中,乘法被分为了几个操作,并且,这几个操作之间是可以并行的。所以,利用这个性质,可以提高乘法的速度。例如,如果一个乘法器有三个步骤,每个步骤占一个时钟周期。要计算下面一段代码。

long mult_eg(long a, long b, long c){
    long p1 = a * b;
    long p2 = a * c;
    long p3 = p1 * p2;
    return p3;
}

采取流水线,可以得到下面的顺序。

乘法流水线计算过程

可以看出,通过流水线,我们可以把本来要9个时钟周期才能完成的计算降低到了7个时钟周期。但我们上面的代码没有办法使用流水线,因为每一次循环,我们需要前一次计算的结果。为了使用流水线,我们把代码修改为下面的形式。

void unroll2aa_combine(vec_ptr v, data_t *dest){
    long length = vec_length(v);
    long limit = length - 1;
    data_t *d = get_vec_start(v);
    data_t x = IDENT;
    long i; /* Combine 2 elements at a time */
    for (i = 0; i < limit; i += 2)
    {
        x = x OP(d[i] OP d[i + 1]);
    } /* Finish any remaining elements */
    for (; i < length; i++)
    {
        x = x OP d[i];
    }
    *dest = x;
}

多个累积变量

上面的代码只使用了一个变量累积,感觉上,如果使用多个变量来累积,会提高运算速度,我们可以把代码修改为下面的版本。

void unroll2a_combine(vec_ptr v, data_t *dest){
    long length = vec_length(v);
    long limit = length - 1;
    data_t *d = get_vec_start(v);
    data_t x0 = IDENT;
    data_t x1 = IDENT;
    long i; /* Combine 2 elements at a time */
    for (i = 0; i < limit; i += 2){
        x0 = x0 OP d[i];
        x1 = x1 OP d[i + 1];
    } /* Finish any remaining elements */
    for (; i < length; i++){
        x0 = x0 OP d[i];
    }
    *dest = x0 OP x1;
}

下面给出测试结果。

测试结果

其中,unroll 2*1这个程序没有写出来,因为它和前面一样,也不能利用流水线,因为每一次乘法都依赖于前面计算的结果。下面两行表示延迟界限,这个界限是因为一系列的操作需要严格顺序执行。吞吐量界限表示处理器功能单元的原始计算能力,界限是程序性能的终极限制。例如,乘法的延迟界限是3,因为计算一个乘法需要3个时钟周期;而乘法的吞吐量限制是1,因为乘法使用了3级的流水线,可以认为,满载的时候,一个时钟周期可以完成一个乘法。

使用SIMD指令

SIMD是“Single-Instruction,Multiple-Data(单指令多数据)”的缩写。SIMD执行模型是用单条指令对整个向量数据进行操作。这些向量保存在一组特殊的向量寄存器中,名字为%ymm0~%ymm15。目前AVX向量寄存器长为32字节,因此每一个都可以存放8个32位数或4个64位数,这些数据即可以是整数也可以是浮点数。AVX指令可以对这些寄存器执行向量操作,比如并行执行8组数值或4组数值的加法或乘法。使用SIMD,可以大幅度提升程序的性能,下面是测试结果。

使用SIMD指令

可以看出,使用了SIMD对程序性能的提升非常大。

分支预测

现在处理器在工业界称为超标量,意思是它可以在每个时钟周期执行多个操作,而且是乱序的,意思是指令执行的顺序不一定要与它们在机器级程序中的顺序一致。

所以,对于分支的处理,在一个使用投机执行的处理器中,处理器会开始执行预测的分支目标处的指令。它会避免修改任何实际的寄存器或内存位置,知道确定了实际的结果。如果预测正确,那么处理器就会“提交”投机执行的的指令的结果,把它们存储到寄存器或内存。如果预测错误,处理器会丢掉所有投机执行的结果,在正确的位置,重新开始取指令的过程。这样做会引起预测错误处罚,因为在产生有用的结果之前,必须重新填充指令流水线。

第五章大致的内容就是这些,在CSAPP的课程中,只用了一节课讲授这里的内容。这节课打开了我的视野,第一点是没有想到现在处理器如此复杂;第二点是通过一系列的优化,可以对执行速度提升这么多,如加法的PCE从1.27变化到0.06。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容

  • 阅读经典——《深入理解计算机系统》07 本文将介绍非常实用的程序性能优化手段,并用一个案例来详细说明。 为什么要优...
    金戈大王阅读 3,733评论 2 23
  • 个人觉得虽然没必要过度优化,现代编译器在优化方面已经做得比较好了,但是在有些方面,还是需要程序员主动去做的。 ...
    索马里的海带阅读 294评论 0 0
  • 优化程序性能 编写高效程序需要做到如下几点1.选择适当的算法和数据结构。 2.编写出编译器能够有效优化以转换成高效...
    Android征途阅读 528评论 0 2
  • 编写高效程序需要做到以下几点:1.选择适当的算法和数据结构。2.编写出能有效优化以转化成高效可执行代码的编译器。 ...
    Xavier_NZX阅读 281评论 0 0
  • 作用 likely unlikely是为编译器提供对分支优化的提示,基本用于if-else的分支优化场景。if-e...
    wywindz阅读 3,009评论 0 0