0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何才能提高代码运行效率?

Linux爱好者 来源:嵌入式与Linux那些事 作者:嵌入式那些事 2021-05-10 17:43 次阅读

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

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

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

ab7e19f0-b0bf-11eb-bf61-12bb97331649.png

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 != ‘’) {

s++;

length++;

}

return length;

}

strlen函数计算字符串长度的原理为:遍历字符串,直到遇到‘’才会停止。因此,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函数的执行时间得到明显提升。

ab86c780-b0bf-11eb-bf61-12bb97331649.png

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 # 从内存中读取某个值放到%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;

ab91f240-b0bf-11eb-bf61-12bb97331649.png

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代码的关键路径如下图所示。

ab9c4ad8-b0bf-11eb-bf61-12bb97331649.png

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;

}

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

aba84d92-b0bf-11eb-bf61-12bb97331649.png

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

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

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

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

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

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

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

原文标题:这几个提高代码运行效率的小技巧我一直在用

文章出处:【微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

责任编辑:haq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 代码
    +关注

    关注

    30

    文章

    4841

    浏览量

    69225

原文标题:这几个提高代码运行效率的小技巧我一直在用

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何提高嵌入式代码质量?

    嵌入式代码的质量是至关重要的。本文将探讨如何通过有效的开发方法和工具来提高嵌入式代码的质量,以确保系统的可靠性和可维护性。 理解嵌入式系统的特点和需求 嵌入式系统与传统的桌面应用程序或服务器软件
    发表于 01-15 10:48

    如何提高SMT生产效率

    在竞争激烈的电子制造领域,SMT生产线的效率直接影响到企业的竞争力。 1. 生产流程优化 1.1 精益生产 精益生产是一种旨在减少浪费、提高效率的生产管理方法。通过识别和消除生产过程中的非增值活动
    的头像 发表于 01-10 16:28 477次阅读

    如何提高poe供电效率

    提高PoE(Power over Ethernet,以太网供电)供电效率是一个涉及多个方面的综合性问题。以下是一些具体的建议,旨在帮助提高PoE供电效率: 一、优化PoE设备选择与设计
    的头像 发表于 11-19 10:45 370次阅读

    如何提高CNC加工效率

    在当今竞争激烈的制造业环境中,提高CNC加工效率是企业保持竞争力的关键。CNC机床的效率不仅影响生产成本,还关系到产品质量和交货速度。 1. 优化CNC程序 1.1 减少空行程 CNC程序中的空行
    的头像 发表于 11-12 09:18 1031次阅读

    如何提高伺服驱动器的效率

    在现代工业自动化领域,伺服驱动器的效率对于整个系统的能效和性能至关重要。本文探讨了影响伺服驱动器效率的关键因素,并提出了一系列提高效率的策略,包括优化控制算法、改善硬件设计、采用先进的功率电子技术
    的头像 发表于 11-04 15:20 624次阅读

    如何优化智能系统的运行效率

    智能系统,无论是在工业自动化、智能家居还是个人设备中,都扮演着越来越重要的角色。随着技术的发展,用户对智能系统的期望也在不断提高,这要求系统必须具备更高的运行效率。 二、影响智能系统运行
    的头像 发表于 10-29 10:02 483次阅读

    MES系统如何提高生产效率

    在当今竞争激烈的制造行业中,提高生产效率是企业生存和发展的关键。MES系统作为一种先进的制造管理工具,已经成为许多制造企业提高生产效率的重要手段。 1. 实时监控与数据收集 MES系统
    的头像 发表于 10-27 09:16 428次阅读

    提高LLC转换器的ZVS和效率

    电子发烧友网站提供《提高LLC转换器的ZVS和效率.pdf》资料免费下载
    发表于 10-14 10:03 0次下载
    <b class='flag-5'>提高</b>LLC转换器的ZVS和<b class='flag-5'>效率</b>

    该如何提高代码容错率、降低代码耦合度?

    提高RT-Thread代码的容错率和降低耦合度是确保代码质量和可维护性的关键,下面列举了几种在编写代码时,提高
    的头像 发表于 06-26 08:10 769次阅读
    该如何<b class='flag-5'>提高</b><b class='flag-5'>代码</b>容错率、降低<b class='flag-5'>代码</b>耦合度?

    运动控制器的代码运行顺序是什么

    运动控制器是一种用于控制机械运动的设备,它可以接收输入信号并根据这些信号控制机械的运动。运动控制器的代码运行顺序对于实现精确的运动控制至关重要。本文将详细介绍运动控制器的代码运行顺序,
    的头像 发表于 06-13 09:25 563次阅读

    东莞mes系统:提高生产效率的利器

    东莞作为中国制造业的重要基地之一,拥有众多制造企业,其中不乏一些领先的MES系统供应商。这些 MES系统供应商 致力于为东莞的制造企业提供智能制造解决方案,帮助企业提高生产效率、降低生产成本、提升
    的头像 发表于 05-21 15:37 501次阅读

    如何提升代码质量与效率的秘诀

    提高编程能力其实没有捷径,最佳方式就是多写代码。 不过,除了写大量代码,提升编程能力还需要大量阅读别人写的代码
    的头像 发表于 04-28 14:53 481次阅读
    如何提升<b class='flag-5'>代码</b>质量与<b class='flag-5'>效率</b>的秘诀

    微机消谐装置提高了电网的运行效率

    在电网系统中,微机消谐装置的应用已经越来越广泛。这种装置的主要功能是通过微处理技术对电网中的谐波进行消除,以提高电网的稳定性和效率。微机消谐装置的应用,对电网系统产生了深远的影响。 首先,微机消谐
    的头像 发表于 04-02 14:40 447次阅读

    变压器运行效率最高的条件是什么

    变压器是用来改变交流电压的电气设备。在变压器的运行过程中,由于存在一些能量的损耗和效率的损失。为了有效的利用能源和提高变压器的运行效率,我们
    的头像 发表于 03-24 10:53 4063次阅读

    提高效率的DC电源模块设计技巧

    BOSHIDA  提高效率的DC电源模块设计技巧 设计高效率的BOSHIDA  DC电源模块可以帮助减少能源浪费和提高系统功耗,以下是一些设计技巧: 1. 选择高效率的功率转换器:选择
    的头像 发表于 02-26 14:27 648次阅读
    <b class='flag-5'>提高效率</b>的DC电源模块设计技巧