近期推动项目屎山代码进行了一波性能优化,实现了较大的性能提升。这里记录了部分近期代码优化的小技巧,这些例子仅从C++语言层面进行优化,主要在于优化类设计、减少隐含函数调用、减少拷贝等,较为基础实用,但涉及的知识点并不少。本文提供了一个视角,可以帮助了解一些C++代码的不同写法性能开销差异。对于很少关注代码性能的人,或许可以看看,提升一下代码性能方面的意识,从而写出性能更高的程序。
通过值传递给函数时会创建临时变量。
void process(vector<int> s);
改为引用传递:
void process(const vector<int>& s);
同理,对类成员访问接口:
Eigen::VectorXd GetStates() const { return current_states_;}
应改为返回 const&,
const Eigen::VectorXd& GetStates() const { return current_states_;}
保存函数返回的引用时,为避免拷贝,同样需要引用:
const auto& states = GetStates();
返回引用避免拷贝还需要避免隐式转换的发生,否则还是会拷贝,详见第3条。
以下for循环,每次从 object_list 容器拷贝临时对象 obj,
for(auto obj: object_list) { obj->func();}
改为 const auto& 避免拷贝。
for(const auto& obj: object_list) { obj->func();}
比如在以下代码中, GetObject() 虽然返回引用类型,但由于返回类型的const语义不一致,实际调用该函数后还是会导致返回临时变量。
class Widget {public: const std::shared_ptr<const object>& GetObject() { return object_; }private: std::shared_ptr<Object> object_;};
比如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& 自动推断类型。
以下代码会调用一次默认构造函数,一次赋值运算符函数;
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
以下代码在循环中创建、销毁临时变量,多次调用拷贝构造函数和析构函数,
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);}
通常operator+=() 等复合运算符实现的形式会返回自身的引用。假如 X 类是一种矩阵类,支持四则运算。
以下代码导致临时变量的创建,
a = (a + b) * c;
以上代码相当于以下过程:
X tmp1(a + b);X tmp2(tmp1 * c);a = tmp2;
operator+=() 和 operator*=() 改写可以避免临时变量的创建。
a += b;a *= c;
以下代码中 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 listprivate: int num_ = 0; Attributes attributes_; std::string name_;}
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}
应视情况定义移动构造函数、移动赋值运算符函数或右值引用函数,以支持右值引用,提高程序性能。
若 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)); // ...};
在C++17以后,编译期默认启用RVO(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}
动态数组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 capaticystd::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 factorif (umap.load_factor() > 0.75) { umap.rehash(umap.size() / 0.75); // Rehash if load factor exceeds 0.75}
若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_;};
以下代码创建了 Widget 临时变量,拷贝到容器中后,再销毁,此过程额外调用了拷贝构造函数和析构函数,
Widget w(x, nums, name);widget_list.emplace_back(w);
改用emplace_back()方法则只需直接在容器内原地构造一次。
widget_list.emplace_back(x, nums, name);
定义这样一个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,避免临时变量带来的开销。
对容器元素进行筛选时直接存入原对象会导致拷贝开销,
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 }}
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}};};
以下代码通过 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]; }
现代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;}
如果本文内容对您有帮助,请点赞关注,鼓励我持续创作;如果对内容有疑问或者有更好的建议,欢迎在评论区留言。
希望进一步深入学习C++的朋友可以关注我的公众号:七昂的技术之旅。关注后回复"C++"送你一份学习资料。