C++性能优化常用技巧

server/2025/2/28 12:19:19/

一. 选择合适的数据结构

1.1 map与unordered_map的选择

如果仅仅只需要使用到快速查找的特性,那么unordered_map更加合适,他的复杂度是O(1)。如果还需要排序以及范围查找的能力,那么就选择map。

1.2 vector与list的选择

通常情况下,顺寻存储一些数据,然后通过下标进行查找,就选择vector。那么什么时候选择list呢?常见的一个用法就是在LRU 缓存的实现中,我们会使用list来存放缓存结点,而不是vector,主要原因就在于list的插入和删除是高效的。

1.3 unordered_map与自定义查找表的选择

如果不需要复杂的哈希函数,仅通过数组下标的形式就能完成哈希,比如key是26个字母或者连续的数字。此时只需要定义一个数组就能实现哈希存储(自定义查找表)。

二. 选择合适的算法

算法对性能的影响是值得考虑的,以排序算法举例:

  • 快速排序:适合数据比较均匀的场景,如果数据已经有序或者接近有序,此时快速排序会退化到O(n*n)的复杂度。
  • 堆排序:适合求topK问题。
  • 归并排序:O(n)的空间复杂度,但是排序性能不会因为数据本身已经有序而退化。
  • 冒泡排序:一般情况下不推荐此排序。性能较差。

三. 避免不必要的拷贝

3.1 使用引用

引用可以直接与被引用对象共享同一份存储,可以认为引用是给对象起一个别名。如下示例代码:

// 形参使用引用
void show(const std::string& str)
{// do something
}std::vector<std::string> strs;
/*
对strs进行填充
*/
// 使用引用接收返回值,前提是函数返回对象的引用
const std::string& str = strs[1];
3.2 使用移动语义

移动语义一般用在初始化对象或者给对象赋值时,可以避免对象的拷贝。如下代码:

std::string stra = "abc";
// 拷贝stra对象
std::string strb = stra;
// 移动strb的资源到strc中
std::string strc = std::move(strb);
3.3 返回值优化(RVO)

通常编译器都有RVO优化的功能,它允许直接在调用者的栈帧上构造对象,从而避免了额外的拷贝和资源析构的开销。如下代码:

class A {
public:A() {}~A() {}A(const A& rhs) {}A(A&& rhs) {}
};int func()
{A a;return a;
}int main()
{A a = func();return 0;
}

上述代码中,返回值优化之后,类A的构造函数和析构函数只被调用一次,不会产生临时变量的构造与析构,以及拷贝构造的过程。

四. 合适的内存管理

4.1 智能指针

在管理动态内存时,智能指针可以帮助我们实现更可靠的内存管理,避免内存泄漏等严重问题。C++11中提供了unique_ptr和shared_ptr两个智能指针,那么该如何选择呢?

  • shared_ptr:共享智能指针。一个对象可能在多个地方被共享。
  • unique_ptr:独占智能指针。一个对象只能被一个智能指针对象所拥有。

通常情况下,如果能明确是独占的场景,那么就选择unique_ptr,虽然shared_ptr也能保证正确性,但是后者性能要比前者差30%。因为uniqe_ptr更接近裸指针,而shared_ptr内部实现相对复杂(引用计数、控制块等)。

4.2 内存池

内存池是一个预先申请和分配好的内存区域。在需要申请内存资源时,可以直接在内存池中获取,在释放内存时,将内存返还给内存池。从而避免了频繁的内存分配和释放的过程。google的tcmalloc就提供了这样的功能。

4.3 对象池

对象池同样也是一种预先申请和分配内存的技术,区别于内存池的是,其针对的是特定的类对象的内存管理。如果一个类型需要频繁的进行对象的创建和释放,并且对象的创建比较耗时。那么我们可以选择使用池化技术,预先创建好一定数量的对象,放到对象池中。其实很多池化技术都属于这个范畴,只是针对不同场景,有其独特的叫法,如MySQL的连接池、线程池等等。

4.4 栈内存的管理

通常我们只需要管理堆内存,而栈内存交给操作系统来管理。但是栈内存的申请和初始化依旧是由程序员来控制的,而操作系统只负责内存的分配和释放。考虑如下场景:

void func()
{for (int i = 0; i < 999999; ++i) {char data[1000000];// do something}
}

上述data在每一次循环都需要分配一次内存,然后对其进行操作。如果data的创建可以放到循环体之外进行也不影响程序的正确性。那么data的内存分配只需要一次即可。
事实上,我们还可以继续优化。因为这块内存很大,函数也可能会被频繁调用,每次调用都会重复申请一大块内存。所以考虑创建一个静态的data变量,或者静态的全局变量。只要它能满足程序的正确性要求。何乐而不为呢?当然静态变量在多线程环境下会存在数据竞争的问题。此时还可以考虑使用threadlocal变量

五、减少函数调用

5.1 使用内联函数

一次函数调用涉及两次指令跳转。如果一个函数频繁调用,可以使用内联机制来避免函数的调用。C++11提供了inline关键字,来告诉编译器被修饰的函数可以在调用处进行替换。一般这样的函数是一些功能简单,代码行较少的函数。当然是否内联取决于编译器,inline只是给编译器建议。是否内联可通过查看汇编代码来确认。

5.2 减少函数递归调用

函数递归通常写起来比较方便,并且也更好理解。但是函数递归带来的开销是巨大的,随着递归深度的加深,性能也会受到影响。稍有不慎,甚至会造成栈溢出。所以应该尽量避免使用递归函数,考虑使用迭代或者只使用尾递归(编译器优化,只需要当前的栈帧空间,无需开辟新空间)的方式。

六、多线程处理

多线程可以利用多核特性,同时处理多个任务,从而提高程序性能。多线程常常涉及数据竞争的问题,为了提高多线程的性能,应减少数据竞争,常见的方法有:

  • 使用原子变量。
  • 使用读写锁。
  • 降低锁的持有时间。
  • 使用线程池模型,重复利用线程资源,利用多队列减少工作线程间的数据竞争。
  • 使用无锁数据结构,避免频繁的线程上下文切换。
  • 减少需要共享的状态。

七、编译器优化

在考虑性能问题之前,首先得开启编译器优化,很多时候,代码虽然写的不是最优的,但是在开启编译优化之后,往往能达到很好的优化效果。常见的优化选项:

  • O0:不做任何优化。
  • O1:主要对代码的分支,常量以及表达式等进行优化。
  • O2:会尝试更多的寄存器级的优化以及指令级的优化。
  • O3:在O2的基础上进行更多的函数内联优化,因此目标代码会更大,但执行速度会更快。

八、指令级优化

8.1 SIMD指令

SIMD(单指令多数据)指的是具有多个处理元件的计算机同时对多个数据执行相同操作的过程。以加法为例,通常情况下,我们需要先取操作数1,再取操作数2,然后求和。而SIMD指令则支持同时取多个操作数,然后执行求和运算。也就是说取操作数的过程是并行的。在gcc中可以通过添加ftree-vectorize编译选项,或者开启O2优化,就可以让编译器使用SIMD优化代码。

九、提高缓存命中率

CPU有3级缓存L1、L2、L3,其中L1离核心最近,因此速度也最快。由于缓存有大小限制,因此只有少量的数据和指令会被加载到缓存中,所以经常会出现缓存无法命中的问题。因此提高cache命中率就可以提高程序性能。

9.1 选择合适的数据结构

数据结构多使用连续的存储,如vector,而少使用list、map这种非连续存储类型。因为连续的内存通常会被一起加载到缓存中。

9.2 内存对齐

内存不对齐的情况下,cpu可能需要夸多个内存行去获取数据。从而增加了cpu的访存次数,也增加了缓存不命中的可能性。

9.3 减少条件分支

程序中的条件判断,如 if-else 语句,可以通过逻辑重组或使用分支预测优化来提高缓存命中率。在某些情况下,消除不必要的条件判断或将其重构为更高效的形式可以减少缓存行的加载和卸载,从而提高性能。例如,使用gcc的提供的关键字__builtin_expect来告诉编译器哪个分支命中的概率最高,从而实现优化。

9.4 其他
  • 避免频繁的内存分配与释放,减少内存碎片。
  • 循环展开,减少循环次数。
  • 循环中按行处理数据要比按列处理更高效。

http://www.ppmy.cn/server/171290.html

相关文章

常用的 Web API

1. Intersection API Interserction API 是一种用于异步检测模板元素月视口&#xff08;或者指定父元素&#xff09;交叉状态变化的浏览器原生接口&#xff0c;主要那个用于解决传统滚动监听性能差、实现复杂的问题&#xff0c;适用于懒加载、慢点曝光、无线滚动等场景。 2. …

游戏引擎学习第122天

仓库:https://gitee.com/mrxiao_com/2d_game_3 讨论了多线程&#xff08;Multithreading&#xff09; 今天开始讨论的话题对大家来说不太好&#xff0c;因为这是一个相对棘手的技术问题&#xff0c;虽然它很重要。这个话题不像优化那样是通过解决一个问题并进一步精细化来变得…

SpringBoot 整合mongoDB并自定义连接池,实现多数据源配置

要想在同一个springboot项目中使用多个数据源&#xff0c;最主要是每个数据源都有自己的mongoTemplate和MongoDbFactory。mongoTemplate和MongoDbFactory是负责对数据源进行交互的并管理链接的。 spring提供了一个注解EnableMongoRepositories 用来注释在某些路径下的MongoRepo…

excel单、双字节字符转换函数(中英文输入法符号转换)

在Excel中通常使用函数WIDECHAR和ASC来实现单、双字节字符之间的转换。其中 WIDECHAR函数将所有的字符转换为双字节&#xff0c;ASC函数将所有的字符转换为单字节 首先来解释一下单双字节的含义。单字节一般对应英文输入法的输入&#xff0c;如英文字母&#xff0c;英文输入法…

Linux中子线程会继承父线程对相关变量的可见性

在 Linux 的 POSIX 线程模型中,当父线程修改全局变量后创建子线程,子线程一定会看到修改后的最新值。这是由线程的内存共享机制和线程创建时序保证的,具体原理如下: 关键机制解析 内存共享本质: 所有线程共享相同的全局数据段修改操作直接作用于物理内存,无副本存在cint …

html css js网页制作成品——HTML+CSS甜品店网页设计(5页)附源码

目录 一、&#x1f468;‍&#x1f393;网站题目 二、✍️网站描述 三、&#x1f4da;网站介绍 四、&#x1f310;网站效果 五、&#x1fa93; 代码实现 &#x1f9f1;HTML 六、&#x1f947; 如何让学习不再盲目 七、&#x1f381;更多干货 一、&#x1f468;‍&#x1f…

ubuntu配置jmeter

1.前提准备 系统 ubuntu server 22.04 前提条件&#xff1a;服务器更新apt与安装lrzsz&#xff1a;更新apt&#xff1a; sudo apt update安装lrzsz: 命令行下的上传下载文件工具 sudo apt install lrzszsudo apt install zip2.安装jemeter 2.1.下载jdk17 输入命令&#xf…

数据结构 【搜索二叉树】

搜索二叉树是STL中map和set的重要铺垫&#xff0c;学好搜索二叉树有助于理解map和set的特性。搜索二叉树也是一种二叉树结构&#xff0c;只是多了一些特定的性质。 一棵搜索二叉树可以为空树&#xff0c;如果不为空时&#xff0c;一定满足下面的性质。 若它的左子树不为空&…