C++: 16个基础的C++代码性能优化实例
前言
近期推动项目屎山代码进行了一波性能优化,实现了较大的性能提升。这里记录了部分近期代码优化的小技巧,这些例子仅从C++语言层面进行优化,主要在于优化类设计、减少隐含函数调用、减少拷贝等,较为基础实用,但涉及的知识点并不少。本文提供了一个视角,可以帮助了解一些C++代码的不同写法性能开销差异。对于很少关注代码性能的人,或许可以看看,提升一下代码性能方面的意识,从而写出性能更高的程序。
1. 使用const引用传递而非值传递
通过值传递给函数时会创建临时变量。
void process(vector<int> s);
改为引用传递:
void process(const vector<int>& s);
- 如果参数对象较大,使用值传递时拷贝的代价大,应该使用引用传递。
- 如果是内置类型int, float, double, char 和 bool,使用值传递,可以支持某一些编译器优化手段。
同理,对类成员访问接口:
Eigen::VectorXd GetStates() const {
return current_states_;
}
应改为返回 const&
,
const Eigen::VectorXd& GetStates() const {
return current_states_;
}
保存函数返回的引用时,为避免拷贝,同样需要引用:
const auto& states = GetStates();
返回引用避免拷贝还需要避免隐式转换的发生,否则还是会拷贝,详见第3条。
2. for循环中使用引用遍历
以下for循环,每次从 object_list
容器拷贝临时对象 obj
,
for(auto obj: object_list) {
obj->func();
}
改为 const auto&
避免拷贝。
for(const auto& obj: object_list) {
obj->func();
}
3. 注意隐式转换带来的拷贝
- 注意const语义不一致带来的隐式转换。
比如在以下代码中, GetObject()
虽然返回引用类型,但由于返回类型的const语义不一致,实际调用该函数后还是会导致返回临时变量。
class Widget {
public:
const std::shared_ptr<const object>& GetObject() {
return object_;
}
private:
std::shared_ptr<Object> object_;
};
当函数返回类型和临时值类型具有继承关系时,也会发生隐式转换。 见第10条提到的
RVO
。对容器如
std::unordered_map
等进行遍历时,如果未使用auto,应写出遍历元素的正确类型。
比如for
遍历std::unordered_map
时,每个元素的类型为 std::pair<const key, value>
。以下代码会对pair进行拷贝,原因在于发生了到std::pair<key, value>
的隐式转换,创建了临时对象。
std::unordered_map<int, Widget> umap;
for(const std::pair<int, Widget>& data: umap) {
//...
}
以下代码更加高效,对key添加了 const
修饰符,避免了隐式转换。
std::unordered_map<int, Widget> umap;
for(const std::pair<const int, Widget>& data: umap) {
//...
}
或者尽量使用 const auto&
自动推断类型。
4. 定义即初始化
以下代码会调用一次默认构造函数,一次赋值运算符函数;
Matrix m1; // call default constructor
m1 = m2 + m3; // call Assignment operator
以下代码只会调用一次拷贝构造函数。
Matrix m1 = m2 + m3; // call copy constructor
另一个是 std::shared_ptr
深拷贝的例子,以下代码先对 Object
默认构造后对其赋值;
std::shared_ptr<Object> object = std::make_shared<Object>();
// call default constructor
*object = *(objects_[index]->GetBaseObject());
// call Assignment operator
由于 std::make_shared<T>()
可以支持T的拷贝构造,可直接通过拷贝构造完成。
ObjectPtr object = std::make_shared<Object>(*(objects_[index]->GetBaseObject()));
// call copy constructor
5. 循环中复用临时变量
以下代码在循环中创建、销毁临时变量,多次调用拷贝构造函数和析构函数,
for (const auto& point : object->points) {
Eigen::Vector3d ref_point(0.0, 0.0, 0.0);
ref_point.head(2) = Transform(loc_info, odom_ref_point.head(2));
object->predicted_state.reference_points.emplace_back(ref_point);
}
改为复用临时变量,创建一次,多次复用。
Eigen::Vector3d ref_point(0.0, 0.0, 0.0);
for (const auto& point: object->points) {
ref_point.head(2) = Transform(loc_info, odom_ref_point.head(2));
object->predicted_state.reference_points.emplace_back(ref_point);
}
6. 尽量使用复合运算符
通常operator+=()
等复合运算符实现的形式会返回自身的引用。假如 X
类是一种矩阵类,支持四则运算。
以下代码导致临时变量的创建,
a = (a + b) * c;
以上代码相当于以下过程:
X tmp1(a + b);
X tmp2(tmp1 * c);
a = tmp2;
通过调用operator+=()
和 operator*=()
改写可以避免临时变量的创建。
a += b;
a *= c;
7. 在构造函数中使用初始化列表
以下代码中 Widget
类的构造函数实现未采用初始化列表,其构造函数的运行过程为:在完成 Widget
的 std::string
和 Attributes
等成员的默认构造之后,再对它们进行赋值,隐含带来了一些额外开销:
class Widget {
public:
Widget(const int num, const std::string& name, const Attributes& attributes) {
num_ = num;
name_ = name;
attributes_ = attributes;
}
private:
int num_ = 0;
Attributes attributes_;
std::string name_;
}
使用初始化列表,可实现构造时即初始化。
class Widget {
public:
Widget(const int num, const std::string& name, const Attributes& attributes) :
num_(num), name_(name), attributes_(attributes) {} // initializer list
private:
int num_ = 0;
Attributes attributes_;
std::string name_;
}
8. 使用std::move()避免拷贝
STL容器通常都支持移动操作,对于不再使用的临时变量,可以使用std::move()
减少拷贝操作,提高程序性能。
void CollectResult(const Type& type, const Assignments& assignments,
const std::vector<size_t>& unassigned_center_index,
const std::vector<size_t>& unassigned_other_index) {
std::vector<IndexPair> result_record; // temporary variable
result_record.reserve(assignments.size());
std::transform(assignments.begin(), assignments.end(),
std::back_inserter(result_record),
[&](const auto& assignment) {
return ...;
});
results_[type] = result_record;
}
以上代码中 std::vector<IndexPair>
生成后, 加入到std::unordered_map
会拷贝一次,之后会销毁。
由于这是一个临时变量,即将消亡,且支持移动操作,因此可使用 std::move()
转移其所有权到 std::unordered_map
中,可避免拷贝。
void CollectResult(const Type& type, const Assignments& assignments,
const std::vector<size_t>& unassigned_center_index,
const std::vector<size_t>& unassigned_other_index) {
std::vector<IndexPair> result_record;
result_record.reserve(assignments.size());
std::transform(assignments.begin(), assignments.end(),
std::back_inserter(result_record),
[&](const auto& assignment) {
return ...;
});
results_[type] = std::move(result_record); // transfer ownership
}
9. 定义移动构造函数、移动赋值运算符函数或右值引用函数
应视情况定义移动构造函数、移动赋值运算符函数或右值引用函数,以支持右值引用,提高程序性能。
若 Widget
类的定义如下:
class Widget {
private:
std::vector<int> data;
public:
Widget()=default;
Widget(const Widget& obj) { // copy constructor
data = obj.data;
}
};
Widget GetWidget() {
Widget w;
// ...
return w;
}
运行以下代码, GetWidget()
返回的是右值,但由于没有定义移动构造函数,会调用 Widget
的拷贝构造函数:
auto w = std::make_unique<Widget>(GetWidget());
添加移动构造函数的定义,则以上代码会调用 Widget
的移动构造函数:
class Widget {
....
Widget(Widget&& w) { // move constructor
data = std::move(w.data);
}
};
以下 ObjectList
类仅定义传入 objects
左值的构造函数,使用std::move()
也不能避免拷贝 ,仍会调用拷贝构造函数。
class ObjectList{
ObjectList(const std::deque<ObjectPtr>& objects);
// ...
};
ObjectList CreateObjectList() {
std::deque<ObjectPtr> objects;
// emplace object to objects
return ObjectList(std::move(objects));
// call ObjectList(const std::deque<ObjectPtr>&);
}
添加定义 `std::deque右值引用函数,以上代码会调用右值版本的构造函数,可避免拷贝。
class ObjectList{
ObjectList(const std::deque<ObjectPtr>& objects);
ObjectList(std::deque<ObjectPtr>&& objects) : objects_(std::move(objects));
// ...
};
10. 利用好Copy Elision
在C++17以后,编译期默认启用RVO(Return Value Optimization),不会对函数返回的局部变量值进行拷贝,直接在函数调用处进行构造,只要满足以下两个条件其一:
- URVO(Unnamed Return Value Optimization):函数的各分支都返回同一个类型的匿名变量。
- NRVO(Named Return Value Optimization):函数的各分支都返回同一个非匿名变量。
如以下代码,满足URVO
,
Widget GenerateWidget(const int a){
if ( a > 0) {
return Widget(1);
}
if ( a < -10) {
return Widget(2);
}
return Widget();
}
函数的各分支返回既有匿名变量又有非匿名变量,RVO失效,如以下代码:
Widget GenerateWidget(const int a){
Widget w;
if ( a > 0) {
return Widget(1);
}
if ( a < -10) {
return Widget(2);
}
return w;
}
不要对函数返回的临时值使用 std::move()
。
以下代码对函数返回的临时值使用了 std::move()
,导致多了一次临时变量的移动构造和析构过程, 破坏了 RVO
。
Widget GenerateWidget() {
Widget w;
process(w);
return std::move(w); // bad
}
测试程序:https://godbolt.org/z/7b14P8a71
当返回类型和临时值的类型或const语义不一致,需要隐式转换时, RVO
也不会生效。
如以下代码:
Widget f()
{
return DerivedWidget();
// constructs a temporary of type DerivedWidget,
// then initializes the returned Widget from the temporary
}
11. 容器预留空间
动态数组std::vector
有扩容机制,每次往容器中添加元素时,如果容器大小达到最大容量,会导致 std::vector
扩容,带来了内存重新分配和元素移动的开销。
std::vector<IndexPair> result_record;
std::transform(assignments.begin(), assignments.end(),
std::back_inserter(result_record), [&](const auto& assignment) {
return ...;
});
添加元素之前使用 .reserve()
方法为 std::vector
预留空间,避免容器扩容开销。
std::vector<IndexPair> result_record;
result_record.reserve(assignments.size()); // reserve capaticy
std::transform(assignments.begin(), assignments.end(),
std::back_inserter(result_record), [&](const auto& assignment) {
return ...;
});
对于std::unordered_map
,当加载因子load_factor达到一定阈值(通常为0.75),为避免哈希冲突过多,会进行rehash,从而导致较大的性能开销。当哈希表需要存储的元素较多时,同样可以使用 reserve()
方法可以减少rehash,提高性能,
unordered_map<int, Widget> umap;
umap.reserve(55000); // Reserve space for 55000 elements
加载因子过大会影响性能,可以通过 load_factor()
方法进行监控或通过 max_load_factor()
进行设置。
umap.max_load_factor(0.75); // Set max load factor
if (umap.load_factor() > 0.75) {
umap.rehash(umap.size() / 0.75); // Rehash if load factor exceeds 0.75
}
12. 容器内原地构造
若Widget
类的构造函数定义如下:
struct Widget{
Widget() = default;
Widget(const float x, const std::vector<int>& nums,
const std::string& name) : x_(x), nums_(nums), name_(name) {}
float x_ = 0.0F;
std::vector<int> nums_;
std::string name_;
};
std::vector
和std::deque
等的emplace_back()
方法
以下代码创建了 Widget
临时变量,拷贝到容器中后,再销毁,此过程额外调用了拷贝构造函数和析构函数,
Widget w(x, nums, name);
widget_list.emplace_back(w);
改用emplace_back()
方法则只需直接在容器内原地构造一次。
widget_list.emplace_back(x, nums, name);
unordered_map、map
插入元素时insert()
vsoperator[]
vsemplace()
定义这样一个unordered_map
,
std::unordered_map<std::string, Widget> myMap;
Widget w(x, nums, name);
当key不存在时,插入元素,会创建临时的 key-value pair
,
myMap.insert({"one", w1});
改为调用 emplace()
方法原地构造。
myMap.emplace("one", w1);
三种方法开销对比如下:
insert()
: 会创建临时的 key-value pair
以及将其拷贝进 myMap
容器,二者都会调用Widget的拷贝构造函数。
operator[]
: 该方法要求 mapped_type
是可默认构造的, 当key不存在时,myMap
中先分配了一个 {key, Widget()}
pair的空间,调用了 Widget
的默认构造函数,再用 Widget(1)
对其进行赋值,此过程调用默认构造函数和赋值运算符函数。
emplace()
:直接传入key-value作为参数,在容器中原地构造 std::pair
,省去了相关函数调用开销。
因此,当对效率要求较高,key不存在时,应优先使用 emplace()
插入key-value,避免临时变量带来的开销。
13. 容器存储指针代替对象拷贝
对容器元素进行筛选时直接存入原对象会导致拷贝开销,
std::unordered_map<int16_t, Object> unified_id_map_;
for (auto& object : msg->object_lists) {
if (unified_id_map_.find(object.id) == unified_id_map_.end()) {
unified_id_map_.emplace(object.id, object); // store by copy object
}
}
改为仅存储指针,开销较小。
std::unordered_map<int16_t, Object*> unified_id_map_;
for (auto& object : msg->object_lists) {
if (unified_id_map_.find(object.id) == unified_id_map_.end()) {
unified_id_map_.emplace(object.id, &object); // store pointer
}
}
14. 常量集合定义添加static
PointsKeys
为一个enum枚举类,以下函数每次调用都创建销毁一个常量集合从中查找:
void ProcessObject(const Object& object) {
std::unordered_set<PointsKeys> target_keys = {
PointsKeys::FirstLine,
PointsKeys::SecondLine,
PointsKeys::ThirdLine,
PointsKeys::ForthLine};
if (target_keys.count(object.key) > 0) {
//...
}
// ...
}
添加static const,使其保留在静态区,避免每次调用该函数都创建该常量集合。
void ProcessObject(const Object& object) {
static const std::unordered_set<PointsKeys> target_keys = {
PointsKeys::FirstLine,
PointsKeys::SecondLine,
PointsKeys::ThirdLine,
PointsKeys::ForthLine};
if (target_keys.count(object.key) > 0) {
//...
}
//..
}
对于定义在类中的常量集合,可以直接在类中定义为 inline static const
(since C++17),避免为每个实例创建该常量。
class Transform{
//...
inline static const std::unordered_map<uint8_t, ObjectType> type_map = {
{1U, ObjectType::PEDESTRIAN}, {2U, ObjectType::CAR},
{3U, ObjectType::BICYCLE}, {4U, ObjectType::TRICYCLE}};
};
15. 减少重复查找和判断
以下代码通过 count()
和 operator[]
在 std::unordered_map
查找了两次
if (myMap.count(a)) {
b = myMap[b];
}
改为通过 find()
方法查找一次,减少查找次数。
auto iter = myMap.find(a);
if (iter != myMap.end()) {
b = iter->second;
}
vector 的 operator[]
方法访问元素会比.at()
访问更快,由于前者不进行越界检查,直接访问,而后者反之。
以下代码通过.at()
访问,由于i的范围在for循环中已经被限定,循环中检查越界是多余的。
for(int i = 0; i < arr.size(); ++i ) {
arr.at(i);
}
改为通过 operator[]
访问,更加高效。
for(int i = 0; i < arr.size(); ++i ) {
arr[i];
}
16. 利用好constexpr编译期计算
现代C++的constexpr
关键字可用于支持编译期计算,可以用来定义constexpr变量和constexpr函数。所谓编译期计算,就是在编译阶段就得到计算结果。
以下a,b是变量。
欧式距离计算过程会进行开方运算,相对较慢,
if (std::sqrt(std::pow(a, 2) + std::pow(b, 2)) > 10) {
return true;
}
改为通过平方比较, 由于库函数std::pow()
被定义为了constexpr,若传入常量参数,会在编译期计算完结果。
if (std::pow(a, 2) + std::pow(b, 2)) > std::pow(10, 2)) {
return true;
}
以上代码在编译完成后等同于如下代码,可以减少运行时计算开销。
if(std::pow(a, 2) + std::pow(b, 2)) > 100){
return true;
}
另一个计算正切值反三角的例子,
if (atan2(a, b) < PI / 3) {
return true;
}
优化为如下代码,atan2的计算开销转移到了编译时,以及将除法改成了乘法,计算也会更快些。
if ( a < b * tan(PI / 3)) {
return true;
}
Reference
- https://en.cppreference.com/w/cpp/language/operators
- https://en.cppreference.com/w/cpp/container/map/operator_at
- https://en.cppreference.com/w/cpp/language/copy_elision
- http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/#rabbitref-NoBugs
如果本文内容对您有帮助,请点赞关注,鼓励我持续创作;如果对内容有疑问或者有更好的建议,欢迎在评论区留言。
希望进一步深入学习C++的朋友可以关注我的公众号:七昂的技术之旅。关注后回复"C++"送你一份学习资料。
C++: 16个基础的C++代码性能优化实例的更多相关文章
- 针对于Java的35 个代码性能优化总结
针对于Java的35 个代码性能优化总结前言代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的 ...
- Python 代码性能优化技巧(转)
原文:Python 代码性能优化技巧 Python 代码优化常见技巧 代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下使得程序的运行效率更高,根据 80/20 原则,实现程序的重构.优化. ...
- JavaScript代码性能优化总结
JavaScript 代码性能优化总结 尽量使用源生方法 javaScript是解释性语言,相比编译性语言执行速度要慢.浏览器已经实现的方法,就不要再去实现一遍了.另外,浏览器已经实现的方法在算法方面 ...
- Java开发代码性能优化总结
代码优化,可能说起来一些人觉得没用.可是我觉得应该平时开发过程中,就尽量要求自己,养成良好习惯,一个个小的优化点,积攒起来绝对是有大幅度效率提升的.好了,将平时看到用到总结的分享给大家. 代码优化的目 ...
- [转] Python 代码性能优化技巧
选择了脚本语言就要忍受其速度,这句话在某种程度上说明了 python 作为脚本的一个不足之处,那就是执行效率和性能不够理想,特别是在 performance 较差的机器上,因此有必要进行一定的代码优化 ...
- Python代码性能优化技巧
摘要:代码优化能够让程序运行更快,可以提高程序的执行效率等,对于一名软件开发人员来说,如何优化代码,从哪里入手进行优化?这些都是他们十分关心的问题.本文着重讲了如何优化Python代码,看完一定会让你 ...
- Python 代码性能优化技巧
选择了脚本语言就要忍受其速度,这句话在某种程度上说明了 python 作为脚本的一个不足之处,那就是执行效率和性能不够理想,特别是在 performance 较差的机器上,因此有必要进行一定的代码优化 ...
- Java代码性能优化的 39个细节
在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身.养成良好的编码习惯非常重要,能够显著地提升程序性能. 1:在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提 ...
- Java开发中程序和代码性能优化
现在计算机的处理性能越来越好,加上JDK升级对一些代码的优化,在代码层针对一些细节进行调整可能看不到性能的明显提升, 但是我觉得在开发中注意这些,更多的是可以保持一种性能优先的意识,对一些敲代码时间比 ...
- js代码性能优化的几个方法
相信写代码对于大部分人都不难,但想写出高性能的代码就需要一定的技术积累啦,下面是一些优化JavaScript代码性能的常见方法. 一.注意作用域 1.避免全局查找 使用全局变量和函数肯定要比局部的开销 ...
随机推荐
- 3.5 Y84-64的流水线实现
我们终于准备好要开始本章的主要任务--设计一个流水线化的Y86-64处理器.首先,对顺序的SEQ处理器做一点小的改动,将PC的计算挪到取指阶段.然后,在各个阶段之间加上流水线寄存器.到这个时候,我们的 ...
- 女朋友问我 LB 是谁?
科普一下 LB(负载均衡)技术 我的编程导航网站:www.code-nav.cn 大家好,我是鱼皮. 周末在家写代码,无意中跟女朋友提了下 LB,还说 LB 好的呱呱叫. 她笑了笑,问我 LB 是谁? ...
- 基于 Vagrant 手动部署多个 Redis Server
环境准备 宿主机环境:Windows 10 虚拟机环境:Vagrant + VirtualBox Vagrantfile 配置 首先,我们需要编写一个 Vagrantfile 来定义我们的虚拟机配置. ...
- 解决php提示Maximum execution time of 30 seconds exceeded错误
如何解决错误? 基本上,有3种方法可以处理此错误: 修改php配置文件php.ini文件 使用 ini_set() 函数 使用set_time_limit()函数 1)修改php配置文件php.ini ...
- [oeasy]python0023_[趣味拓展]Guido的简历_从ABC到python
Guido的简历 回忆上次内容 上次 添加了 各种 符号 铭文 各种 颜色 铸造了 自己的宝剑 添加图片注释,不超过 140 字(可选) 这些都是 用python画出来的宝剑 py ...
- oauth2协议
什么是OAUTH2协议: 首先是几个概念问题: 资源:用户信息,在微信中存储 资源拥有者:用户 认证服务:微信负责认证用户的身份,也负责为客户端颁发令牌 客户端:携带令牌请求微信获取用户信息 仍以微信 ...
- leetcode2397. 被列覆盖的最多行数 回溯法/枝剪
第一次手搓一个回溯法,超时后采用枝剪勉强通过 class Solution { int max=0; int numSelect; public int maximumRows(int[][] mat ...
- Jmeter参数化5-JSON提取器
后置处理器[JSON提取器] ,一般放于请求接口下面,用于获取接口返回数据里面的json参数值 1.以下json为例,接口返回的json结果有多组数据.我们要取出purOrderNo值 2.在jmet ...
- 【Linux】00 Docker下载安装(CentOS8)
官方安装文档: https://docs.docker.com/engine/install/centos/ 先全部卸载Docker有无关系的一些环境 [保证一个干净的部署环境] sudo yum r ...
- Continue-AI编程助手本地部署llama3.1+deepseek-coder-v2
领先的开源人工智能代码助手.您可以连接任何模型和任何上下文,以在 IDE 内构建自定义自动完成和聊天体验 推荐以下开源模型: 聊天:llama3.1-8B 推理代码:deepseek-coder-v2 ...