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

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

3天内不再提示

9个提高代码运行效率的小技巧

Q4MP_gh_c472c21 来源:嵌入式与Linux那些事 作者:嵌入式与Linux那些 2021-01-21 09:59 次阅读

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

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

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

7b754a5c-57dd-11eb-8b86-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函数的执行时间得到明显提升。

7b9dce3c-57dd-11eb-8b86-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;

7bdcb444-57dd-11eb-8b86-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代码的关键路径如下图所示。

7bfbd612-57dd-11eb-8b86-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 OP (data[i] OP data[i+1]); } /* Finish any remaining elements */ for (; i 《 length; i++) { acc = acc OP data[i]; } *dest = acc; }

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

7c1b2288-57dd-11eb-8b86-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. 总结

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

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

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

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

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

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

原文标题:9个提高代码运行效率的小技巧你知道几个?

文章出处:【微信公众号:嵌入式ARM】欢迎添加关注!文章转载请注明出处。

责任编辑:haq

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

    关注

    116

    文章

    3775

    浏览量

    80843
  • 代码
    +关注

    关注

    30

    文章

    4744

    浏览量

    68345

原文标题:9个提高代码运行效率的小技巧你知道几个?

文章出处:【微信号:gh_c472c2199c88,微信公众号:嵌入式微处理器】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何提高poe供电效率

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

    如何提高CNC加工效率

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

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

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

    MES系统如何提高生产效率

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

    如何提高云计算的性能和效率

    提高云计算的性能和效率是一多维度的问题,需要从计算性能、存储性能、网络性能等多个方面入手。以下是一些具体的策略和方法: 一、计算性能优化 资源分配 : 根据用户需求动态分配资源,以提高
    的头像 发表于 10-24 09:23 346次阅读

    凤凰动力舵轮驱动轮的稳定性如何影响AGV的运行效率和稳定性

    的准确性。一稳定的舵轮能够确保AGV在复杂的工作环境中精确地按照预定路径行驶,避免偏离或产生误差。这有助于提高AGV的导航精度,减少因定位不准确而导致的重复作业或无效移动,从而提高运行
    的头像 发表于 08-27 13:20 279次阅读
    凤凰动力舵轮驱动轮的稳定性如何影响AGV的<b class='flag-5'>运行</b><b class='flag-5'>效率</b>和稳定性

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

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

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

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

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

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

    Falsh和RAM中运行同一段代码,执行效率相同吗?

    在Flash中运行程序和在RAM中运行程序,同一段代买执行时间是不是在RAM中更短,如果RAM中执行效率更高,如何将代码段全部拷贝到RAM中运行
    发表于 02-19 07:10

    Ethernet/iP转profinet技术使 AGV 提高生产效率

    Ethernet/iP转profinet技术​正在确保重型车辆零部件制造商的自动导引车 (AGV) 可靠运行,从而帮助提高其生产效率和吞吐量。
    的头像 发表于 01-18 10:51 384次阅读
    Ethernet/iP转profinet技术使 AGV <b class='flag-5'>提高</b>生产<b class='flag-5'>效率</b>

    提高支撑座效率的重要性

    自动化机械设备在运行过程中需要消耗大量的能源和资源,提高效率意味着更有效地利用这些资源,降低运行成本,而支撑座作为自动化机械设备中重要的传动元件,提高支撑座的
    的头像 发表于 01-17 17:47 278次阅读
    <b class='flag-5'>提高</b>支撑座<b class='flag-5'>效率</b>的重要性

    提高电源整机效率的方法

    电源整机效率是指电源将输入电能转换为输出电能的效率,通常用百分比表示。提高电源整机效率可以降低能源消耗,减少环境污染,提高设备的
    的头像 发表于 01-17 15:57 666次阅读

    提高系统效率的几个误解解析

    重复使用才会提高系统效率。所以在通信系统中一般只打开指令CACHE,数据CACHE即使打开也只局限在部分存储空间,如堆栈部分。同时也要求程序设计 要兼顾CACHE的容量及块大小,这涉及到关键代码循环体
    发表于 01-15 07:29

    python软件怎么运行代码

    Python是一种高级编程语言,它被广泛用于开发各种类型的应用程序,从简单的脚本到复杂的网络应用和机器学习模型。要运行Python代码,您需要一Python解释器,它可以将您的代码
    的头像 发表于 11-28 16:02 861次阅读