优化程序性能的几个方法(来自于《深入理解计算机系统》)

简介:

这部分的代码出自《深入理解计算机系统》(CS:APP)第五章,其目的是通过手工改变代码结构,而不是算法效率和数据结构优化,提高执行效率。有些编译器在某些优化选项下可能会做出类似的改动。

  为了便于以后的查阅和使用,本文进行了摘录和简要分析,其中包含了一些个人理解。对于更深层次的原理如汇编、处理器结构等请参考原书。

  大致地,越靠后的代码性能越好,版本6和7性能近似,版本6略好一些。二者均能达到版本1性能的10倍左右。

  示例演示对于一个向量的所有元素完成一个运算。这个运算可以是所有元素的累加

#define IDENT 0
#define OP +

  或这所有元素的累计乘积 

#define IDENT 1
#define OP *

 

  data_t代表一种数据类型,在这个示例中可以是int、float、double

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

   对于vec_rec,有以下操作

创建向量

vec_ptr new_vec(long int len)
{
    /* Allocate header structure */
    vec_ptr result = (vec_ptr) malloc(sizeof(vec_rec));
    if (!result)
        return NULL; /* Couldn’t allocate storage */
    result->len = len;
    /* Allocate array */
    if (len > 0) {
        data_t *data = (data_t *)calloc(len, sizeof(data_t));
        if (!data) {
            free((void *) result);
            return NULL; /* Couldn’t allocate storage */
        }
        result->data = data;
    }
    else
        result->data = NULL;
    return result;
}
vec_ptr new_vec(long int len)

 

根据索引号获取向量元素

int get_vec_element(vec_ptr v, long int index, data_t *dest)
{
    if (index<0||index >= v->len)
        return 0;
    *dest = v->data[index];
    return 1;
}
int get_vec_element(vec_ptr v, long int index, data_t *dest)

 

获得向量长度

long int vec_length(vec_ptr v)
{
    return v->len;
}
long int vec_length(vec_ptr v)

 

用红色标记各个版本的改变。

 


版本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;
    }
}
复制代码

  如果不考虑错误的参数传递,这是一个可用版本,有着巨大的优化空间。

 


 

版本2:消除循环的低效率

  并非不使用循环,而是将循环中每次都需要重新计算但实际并不会发生改变的量用常量代替。本例中,这个量是向量v的长度。类似的还有使用strlen(s)求得的字符串长度。

  显然,如果这个量在每次循环时都会被改变,是不适用的。

复制代码
void combine2(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    *dest = IDENT;
    for (i = 0; i < length; i++) {
        data_t val;
        get_vec_element(v, i, &val);
        *dest = *dest OP val;
    }
}
复制代码

 


 

版本3:减少过程调用

  也即减少函数调用。通过对汇编代码的学习,可以知道函数调用是需要一些额外的开销的,包括建栈、传参,以及返回。通过把访问元素的函数直接扩展到代码中可以避免这个开销。但是这样做牺牲了代码的模块性和抽象性,对这种改变编写文档是一种折衷的补救措施。

  在这里,是通过把get_vec_element()展开来达到这个目的的,这需要编程人员对这个数据结构的细节有所了解,增加了阅读和改进代码的难度。

  更通用地方法是使用内联函数inline,可以兼顾模块性和抽象性。

复制代码
void combine3(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    data_t *data = get_vec_start(v);
    *dest = IDENT;
    for (i = 0; i < length; i++) {
        *dest = *dest OP data[i];
    }
}
复制代码

 


 

版本4:消除不必要的存储器引用

  在对于vec_rec结构操作时,其中间结果*dest是存放在存储器中的,每次取取值和更新都需要对存储器进行load或store, 要慢于对寄存器的操作。如果使用寄存器来保存中间结果,可以减少这个开销。

  这个中间结果,可以通过register显式声明为寄存器变量。不过下面的代码经过汇编后可以发现acc是存放在寄存器中的,不必显示地声明。

  然而,中间变量并非越多越好。原书5.11.1节展示了一个情形,如果同时使用的中间变量数过多,会出现“寄存器溢出”现象,部分中间变量仍然需要通过存储器保存。同理,多于机器支持能力的register声明的变量并不一定全部使用了寄存器来保存。

复制代码
void combine4(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    data_t *data = get_vec_start(v);
    data_t acc = IDENT;
    for (i = 0; i < length; i++) {
        acc = acc OP data[i];
    }
    *dest = acc;
}
复制代码

 


版本5:循环展开

  这个优化措施利用了对CPU数据流的知识,比汇编代码更接近机器底层。简单地说是利用了CPU的并行性,将数据分成不相关的部分并行地处理。版本5~7的更多细节和原理可以参考原书。类似的原理在练习题5.5和5.6中展示了为什么Horner法比一般的多项式求值的运算次数少,反而更慢的原因。

  展开的次数可以根据情况而定,下面的代码只展开了两次。对于未处理的部分元素,不能遗漏。

  gcc可以通过-funroll-loops选项执行循环展开。

复制代码
void combine5(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    long int limit = length-1;
    data_t *data = get_vec_start(v);
    data_t acc = IDENT;
    /* Combine 2 elements at a time */
    for (i = 0; i < limit; i+=2) {
        acc = (acc OP data[i]) OP data[i+1];
    }

    /* Finish any remaining elements */
    for (; i < length; i++) {
      acc = acc OP data[i];
    }
    *dest = acc;
}
复制代码

 


 

版本6与版本7:提高并行性

  和版本5的思想类似,但由于并行化更高,性能更好一些,充分利用了向量中各个元素的不相关性。

  版本6使用多个累积变量方法。

复制代码
/* Unroll loop by 2, 2-way parallelism */
void combine6(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    long int limit = length-1;
    data_t *data = get_vec_start(v);
    data_t acc0 = IDENT;
    data_t acc1 = IDENT;

    /* Combine 2 elements at a time */
    for (i = 0; i < limit; i+=2) {
    acc0 = acc0 OP data[i];
    acc1 = acc1 OP data[i+1];
    }
    /* Finish any remaining elements */
    for (; i < length; i++) {
    acc0 = acc0 OP data[i];
    }

*dest = acc0 OP acc1;
}
复制代码

 

  版本7是在版本5的基础上打破顺序相关,改变了并行执行的操作数量。

复制代码
void combine7(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    long int limit = length-1;
    data_t *data = get_vec_start(v);
    data_t acc = IDENT;

    /* Combine 2 elements at a time */
    for (i = 0; i < limit; i+=2) {
        acc = acc OP (data[i] OP data[i+1]);
    //in combine5:
//acc =
( acc OP data[i]) OP data[i+1];
    }

    /* Finish any remaining elements */
    for (; i < length; i++) {
        acc = acc OP data[i];
    }
    *dest = acc;
}
复制代码

 


补充说明

  这个示例中没有提到的改进方法还有:书写适合条件传送实现的代码,下面是原书的两段用于对比的代码,后者更适合条件传送实现。

复制代码
void minmax1(int a[], int b[], int n) {
    int i;
    for(i=0;i<n; i++) {
        if (a[i] > b[i]) {
            int t = a[i];
            a[i] = b[i];
            b[i] = t;
        }
    }
}

/* Rearrange two vectors so that for each i, b[i] >= a[i] */
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;
    }
}
复制代码

 

 

 

 





本文转自五岳博客园博客,原文链接:http://www.cnblogs.com/wuyuegb2312/p/3656183.html,如需转载请自行联系原作者

目录
相关文章
|
2月前
|
Linux 编译器 C++
C/C++性能优化:从根本上消除拷贝操作的浪费
C/C++性能优化:从根本上消除拷贝操作的浪费
56 0
|
2月前
|
存储 算法 C++
程序性能分析
1. 什么是程序性能 程序性能指的是程序在执行过程中所消耗的时间和资源的多少。一个好的程序应该能够在较短的时间内完成所需的任务,并且尽可能地利用少量的资源。
23 3
|
2月前
|
监控 Java 编译器
Go语言内存与并发性能综合优化策略
【2月更文挑战第11天】Go语言以其高效的并发处理能力和简洁的内存管理机制成为了现代软件开发中的热门选择。然而,在实际应用中,如何综合优化Go程序的内存使用和并发性能,仍然是一个值得探讨的话题。本文将深入探讨Go语言内存与并发性能的综合优化策略,包括内存布局优化、并发模式设计、资源池化以及性能监控与分析等方面,旨在帮助开发者全面提升Go程序的整体性能。
|
4月前
|
监控 前端开发 UED
应用程序性能
应用程序性能
|
4月前
|
存储 算法 Java
内存管理探秘:自动化与性能的完美平衡
内存管理探秘:自动化与性能的完美平衡
35 0
|
6月前
|
存储 缓存 算法
深入理解内存管理:优化你的C++代码
深入理解内存管理:优化你的C++代码
|
8月前
|
机器学习/深度学习 缓存 Linux
很底层的性能优化:让CPU更快地执行你的代码
很底层的性能优化:让CPU更快地执行你的代码
|
程序员
【编程】程序的局部性原理对代码效率的影响
【编程】程序的局部性原理对代码效率的影响
85 0
|
Java 数据库 图形学
论系统的木桶理论与性能瓶颈
在我们实际开发环境中,根据木桶理论,系统的最终性能取决于系统中性能表现最差的组件,因此为了提高整体系统性能,必须对系统中表现最差的组件进行优化,而不是对表现良好的组件进行优化。 根据应用的特点不同,任何计算机资源都i有可能成为系统瓶颈,其中最有可能成为瓶颈的计算资源如下。
|
人工智能 缓存 算法
解读《深入理解计算机系统(CSAPP)》第5章优化程序性能
程序优化涉及的范围,比如如何撰写的程序,针对不同硬件平台可能进行特定的优化等等,优化的难点在于你需要对系统有充分理解,那么如何优化是本章讨论的重点。
解读《深入理解计算机系统(CSAPP)》第5章优化程序性能

相关实验场景

更多