C/C++代码性能优化——编程实践

1. 编程实践

在一些关键的地方,相应的编程技巧能够给性能带来重大提升。

1.1. 参数传递

传递非基本类型时,使用引用或指针,这样可以避免传递过程中发生拷贝。参数根据是否需要返回,相应加上const修饰,代码更安全,且编译器能够更大可能地进行参数优化。

1.2. 函数返回

函数返回非基本类型时,同样会发生拷贝,降低性能。C++代码中使用右值引用和返回值优化,不影响性能。

1.3. 循环展开

循环为什么慢,一次循环就要产生自加、比较、跳转3条指令。减少循环次数,就能提升性能。尤其是针对一些循环体内代码少的情况,性能影响更大。如下示例:

int64_t calc1(int64_t n)
{int64_t fact = 1;for (int64_t i = 1; i < n; i++){fact += i;}return fact;
}int64_t calc2(int64_t n)
{int64_t fact = 1;for (int64_t i = 1; i < n; i += 4){fact += i;fact += i + 1;fact += i + 2;fact += i + 3;}return fact;
}int64_t calc3(int64_t n)
{int64_t fact = 1;for (int64_t i = 1; i < n; i += 8){fact += i;fact += i + 1;fact += i + 2;fact += i + 3;fact += i + 4;fact += i + 5;fact += i + 6;fact += i + 7;}return fact;
}

gcc分别测试优化级别-O2和-O3的效果,结果显示循环展开效果明显,但是-O3优化级别下展开4层和8层几无差异。

C:\Mingw64\mingw64\bin\g++.exe -std=c++17 -Wall -Wextra -g -O2 -mavx2 -Iinclude -c -MMD src/main.cpp  -o src/main.o
C:\Mingw64\mingw64\bin\g++.exe -std=c++17 -Wall -Wextra -g -O2 -mavx2 -Iinclude -o output\main.exe src/main.o  -Llib
Executing 'all' complete!
Calc1  932355974711512065:seconds: 26.159987
Calc2 932356074711512065:seconds: 19.535794
Calc3 932356074711512065:seconds: 9.783930C:\Mingw64\mingw64\bin\g++.exe -std=c++17 -Wall -Wextra -g -O3 -mavx2 -Iinclude -c -MMD src/main.cpp  -o src/main.o
C:\Mingw64\mingw64\bin\g++.exe -std=c++17 -Wall -Wextra -g -O3 -mavx2 -Iinclude -o output\main.exe src/main.o  -Llib
Executing 'all' complete!
Calc1  932355974711512065:seconds: 13.093723
Calc2 932356074711512065:seconds: 6.641366
Calc3 932356074711512065:seconds: 6.605240

1.3. 查表

例如计算char类型中bit1的个数,事先准备256大小的数组,存储对应下标的bit1个数。这样在使用时,直接通过数据下标来查询对应的bit1个数,性能非常好。

1.4. 慎用位域

位域节省空间,但是其读写性能非常差,在性能关键处,慎用位域变量。

1.5. 尾递归

我们知道递归容易导致栈爆了,但是很多场景下递归又非常好用。如何避免递归调用栈爆了呢?使用尾递归技术。

尾递归的递归调用必须是函数体内的最后一个操作。这意味着在递归调用之后不应有任何其他计算或表达式。这个要求是为了确保在递归调用之后没有需要保存的局部变量或表达式结果,从而可以通过直接替换参数值并跳转到函数开头来优化。可以简单理解为递归调用发生时,前面的临时变量都可以覆盖操作,不用保存,这样就可以优化栈内存不断增加的问题。示例:

unsigned long long factorialTail(int64_t n, unsigned long long result)
{if (n == 0){return result;}return factorialTail(n - 1, result * n);
}unsigned long long factorial(int64_t n)
{if (n == 0){return 1;}return n * factorial(n - 1);
}

factorial在很多讲解中被认为不符合尾递归优化,因为要暂存n,可能导致栈爆了。但是现代编译器很聪明,只要开启了-O2或-O3即会开启尾递归优化,上面两个代码都可以正常优化,无论多么深的调用,都不会异常。

1.6. 位运算替换算术运算

位运算在2的倍数操作时,非常方便,性能比较好。如

int x = y << 3;  // 相当于y*8
int x = y >>4;   // 相当于y/16
int x = y & 7;    // 相当于y%8

在低功耗嵌入式32位MCU中,位操作一般需要一个指令周期完成操作。而乘法要2个指令周期。在不支持浮点运算的MCU中,除法是编译器通过乘法操作来模拟的,所以性能更低。取余操作类似除法操作,性能很低。

所以像这些2的倍数的乘法除法取余操作,使用位运算性能会大幅提升。

1.7. 0大小数组

0大小数组不是C/C++的标准语法,是编译器的扩展语法,其也被称为"柔性数组"(Flexible Array)。armcc和gcc均支持此语法。0大小数组不占用结构大小,只是一个占位符。传统的指针可能导致结构体变量出现缓存不友好,影响性能。如果使用此结构,简单方法,且缓存非常友好。在Windows SDK和Linux内核中均有使用此语法形式。

1.8. 减少循环中的判断

分支预测错误非常影响性能,所以在循环中尽量少用判断。在性能关键处的判断,可以加上__builtin_expect来优化。

// Bad
void calc(bool bFlag)
{init();for (int i = 0; i < 10000000; i++){if (bFlag){dosomeA();}else{doSomeB();}}
}// Good
void calc(bool bFlag)
{init();if (bFlag){for (int i = 0; i < 10000000; i++){dosomeA();}}else{for (int i = 0; i < 10000000; i++){dosomeA();}}
}void main()
{calc(true);calc(false);
}

1.9. const、restrict和static

const和static应用尽用,不仅代码更安全可靠,编译器也能更明确代码的意图,可以更进一步地对代码进行优化,如更好的内联,更好的变量替换等,进而提升性能。

restrict是C99中新引入的关键字,指示指针是唯一访问某个内存区域的,从而帮助编译器进行更好的优化。

1.10. 不定义不使用的返回值

函数定义并不知道函数返回值是否被使用,假如返回值从来不会被用到,应该使用void来明确声明函数不返回任何值。

1.11. 异步计算

1.11.1 单核

要提升性能,就不能让CPU停下来,那么在面对一些高时延IO操作时。有一些外设,如UART,一般配置了中断,这样就不轮询来监听UART,专心做正常的事情,UART中断产生了,就来处理UART即可,这样就可以充分利用CPU。

还有一些外设,操作响应慢,如NAND Flash,如果一直轮询来等待NAND Flash响应,非常浪费CPU资源。此时可以使用异步计算,先去做其他事情,估算到NAND Flash差不多结束操作时,再回来轮询NAND Flash状态进行相应的处理。在等待NAND Flash响应的这段时间,虽然可以去做A事情,但是A事件做到一半的时候,先暂存A事情的相差状态,再去响应NAND Flash。响应完NAND Flash之后,再回来接着恢复A事情的相关状态,继续A事情。这种操作方式,非常影响代码编写。在单核单线程CPU中,无法使用多线程,此时就非常需要一个好的异步架构来解决上述问题。方案有两个,一个是使用一些RTOS的多任务模型,一个是使用协程,都可以提升较好的异步计算方案来应对上述场景。

1.11.2 多核

在多核架构MCU中,依然会面临外设阻塞的问题,如果计算资源足够,可以某个核阻塞等待。如果计算资源有限,可以多核并行计算结合多任务模型或协程来实现异步计算。

1.12. 事件驱动框架

一个好的框架,能够提升代码的整体性能。事件驱动架构就是一个追求实时性能的框架。事件驱动的架构由生成事件流的事件生成者和侦听事件的事件使用者组成 。

事件驱动框架是基于发布-订阅设计模式实现的,生产者产生需要处理的相关事件,消费者订阅想要处理的事件(通过回调函数注册),当事件产生时,事件代理将根据注册信息调用相应的消费者处理。多个消费者之间的采用多核或异步计算模型处理。事件代理可以将所有事件整理之后再来通知相应的消费者处理。

1.13. 生成式AI

生成式AI随着ChatGPT的出现,进化更加快了,越来越聪明了。GPT-4,Claude 3还有Gemini Pro都非常厉害。我们会因为思维、信息的局限,走进一些误区。所以在性能关键处的优化,我们都可以请教生成式AI,让它给我们一些意见或建议,指导我们更好地进行性能优化。优化的内容可以是具体代码,也可以是数据结构或算法的选择,也可以是架构的优缺点分析等。

有些关键的地方,汇编代码更有效率,但是汇编代码编写比较麻烦。虽然内联汇编简化了传参和返回,但是编写依然不容易。此时我们就可以借助生成式AI,如下图所示的Prompt,生成的代码测试直接可用,不用修改。

2. 其他

避免性能负优化,也是一种优化性能的方法。另外,理论和实际可能存在一些误解,关键优化一定要真机验证。

2.1. 交换函数

例如交换变量的函数,有人可能以为不用中间变量是不是效率更高,看起来可能是。但是在编译器性能优化下,交换变量的函数直接被优化掉了,编译器直接将两个变量对应的寄存器交换使用即可。gcc的编译结果略有差异,但是swap_ex函数的性能依然较差。

2.2. volatile

volatile作用是禁止编译器优化变量的访问,强制每次从主存上进行存取。

  1. 硬件寄存器对应的变量,需要实时响应,所以需要禁止优化到寄存器上操作。
  2. 中断函数与主流程函数的交互变量,也需要实时响应,所以要禁止优化。
  3. 多核交互的变量,,也需要实时响应,所以要禁止优化。

其他情况下,不要使用volatile,影响性能。

2.3. 不影响性能的代码

2.3.1. 前置自加和后置自加

i++;
++i;
i--;
--i;
for {;;}
while (1)

如果上述代码未优化,性能上确实有差异,但是在开启优化之后,性能是完全一样的。

2.3.2. 栈变量

void test1()
{int sum = 0;for (int i = 0; i < 10; i++){int temp = i*2;sum += temp;}
}void test2()
{int sum = 0;int temp = 0;for (int i = 0; i < 10; i++){temp = i*2;sum += temp;}
}

栈变量和堆变量不一样。堆变量需要申请释放。栈变量不需要申请释放,用与不用栈内存都在那里放着的。C99之后支持的新语法,栈变量可以随便放,不需要放置在块作用域的最前面。栈遵循最小作用域原则即可。

2.3.3. 寄存器

最终实际参与计算的都是寄存器,32位CPU上的大小都是32位的,64位CPU上一般是兼容32寄存器,也即64位CPU有两套寄存器。

在32位CPU上,计算时,无论是int,short还是char类似,最终都是加载到32位寄存器上进行计算,最终结果也是存在32位寄存器上。也就是说,参与计算的变量是int还是short或char,不影响计算的性能。

在64位CPU上,在数据真实大小小于32位时,用int64还是用int32参与计算,性能是一样的。

2.4. 避免过早优化

著名计算机科学家、图灵奖得主,Donald Knuth曾说过:Premature optimization is the root of all evil (过早优化是万恶之源)。

针对x86_64或Cortext-A系列,现在的编译器和CPU非常智能化,能够帮你极好地优化代码执行性能。所以在开发前期,不必过分花力气去优化代码。在后期发现需要提升性能的时候,再来针对性地优化代码,收益付出比会更大。

避免过早优化不是说设计之初始不考虑优化,而是不要花过多时间去关注一些非优先项的性能优化。

2.5. 验证

由于编译器和处理器的发展,有一些优化它们已经做得很好。过分的手动优化,反倒会干扰编译器和处理器来进行优化。如循环展开,预取指令等,针对一些基本的代码结构,编译器能够做得比较好,所以自行进行优化的代码,一定要进行基准测试。

有一些代码的场景,依据数据局部性的优化和依据分支预测的优化是相斥的,此时同样需要基于实际情况来模拟验证,决定最终优化方案。

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

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

相关文章

FPGA学习_时序分析

文章目录 前言一、组合逻辑与时序逻辑二、建立时间和保持时间三、建立时间和保持时间 前言 心中有电路&#xff0c;下笔自然神&#xff01;&#xff01;&#xff01; 一、组合逻辑与时序逻辑 组合逻辑&#xff1a;没有时钟控制的数字电路&#xff0c;代码里的判断逻辑都是组…

解决淘宝镜像过期问题 ERR! request https://registry.npm.taobao.org

目录 一、问题描述 二、解决方案 2.1、针对于域名更换解决方案 2.2、针对于证书过期解决方案 三、进行测试 一、问题描述 针对于2022年5月31号和2024年1 月 22 日前的前端项目 npm.taobao.org和旧域名于2021年官方公告域名更换事件&#xff0c;已于2022年05月31日零时起…

每日一题 --- 977. 有序数组的平方[力扣][Go]

今天这一题和昨天的知识点是一样的&#xff0c;就是双指针法。 题目&#xff1a; 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 示例 1&#xff1a; 输入&#xff1a;nums [-4,-1,0,3,1…

STM32 ESP8266模块的曲折探索

这是本文的配套资料&#xff0c;最终工程请参考 新_ESP8266资料\stm32f103成功移植的项目 【免费】stm32f103c8t6esp8266资料资源-CSDN文库 一、等到了ready 产品参数 我使用的是ai-thinker的esp8266-01s&#xff0c;以下为产品规格书 引脚定义&#xff1a; 依据引脚定义&…

专业140+总分410+南京大学851信号与系统考研经验南大电子信息与通信集成,电通,真题,大纲,参考书。

今年分数出来还是有点小激动&#xff0c;专业851信号与系统140&#xff08;感谢Jenny老师辅导和全程悉心指导&#xff0c;答疑&#xff09;&#xff0c;总分410&#xff0c;梦想的南大离自己越来越近&#xff0c;马上即将复试&#xff0c;心中慌的一p&#xff0c;闲暇之余&…

ubuntu 如何使用阿里云盘

你好&#xff0c;我是 shengjk1&#xff0c;多年大厂经验&#xff0c;努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注&#xff01;你会有如下收益&#xff1a; 了解大厂经验拥有和大厂相匹配的技术等 希望看什么&#xff0c;评论或者私信告诉我&#xff01; 文章目录 一…

通讯录的动态实现

文章目录 通讯录的动态实现模块化编程通讯录的框架构建功能的具体实现初始化通讯录添加联系人删除联系人查找联系人修改联系人打印通讯录排序通讯录检查容量并扩容加载通讯录保留通讯录销毁通讯录 完整代码总结 通讯录的动态实现 模块化编程 分文件 不同模块放在不同的文件下 …

XSKY 智能存储,助力“数据要素 X”先进制造

3 月 21-22 日&#xff0c;主题为“突破 智行”的 IMC2024 第七届中国智造数字科技峰会在重庆召开。作为在先进制造领域拥有领先存储解决方案以及众多应用实践的企业&#xff0c;星辰天合受邀参加了此次峰会并荣获大会颁发的“最佳存储解决方案奖”。同时&#xff0c;星辰天合先…

CISP 4.2备考之《安全支撑技术》知识点总结

文章目录 第一节 密码技术第二节 标识和身份鉴别技术第三节 访问控制技术 第一节 密码技术 密码学发展阶段&#xff1a;古典、近代、现代和公钥密码学及特点。 密码系统组成&#xff1a;明文、加密、密钥、解密、密文。 柯克霍夫原则&#xff1a;密钥保密&#xff0c;算法公开…

手撕算法-删除链表的倒数第 N 个结点

描述 思路 快慢指针&#xff0c;快指针先走N步&#xff0c;走不够N步返回空。慢指针和快指针一起走&#xff0c;当快指针到达终点&#xff0c;即快指针为null时&#xff0c;慢指针到达倒数第N个节点。因为要删除倒数第N个&#xff0c;所以要记录之前的节点pre&#xff0c;假设…

思科网络中DHCP中继的配置

一、什么是DHCP中继&#xff1f;DHCP中继有什么用? &#xff08;1&#xff09;DHCP中继是指一种网络设备或服务&#xff0c;用于在不同的子网之间传递DHCP&#xff08;动态主机配置协议&#xff09;消息。DHCP中继的作用是帮助客户端设备获取IP地址和其他网络配置信息&#x…

开源项目ChatGPT-Next-Web的容器化部署(三)-- k8s deployment.yaml部署

一、说在前面的话 有了docker镜像&#xff0c;要把一个项目部署到K8S里&#xff0c;主要就是编写deployment.yaml。 你需要考虑的是&#xff1a; 环境变量服务的健康检测持久化启动命令程序使用的数据源程序使用的配置文件 因为本前端项目比较简单&#xff0c;这里只做一个…

基于springboot+vue+Mysql的“智慧食堂”设计与实现

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

Vue响应式原理全解析

前言 大家好&#xff0c;我是程序员蒿里行。浅浅记录一下面试中的高频问题&#xff0c;请你谈一下Vue响应式原理。 必备前置知识&#xff0c;​​Vue2​​官方文档中​​深入响应式原理​​​及​​Vue3​​官方文档中​​深入响应式系统​​。 什么是响应式 响应式本质是当…

Redis 不再“开源”,对中国的影响及应对方案

Redis 不再“开源”&#xff0c;使用双许可证 3 月 20 号&#xff0c;Redis 的 CEO Rowan Trollope 在官网上宣布了《Redis 采用双源许可证》的消息。他表示&#xff0c;今后 Redis 的所有新版本都将使用开源代码可用的许可证&#xff0c;不再使用 BSD 协议&#xff0c;而是采用…

基于SpringBoot实现WebSocket实时通讯的服务端和客户端

实现功能 服务端注册的客户端的列表&#xff1b;服务端向客户端发送广播消息&#xff1b;服务端向指定客户端发送消息&#xff1b;服务端向多个客户端发送消息&#xff1b;客户端给服务端发送消息&#xff1b; 效果&#xff1a; 环境 jdk&#xff1a;1.8 SpringBoot&#x…

如何确保多人游戏的配对体验快速而顺利

开发人员知道,游戏玩家在玩游戏时最快乐,他们等待进入多人游戏的时间越长,你失去他们的速度就越快。玩家不喜欢在大厅里等待;他们想马上参与行动。这就是为什么优化匹配是关键。 谷歌(Google)和Unity的Open Match等系统正在加速使用自定义逻辑构建匹配器的能力,使开发人…

跳过mysql权限验证来修改密码-GPT纯享版

建议重新配置一遍&#xff0c;弄成功好多次了&#xff0c;每次都出bug&#xff0c;又要重新弄&#xff0c;不是过期就是又登不进去了&#xff0c;我服了 电脑配置MySQL环境&#xff08;详细&#xff09;这个哥们的10min配完&#xff0c;轻轻松松&#xff0c; 旧方法&#xff…

Python-VBA编程500例-015-03(入门级)

飞行棋(Flying Chess)算法是一种搜索算法&#xff0c;主要用于解决图搜索和路径规划问题。它的主要特点是可以“飞跃”到棋盘上任何位置&#xff0c;从而大大减少了搜索的时间和空间复杂度。以下是一些飞行棋算法的实际应用场景&#xff1a; 1、路径规划&#xff1a;在机器人领…

利用Scala与Apache HttpClient实现网络音频流的抓取

概述 在当今数字化时代&#xff0c;网络数据的抓取和处理已成为许多应用程序和服务的重要组成部分。本文将介绍如何利用Scala编程语言结合Apache HttpClient工具库实现网络音频流的抓取。通过本文&#xff0c;读者将学习如何利用强大的Scala语言和Apache HttpClient库来抓取网…