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.783930

C:\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. 验证

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

硬盘、内存、缓存(CPU)和寄存器 空间大小与存取速度的区别及设计原理

一、寄存器和存储器是不同的 很多人会将 寄存器 与 存储器 二者混淆&#xff0c;认为它们是同一个东西。但并不是&#xff01;&#xff01; 寄存器是CPU上的一个模块 存储器是 内存硬盘的统称 二、存取速度的比较 CPU(包含寄存器&#xff0c;缓存) > 内存 > 硬盘 内…

浅谈Postman与Jmeter的区别、用法

前阶段做了一个小调查&#xff0c;发现软件测试行业做功能测试和接口测试的人相对比较多。在测试工作中&#xff0c;有高手&#xff0c;自然也会有小白&#xff0c;但有一点我们无法否认&#xff0c;就是每一个高手都是从小白开始的&#xff0c;所以今天我们就来谈谈一大部分人…

【TD3思路及代码】【自用笔记】

1 组成&#xff08;Target Network Delayed Training&#xff09; Actor网络&#xff1a;这个网络负责根据当前的状态输出动作值。在训练过程中&#xff0c;Actor网络会不断地学习和优化&#xff0c;以输出更合适的动作。Critic网络&#xff1a;TD3中有两个Critic网络&#xff…

2024Postman中变量的使用!

Postman中可设置的变量类型有全局变量&#xff0c;环境变量&#xff0c;集合变量&#xff0c;数据变量及局部变量。区别则是各变量作用域不同&#xff0c;全局变量适用于所有集合&#xff0c;环境变量适用于当前所选环境&#xff08;所有集合中均可使用不同环境变量&#xff09…

重磅|国家能源局开展配电网安全风险管控重点行动

据国家能源局3月21日消息&#xff0c;为紧扣新形势下电力保供和转型目标&#xff0c;聚焦配电网安全运行、供电保障、防灾减灾和坚强可靠等方面安全风险&#xff0c;推动解决城乡配电网发展薄弱等问题&#xff0c;全面提升配电网供电保障和综合承载能力&#xff0c;国家能源局决…

Mysql数据库:索引管理

目录 一、索引的概述 1、索引的概念 2、索引的作用 3、索引的副作用 4、创建索引的原则依据 5、索引优化 6、索引的分类 7、数据文件与索引文件 二、管理数据库索引 1、查询索引 2、创建索引 2.1 创建普通索引 2.2 创建唯一索引 2.3 创建主键索引 2.4 创建组合…

Xinstall让App推广变得高效而简单

随着移动互联网的迅猛发展&#xff0c;App已成为人们生活中不可或缺的一部分。然而&#xff0c;对于众多开发者和广告主来说&#xff0c;如何高效地推广自己的App&#xff0c;却一直是一个令人头疼的问题。今天&#xff0c;我们要为大家介绍的&#xff0c;正是国内专业的App全渠…

AI大模型学习在当前技术环境下的重要性与发展前景

目录 前言1 学科基础与技能要求1.1 数学基础的深厚性1.2 编程能力的必要性1.3 对特定领域业务场景的了解 2 模型结构与算法的优化2.1 模型结构的不断演进2.2 算法优化的重要性2.3 准确性与效率的提升 3 AI大模型学习的应用场景3.1 自然语言处理3.2 计算机视觉3.3 推荐系统 结语…

独家发布! 10个2024年新算法跑10个测试集!

前言&#xff1a;独家发布&#xff01; 10个2024年新算法跑10个测试集&#xff01;每个算法都是独立.m文件&#xff0c;高效管理&#xff0c;所有结果均可一键运行自动保存&#xff0c;可用于算法对比、学习、改进等等&#xff0c;趁现在知道的人少&#xff0c;先用先发&#x…

DMA的设置

DMA&#xff08;Direct Memory Access&#xff0c;直接内存访问&#xff09;是一种用于提高数据传输效率的重要技术&#xff0c;在现代计算机系统中被广泛应用。DMA的设置涉及到配置DMA控制器、分配内存缓冲区、设置传输模式等多个方面。本文将介绍DMA的设置过程及相关注意事项…

Mysql数据库的SQL语言详解

目录 一、数据库的基础操作 1、数据库的基本查看和切换 1.1 查看数据库信息 1.2 切换数据库 1.3 查看数据库中的表信息 1.4 查看数据库或数据库中表的结构&#xff08;字段&#xff09; 1.5 数据类型 1.5.1 整数型 1.5.2 浮点型(float和double) 1.5.3 定点数 1.5.4…

基于Lealfet.js展示Turf.js生成的平滑曲线实践

目录 前言 一、问题的由来 1、创建网页框架 2、创建map对象 3、构建点位&#xff0c;生成路线 二、Turf.js平滑曲线改造 1、官网方法介绍 2、0.4弯曲度曲线 3、0.85弯曲度曲线 4、0.1度弯曲曲线 5、综合对比 总结 前言 在很多的关于路线的gis应用中&#xff0c;我们…

什么是485数据采集模块?

在工业自动化、智能楼宇、环境监测等众多领域&#xff0c;数据的采集与传输是实现智能化管理的关键。而485数据采集模块&#xff0c;作为一种高效、稳定的数据采集设备&#xff0c;正日益受到广泛关注。HiWoo Box&#xff0c;作为一款卓越的485数据采集模块&#xff0c;以其强大…

Android性能自测

目录 一、应用启动耗时自测 二、帧率查看 三、Top命令查看系统资源占用 3.1 第一行&#xff1a;任务(进程) 3.2 第二行&#xff1a;mem状态 3.3 第三行&#xff1a;swap交换分区 3.4 第四行&#xff1a;cpu状态 3.5 第五行&#xff1a;标题 四、抓取trace.html文件分析…

CCF-CSP认证考试 202305-3 解压缩 100分题解

更多 CSP 认证考试题目题解可以前往&#xff1a;CSP-CCF 认证考试真题题解 原题链接&#xff1a; 202305-3 解压缩 时间限制&#xff1a; 5.0s 内存限制&#xff1a; 512.0MB 题目背景 西西艾弗岛运营公司是一家负责维护和运营岛上基础设施的大型企业。在公司内&#xff0c;…

如何注册 onlyFans? onlyFans 如何订阅教程

一、onlyFans 介绍 OnlyFans 是一个在线平台&#xff0c;允许创作者为他们的内容提供订阅服务&#xff0c;通常是成人内容或独家内容。该平台于2016年成立&#xff0c;起初被认为是一个用于演员、模特、艺术家和其他创意行业从业者分享内容的地方。随着时间的推移&#xff0c;O…

linux命令(四)

操作文件 创建文件 touch test.txt修改文件后缀 mv test.txt test.sh将test.txt 改为test.sh 查看当前路径下的文件 ls如果你想知道你现在是在哪个目录下&#xff0c;可以使用pwd命令(不要认为pwd是密码的意思&#xff0c;pwd 是Print Working Directory的缩写) pwd查看文…

初识二叉树

文章目录 一.什么是树二.什么是二叉树三.二叉树的访问次序四.特殊的二叉树五.求结点个数六.平衡二叉树总结 一.什么是树 树是由一个集合以及在该集合上定义的一种关系构成的。 集合中的元素称为树的节点&#xff0c;所定义的关系称为父子关系。 父子关系在树的节点之间建立了一…

【C++ leetcode】双指针问题

1. 611. 有效三角形的个数 题目 给定一个包含非负整数的数组 nums &#xff0c;返回其中可以组成三角形三条边的三元组个数。 题目链接 . - 力扣&#xff08;LeetCode&#xff09; 画图 和 文字 分析 判断是否是三角形要得到三边&#xff0c;由于遍历三边要套三层循环&#x…

LeetCode每日一题【24. 两两交换链表中的节点】

思路&#xff1a;先创建虚拟头结点&#xff0c;再用双指针&#xff0c;两两交换 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr…