值得收藏的9个提高代码运行效率的小技巧(推荐)

我们写程序的目的就是使它在任何情况下都可以稳定工作。一个运行的很快但是结果错误的程序并没有任何用处。在程序开发和优化的过程中,我们必须考虑代码使用的方式,以及影响它的关键因素。通常,我们必须在程序的简洁性与它的运行速度之间做出权衡。今天我们就来聊一聊如何优化程序的性能。

1. 减小程序计算量

1.1 示例代码

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

1.2 分析代码

  代码如上所示,外循环每执行一次,我们要进行一次乘法计算。i = 0,ni = 0;i = 1,ni = n;i = 2,ni = 2n。因此,我们可以把乘法换成加法,以n为步长,这样就减小了外循环的代码量。

1.3 改进代码

int ni = 0;
for (i = 0; i < n; i++) {
  for (j = 0; j < n; j++)
    a[ni + j] = b[j];
  ni += n;         //乘法改加法
}

计算机中加法指令要比乘法指令快得多。

2. 提取代码中的公共部分

2.1 示例代码

  想象一下,我们有一个图像,我们把图像表示为二维数组,数组元素代表像素点。我们想要得到给定像素的东、南、西、北四个邻居的总和。并求他们的平均值或他们的和。代码如下所示。

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;

2.2 分析代码

  将以上代码编译后得到汇编代码如下所示,注意下3,4,5行,有三个乘以n的乘法运算。我们把上面的up和down展开后会发现四格表达式中都有i*n + j。因此,可以提取出公共部分,再通过加减运算分别得出up、down等的值。

leaq   1(%rsi), %rax  # i+1
leaq   -1(%rsi), %r8  # i-1
imulq  %rcx, %rsi     # i*n
imulq  %rcx, %rax     # (i+1)*n
imulq  %rcx, %r8      # (i-1)*n
addq   %rdx, %rsi     # i*n+j
addq   %rdx, %rax     # (i+1)*n+j
addq   %rdx, %r8      # (i-1)*n+j

2.3 改进代码

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;

  改进后的代码的汇编如下所示。编译后只有一个乘法。减少了6个时钟周期(一个乘法周期大约为3个时钟周期)。

imulq	%rcx, %rsi  # i*n
addq	%rdx, %rsi  # i*n+j
movq	%rsi, %rax  # i*n+j
subq	%rcx, %rax  # i*n+j-n
leaq	(%rsi,%rcx), %rcx # i*n+j+n
...

  对于GCC编译器来说,编译器可以根据不同的优化等级,有不同的优化方式,会自动完成以上的优化操作。下面我们介绍下,那些必须是我们要手动优化的。

3. 消除循环中低效代码

3.1 示例代码

  程序看起来没什么问题,一个很平常的大小写转换的代码,但是为什么随着字符串输入长度的变长,代码的执行时间会呈指数式增长呢?

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

3.2 分析代码

  那么我们就测试下代码,输入一系列字符串。

  当输入字符串长度低于100000时,程序运行时间差别不大。但是,随着字符串长度的增加,程序的运行时间呈指数时增长。

  我们把代码转换成goto形式看下。

void lower1(char *s)
{
   size_t i = 0;
   if (i >= strlen(s))
     goto done;
 loop:
   if (s[i] >= 'A' && s[i] <= 'Z')
       s[i] -= ('A' - 'a');
   i++;
   if (i < strlen(s))
     goto loop;
 done:
}

  以上代码分为初始化(第3行),测试(第4行),更新(第9,10行)三部分。初始化只会执行一次。但是测试和更新每次都会执行。每进行一次循环,都会对strlen调用一次。

  下面我们看下strlen函数的源码是如何计算字符串长度的。

size_t strlen(const char *s)
{
    size_t length = 0;
    while (*s != '\0') {
	s++;
	length++;
    }
    return length;
}

  strlen函数计算字符串长度的原理为:遍历字符串,直到遇到‘\0'才会停止。因此,strlen函数的时间复杂度为O(N)。lower1中,对于长度为N的字符串来说,strlen 的调用次数为N,N-1,N-2 … 1。对于一个线性时间的函数调用N次,其时间复杂度接近于O(N2)。

3.3 改进代码

  对于循环中出现的这种冗余调用,我们可以将其移动到循环外。将计算结果用于循环中。改进后的代码如下所示。

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

  将两个函数对比下,如下图所示。lower2函数的执行时间得到明显提升。

4. 消除不必要的内存引用

4.1 示例代码

  以下代码作用为,计算a数组中每一行所有元素的和存在b[i]中。

void sum_rows1(double *a, double *b, long n) {
    long i, j;
    for (i = 0; i < n; i++) {
	b[i] = 0;
	for (j = 0; j < n; j++)
	    b[i] += a[i*n + j];
    }
}

4.2 分析代码

  汇编代码如下所示。

# sum_rows1 inner loop
.L4:
        movsd   (%rsi,%rax,8), %xmm0	# 从内存中读取某个值放到%xmm0
        addsd   (%rdi), %xmm0		    # %xmm0 加上某个值
        movsd   %xmm0, (%rsi,%rax,8)	# %xmm0 的值写回内存,其实就是b[i]
        addq    $8, %rdi
        cmpq    %rcx, %rdi
        jne     .L4

  这意味着每次循环都需要从内存中读取b[i],然后再把b[i]写回内存 。 b[i] += b[i] + a[i*n + j]; 其实每次循环开始的时候,b[i]就是上一次的值。为什么每次都要从内存中读取出来再写回呢?

4.3 改进代码

/* Sum rows is of n X n matrix a
   and store in vector b  */
void sum_rows2(double *a, double *b, long n) {
    long i, j;
    for (i = 0; i < n; i++) {
	double val = 0;
	for (j = 0; j < n; j++)
	    val += a[i*n + j];
         b[i] = val;
    }
}

  汇编如下所示。

# sum_rows2 inner loop
.L10:
        addsd   (%rdi), %xmm0	# FP load + add
        addq    $8, %rdi
        cmpq    %rax, %rdi
        jne     .L10

  改进后的代码引入了临时变量来保存中间结果,只有在最后的值计算出来时,才将结果存放到数组或全局变量中。

5. 减小不必要的调用

5.1 示例代码

  为了方便举例,我们定义一个包含数组和数组长度的结构体,主要是为了防止数组访问越界,data_t可以是int,long等类型。具体如下所示。

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

  get_vec_element函数的作用是遍历data数组中元素并存储在val中。

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 = NULL;
    for (i = 0; i < vec_length(v); i++) {
	data_t val;
	get_vec_element(v, i, &val);
	*dest = *dest * val;
    }
}

5.2 分析代码

  get_vec_element函数的作用是获取下一个元素,在get_vec_element函数中,每次循环都要与v->len作比较,防止越界。进行边界检查是个好习惯,但是每次都进行就会造成效率降低。

5.3 改进代码

  我们可以把求向量长度的代码移到循环体外,同时抽象数据类型增加一个函数get_vec_start。这个函数返回数组的起始地址。这样在循环体中就没有了函数调用,而是直接访问数组。

data_t *get_vec_start(vec_ptr v)
{
	return v->data;
}

void combine2 (vec_ptr v, data_t *dest)
{
	long i;
	long length  = vec_length(v);
    data_t *data = get_vec_start(v);
	*dest = NULL;
	for (i=0;i < length;i++)
	{
		*dest = *dest * data[i];
	}
}

6. 循环展开

6.1 示例代码

  我们在combine2的代码上进行改进。

6.2 分析代码

  循环展开是通过增加每次迭代计算的元素的数量减少循环的迭代次数

6.3 改进代码

void combine3(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 acc = NULL;

    /* 一次循环处理两个元素 */
    for (i = 0; i < limit; i+=2) {
		acc = (acc * data[i]) * data[i+1];
    }
    /*     完成剩余数组元素的计算    */
    for (; i < length; i++) {
		acc = acc * data[i];
    }
    *dest = acc;
}

  在改进后的代码中,第一个循环每次处理数组的两个元素。也就是每次迭代,循环索引i加2,在一次迭代中,对数组元素i和i+1使用合并运算。一般我们称这种为2×1循环展开,这种变换能减小循环开销的影响。

注意访问不要越界,正确设置limit,n个元素,一般设置界限n-1

7. 累计变量,多路并行

7.1 示例代码

  我们在combine3的代码上进行改进。

7.2 分析代码

  对于一个可结合和可交换的合并运算来说,比如说整数加法或乘法,我们可以通过将一组合并运算分割成两个或更多的部分,并在最后合并结果来提高性能。

特别注意:不要轻易对浮点数进行结合。浮点数的编码格式和其他整型数等都不一样。

7.3 改进代码

void combine4(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 = 0;
    data_t acc1 = 0;

    /* 循环展开,并维护两个累计变量 */
    for (i = 0; i < limit; i+=2) {
		acc0 = acc0 * data[i];
		acc1 = acc1 * data[i+1];
    }
    /*     完成剩余数组元素的计算    */
    for (; i < length; i++) {
        acc0 = acc0 * data[i];
    }
    *dest = acc0 * acc1;
}

  上述代码用了两次循环展开,以使每次迭代合并更多的元素,也使用了两路并行,将索引值为偶数的元素累积在变量acc0中,而索引值为奇数的元素累积在变量acc1中。因此,我们将其称为”2×2循环展开”。运用2×2循环展开。通过维护多个累积变量,这种方法利用了多个功能单元以及它们的流水线能力

8. 重新结合变换

8.1 示例代码

  我们在combine3的代码上进行改进。

8.2 分析代码

  到这里其实代码的性能已经基本接近极限了,就算做再多的循环展开性能提升已经不明显了。我们需要换个思路,注意下combine3代码中第12行的代码,我们可以改变下向量元素合并的顺序(浮点数不适用)。重新结合前combine3代码的关键路径如下图所示。

8.3 改进代码

void combine7(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 acc = IDENT;

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

  重新结合变换能够减少计算中关键路径上操作的数量,这种方法增加了可以并行执行的操作数量了,更好地利用功能单元的流水线能力得到更好的性能。重新结合后关键路径如下所示。

9 条件传送风格的代码

9.1 示例代码

void minmax1(long a[],long b[],long n){
	long i;
	for(i = 0;i,n;i++){
        if(a[i]>b[i]){
            long t = a[i];
            a[i] = b[i];
            b[i] = t;
        }
   }
}

9.2 分析代码

  现代处理器的流水线性能使得处理器的工作远远超前于当前正在执行的指令。处理器中的分支预测在遇到比较指令时会进行预测下一步跳转到哪里。如果预测错误,就要重新回到分支跳转的原地。分支预测错误会严重影响程序的执行效率。因此,我们应该编写让处理器预测准确率提高的代码,即使用条件传送指令。我们用条件操作来计算值,然后用这些值来更新程序状态,具体如改进后的代码所示。

9.3 改进代码

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

  在原代码的第4行中,需要对a[i]和b[i]进行比较,再进行下一步操作,这样的后果是每次都要进行预测。改进后的代码实现这个函数是计算每个位置i的最大值和最小值,然后将这些值分别赋给a[i]和b[i],而不是进行分支预测。

10. 总结

  我们介绍了几种提高代码效率的技巧,有些是编译器可以自动优化的,有些是需要我们自己实现的。现总结如下。

消除连续的函数调用。在可能时,将计算移到循环外。考虑有选择地妥协程序的模块性以获得更大的效率。

消除不必要的内存引用。引入临时变量来保存中间结果。只有在最后的值计算出来时,才将结果存放到数组或全局变量中。

展开循环,降低开销,并且使得进一步的优化成为可能。

通过使用例如多个累积变量和重新结合等技术,找到方法 提高指令级并行。

用功能性的风格重写条件操作,使得编译采用条件数据传送。

到此这篇关于值得收藏的9个提高代码运行效率的小技巧(推荐)的文章就介绍到这了,更多相关提高代码运行效率内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java groovy如何提升代码运行效率

    刚开始学groovy,知道了它会先变异成class 文件,然后再用jvm 执行.写了Hello World程序,查看它的编译文件,发现groovy的效率挺低的.不但编译文件的代码多,而且需要依赖很多groovy包,导致了不能够直接使用java 命令运行class文件 比较如下: Java版Hello World,JavaTest.java public class JavaTest { public static void main(String[] args){ System.out.prin

  • java与C 代码运行效率的对比(整理)

    1.Java 语言的概述 作为一种面向对象的程序设计语言,Java 与 C++极为 类似,但却要比 C++简单的多.它在集成其他语言的特点 和优势的同时又有自己独特的优势. Java 的主要特点如下: (1)简单性.Java 可以对内存中产生的垃圾进行自动收集, 大幅度降低了程序的复杂程度,此外,Java 添加了更为实 用的功能的,这使得程序开发更加简单可靠. (2)平台独 立性.Java 语言在程序编程过程中是先编译成中间码,然 后再进行装载与校验,最后通过翻译出来的不同的机器码 来执行.因此

  • 提高python代码运行效率的一些建议

    1. 优化代码和算法 一定要先好好看看你的代码和算法.许多速度问题可以通过实现更好的算法或添加缓存来解决.本文所述都是关于这一主题的,但要遵循的一些一般指导方针是: 测量,不要猜测. 测量代码中哪些部分运行时间最长,先把重点放在那些部分上. 实现缓存. 如果你从磁盘.网络和数据库执行多次重复的查找,这可能是一个很大的优化之处. 重用对象,而不是在每次迭代中创建一个新对象.Python 必须清理你创建的每个对象才能释放内存,这就是所谓的"垃圾回收".许多未使用对象的垃圾回收会大大降低软件

  • JavaScript查看代码运行效率console.time()与console.timeEnd()用法

    程序运行时间计算: 注意:测试代码运行时间的话console.time()与console.timeEnd()要一起使用,这是一个组合:这两个之间的代码就是要运行测试程序的时间 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body>

  • 50个PHP程序性能优化的方法

    1. 用单引号代替双引号来包含字符串,这样做会更快一些.因为 PHP 会在双引号包围的 字符串中搜寻变量,单引号则不会,注意:只有 echo 能这么做,它是一种可以把多个字符 串当作参数的"函数"(译注:PHP 手册中说 echo 是语言结构,不是真正的函数,故把函数 加上了双引号). 2.如果能将类的方法定义成 static,就尽量定义成 static,它的速度会提升将近 4 倍. 3.$row['id'] 的速度是$row[id]的 7 倍. 4.echo 比 print 快,并且

  • 值得收藏的9个提高代码运行效率的小技巧(推荐)

    我们写程序的目的就是使它在任何情况下都可以稳定工作.一个运行的很快但是结果错误的程序并没有任何用处.在程序开发和优化的过程中,我们必须考虑代码使用的方式,以及影响它的关键因素.通常,我们必须在程序的简洁性与它的运行速度之间做出权衡.今天我们就来聊一聊如何优化程序的性能. 1. 减小程序计算量 1.1 示例代码 for (i = 0; i < n; i++) { int ni = n*i; for (j = 0; j < n; j++) a[ni + j] = b[j]; } 1.2 分析代码

  • C/C++中提高查找速度的小技巧

    前言 当看到题目是在一个数组中查找某一个元素,或是在一个字符串中查找某个字符,我们一般都会写出如下代码.但这样的代码虽然简洁明了,但在数组元素很多的情况下,并不是一个很好的解决方案,今天我就来分享一个提高查找速度的小技巧. //在一个int数组中查找某个元素 int find(int A[],int n,int element) { for( int i = 0; i < n; i++ ) { if( A[i] == element ) return i; } return -1; } //在一

  • 提高iOS开发的小技巧和思路小结 (二)

    前言 之前跟大家分享过一篇<提高iOS开发的小技巧和思路>,但是里面整理的不够全面,通过最近这段时间的整理学习,又整理了一些小技巧和思路分享给大家,下面话不多说了,来看看详细的介绍吧. 一.如何让你的app在后台持续运行一段时间 iOS的app在按下home键的时候,只有不到五秒钟的时间去处理保存或者是清楚资源的工作,这里提供一种办法.(这是在唐巧的书上面学习的) 产品经理的需求:如果app的本地持久化的额图片超过了100mb,需要去清除. 我之前的做法是在程序启动的时候去做这个事情,很显然效

  • 9个提高 Python 编程的小技巧

    目录 01 交换变量 02 字典推导和集合推导 03 计数时使用Counter计数对象 04 漂亮的打印出JSON 05 解决FizzBuzz 06 连接 07 数值比较 08 同时迭代两个列表 09 带索引的列表迭代 前言: 初识Python语言,觉得python满足了你上学时候对编程语言的所有要求.python语言的高效编程技巧让那些曾经苦逼学了四年c或者c++的人,兴奋的不行不行的,终于解脱了.高级语言,如果做不到这样,还扯啥高级呢? 01 交换变量 >>>a=3 >>

  • 编程人员阅读代码的一些小技巧分享

    代码阅读的必要性 阅读别人的代码作为研发人员是一件经常要做的事情.一个是学习新的编程语言的时候通过阅读别人的代码是个最佳的学习方法,另外是积累编程经验.如果你有机 会阅读一些操作系统的代码会帮助你理解一些基本的原理.更有就是在你作为一个质量确保人员或一个小领导的时候如果你要做白盒测试的时候没有阅读代码的能力 是不能完成相应的任务.最后一个就是如果你中途接手一个项目的时候或给一个项目做售后服务的时候是要有阅读代码的能力的. 收集所有可能收集的材料 阅读代码要做的第一件事情是收集所有和项目相关的资料

  • 值得收藏的iOS开发常用代码块

    遍历可变数组的同时删除数组元素 NSMutableArray *copyArray = [NSMutableArray arrayWithArray:array]; NSString *str1 = @"zhangsan"; for (AddressPerson *perName in copyArray) { if ([[perName name] isEqualToString:str1]) { [array removeObject:perName]; } } 获取系统当前语言

  • javascript两段代码,两个小技巧

    第一段代码就是强调一下这个用法,我在我的项目中使用了一个switch,后来我发现这样的代码好丑,于是我就写成||&&形式的, 后来测试性能的时候,发现性能竟然上了一个数量级,可见这种写法在某些情况下可以增加性能,但是我并不确定是何种情况才能提高性能,因为我测试在通常情况下switch和||&&的性能是差不多的. 原来的代码: 复制代码 代码如下: switch(this.now_char=this.str.charAt(this.index)){ case "/&

  • 提高域名信任度的8个小技巧[推荐]

    也有几个简单可以做到的方法,不妨一试. 1)提供完整的联系方法 包括电话和真正地址. 2)在没有大量好的内容之前,至少也要把网站做到四五十页以上 尽量原创,没有就转载. 3)链接向几个同行业内的权威性网站 不要多,而是精. 4)把域名多注册几年,而不是每年续费 域名的Whois数据和网站上的联系方式吻合,这两点在Google的一个专利申请中曾经提到过. 5)使用独立IP地址(没钱就算了) 绝大部分网站都是在虚拟主机上共用一个IP地址.不过有的人建议,独立IP地址是高质量网站的一个标志,这也很符合

  • Xcode中代码注释编写的一些小技巧

    目录 前言 Objective-C的代码注释 Swift的代码注释 Objective-C和Swift的注释风格现在已经统一 快速修改注释 参考文档 总结 前言 码农总是在搬砖,日复一日,年复一年,有的时候都会麻木. 代码大家都会写,但是把注释写好却是一个技术活. 下面这段话,很好的说明了写好注释的感觉: 注释代码很像清洁你的厕所--你不想干,但如果你做了,这绝对会给你和你的客人带来更愉悦的体验.-- Ryan Campbell 今天给大家聊的就是在Xcode中,代码注释编写小技巧. Objec

随机推荐