9个提高代码运行效率的小技巧你知道几个?

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

  • 1. 减小程序计算量

    • 1.1 示例代码

    • 1.2 分析代码

    • 1.3 改进代码

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

    • 2.1 示例代码

    • 2.2 分析代码

    • 2.3 改进代码

  • 3. 消除循环中低效代码

    • 3.1 示例代码

    • 3.2 分析代码

    • 3.3 改进代码

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

    • 4.1 示例代码

    • 4.2 分析代码

    • 4.3 改进代码

  • 5.  减小不必要的调用

    • 5.1 示例代码

    • 5.2 分析代码

    • 5.3 改进代码

  • 6. 循环展开

    • 6.1 示例代码

    • 6.2 分析代码

    • 6.3 改进代码

  • 7. 累计变量,多路并行

    • 7.1 示例代码

    • 7.2 分析代码

    • 7.3 改进代码

  • 8. 重新结合变换

    • 8.1 示例代码

    • 8.2 分析代码

    • 8.3 改进代码

  • 9 条件传送风格的代码

    • 9.1 示例代码

    • 9.2 分析代码

    • 9.3 改进代码

  • 10. 总结

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 分析代码

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

lower1代码性能测试

  当输入字符串长度低于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函数的执行时间得到明显提升。

lower1和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 # 从内存中读取某个值放到%xmm0addsd   (%rdi), %xmm0      # %xmm0 加上某个值movsd   %xmm0, (%rsi,%rax,8) # %xmm0 的值写回内存,其实就是b[i]addq    $8, %rdicmpq    %rcx, %rdijne     .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 aand 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 + addaddq    $8, %rdicmpq    %rax, %rdijne     .L10

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

5.  减小不必要的调用

5.1 示例代码

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

typedef struct{size_t len;data_t *data;  
} vec;
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代码的关键路径如下图所示。

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 OP (data[i] OP data[i+1]);}/* Finish any remaining elements */for (; i < length; i++) {acc = acc OP data[i];}*dest = acc;
}

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

combine3重新结合后关键路径

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],而不是进行分支预测。

推荐阅读:

专辑|Linux文章汇总

专辑|程序人生

专辑|C语言

我的知识小密圈

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/466603.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STM32——按键

STM32——按键 宗旨&#xff1a;技术的学习是有限的&#xff0c;分享的精神是无限的。 一、GPIO工作模式 1、当I/O端口配置为输入时&#xff1a; 输出缓冲器被禁止 施密特触发输入被激活 根据输入配置(上拉&#xff0c;下拉或浮动)的不同&#xff0c;弱上拉和下拉电阻被连接 …

深度学习——模型的压缩和加速

1. 简介 随着深度学习发展&#xff0c;越来越多的模型被发现和应用&#xff0c;模型的体量也越来越大&#xff0c;出现了模型过于庞大和参数冗余的问题。同时&#xff0c;移动端对模型的需求也是越轻量越好&#xff0c;因此&#xff0c;模型压缩和加速技术应运而生。 模型压缩…

干货,记一次解决录音杂音问题

最近在项目上遇到一个问题&#xff0c;也不能说是最近项目上的问题了&#xff0c;是之前一直存在的问题&#xff0c;但是对项目没什么影响&#xff0c;所以我就不怎么理会&#xff0c;直到最近&#xff0c;同事说这个杂音已经影响到了项目的开发&#xff0c;所以今天花了一天时…

3.5.2 冒泡排序类

那么&#xff0c;我们就以冒泡排序为例&#xff0c;把它改造成一个类。首先&#xff0c;单击菜单&#xff0c;“项目”&#xff0d;“添加类”&#xff0c;添加一个BubbleSort.cs类文件。IDE自动为我们创建如下代码&#xff1a; usingSystem;usingSystem.Collections.Generic;u…

STM32——串口通信

STM32——串口通信 宗旨&#xff1a;技术的学习是有限的&#xff0c;分享的精神是无限的。 一、异步串口通信协议 STM32 的串口非常强大&#xff0c;它不仅支持最基本的通用串口同步、异步通信&#xff0c;还具有 LIN 总线功能&#xff08;局域互联网&#xff09;、IRDA 功能&…

操作系统——死锁(银行家算法)

1、概述 1.1 死锁 死锁是多个进程因竞争资源而造成的一种僵局&#xff08;互相等待&#xff09;&#xff0c;若无外力作用&#xff0c;这些进程都将无法向前推进。 1.2 死锁产生的原因和条件 原因&#xff1a;&#xff08;1&#xff09;竞争资源&#xff1b;&#xff08;2&…

操作系统——内存管理

1、内存基本概念 1.1 主要功能 内存空间的分配与回收&#xff1b;地址转换内存保护&#xff1a;使用上下限寄存器或者重定位寄存器和界地址寄存器内存扩充&#xff1a;交换和覆盖内容共享 2、内存的分配与回收 2.1 连续分配方式 连续分配方式是指为一个用户程序分配一个连续…

解决一个驱动代码解耦合问题

之前解决的项目LCD设备兼容问题&#xff0c;在 a.c 文件里面定义了一个变量&#xff0c;然后在 b.c 里面使用 extern声明引用这个变量&#xff0c;通过这种方法可以在b.c中使用在a.c 里面初始化的变量。但是这中情况就会引起一个问题&#xff0c;就是驱动代码之间耦合了&#x…

STM32——DMA

STM32——DMA 宗旨&#xff1a;技术的学习是有限的&#xff0c;分享的精神是无限的。 DMA 是为CPU分担数据转移的工作。因为DMA的存在CPU才被解放出来&#xff0c;它可以在 DMA 转移数据的过程中同时进行数据运算、响应中断&#xff0c;大大提高效率。 1、DMA工作分析 数据传…

YOLOv8改进 | 主干篇 | 利用SENetV1改进网络结构 (ILSVRC冠军得主)

一、本文介绍 本文给大家带来的改进机制是SENet&#xff08;Squeeze-and-Excitation Networks&#xff09;其是一种通过调整卷积网络中的通道关系来提升性能的网络结构。SENet并不是一个独立的网络模型&#xff0c;而是一个可以和现有的任何一个模型相结合的模块(可以看作是一…

搭建Servlet在线视频

这个视频flash上传及在线播放&#xff0c;搞了我一天了&#xff0c;总算有点成果&#xff0c;但还有一些疑问没有解决&#xff0c;现在发这篇随笔&#xff0c;为的就是交流视频在线上传和观看的一些问题。 在线编辑器使用FCKEditor&#xff0c;首先是修改FCKEditor的配置文件&a…

操作系统——内存管理例题

1、关于分配策略例题 case1&#xff1a;某系统的空闲分区见下表&#xff0c;如有下列作业&#xff1a;96KB&#xff0c;20KB&#xff0c;200KB&#xff0c;分别采用首次适应算法和最佳适应算法来处理这些作业序列&#xff0c;哪种算法能满足该作业序列请求&#xff1f; 分区号…

STM32中C语言知识点:初学者必看,老鸟复习(长文总结)

说在前面的话一位初学单片机的小伙伴让我推荐C语言书籍&#xff0c;因为C语言基础比较差&#xff0c;想把C语言重新学一遍&#xff0c;再去学单片机&#xff0c;我以前刚学单片机的时候也有这样子的想法。其实C语言是可以边学单片机边学的&#xff0c;学单片机的一些例程中&…

STM32——ADC

STM32——ADC 宗旨&#xff1a;技术的学习是有限的&#xff0c;分享的精神是无限的。 一、ADC指标 有 18 个通道&#xff0c;可测量 16 个外部和 2 个内部信号源。各通道的 A/D 转换可以单次、连续、扫描或间断模式执行 &#xff1b;ADC的结果可以左对齐或右对齐方式存储在 16…

时间复杂度和空间复杂度,一看就懂,面试前必过一遍

一、定义时间和空间是程序的一个硬性指标&#xff0c;一个用来衡量 代码执行的速度 &#xff0c;一个用来衡量 存储空间的大小程序 数据结构 算法时间复杂度&#xff1a;就是执行程序的快慢&#xff0c;速度越快&#xff0c;时间复杂度就越好。空间复杂度&#xff1a;就是执…

数据结构——排序【仅用于考试】

1、简介 排序&#xff0c;是重新排列表中的元素&#xff0c;使表中的元素满足按关键字有序的过程 稳定性&#xff1a;选取两个元素Ri<Rj&#xff0c;经过排序算法之后&#xff0c;仍为Ri<Rj 不稳定的排序&#xff1a;【简单选择排序&#xff0c;快速排序&#xff0c;堆…

[UWP]做个调皮的BusyIndicator

1. 前言 最近突然想要个BusyIndicator。做过WPF开发的程序员对BusyIndicator应该不陌生&#xff0c;Extended WPF Toolkit 提供了BusyIndicator的开源实现&#xff0c;Silverlight Toolkit也有一个&#xff0c;这次想要把这个控件移植到UWP中。 2. 先说点正经的 2.1 BusyIndica…

STM32——I2C

STM32——I2C 宗旨&#xff1a;技术的学习是有限的&#xff0c;分享的精神是无限的。 一、I2C协议 I 2 C &#xff08;Inter-Integrated Circuit&#xff09;协议是由 Philips 公司开发的&#xff0c;由于它具备引脚少、硬件实现简单、可扩展性强、不需要如 USART、CAN 的外部…

C语言发展简史

1、起源C 语言最早的原型是 ALGOL 60 1963 年&#xff0c;剑桥大学将其发展成为 CPL(Combined Programing Language)。1967 年&#xff0c;剑桥大学的 Matin Richards 对 CPL 语言进行了简化&#xff0c;产生了 BCPL 语言。1970 年&#xff0c;美国贝尔实验室(Bell Labs)的 Ken…