C++Tips
[!quote] Intuition is the supra-logic that cuts out all the routine processes of thought and leaps straight from the problem to the answer. — Robert Graves
C++的零成本抽象相关概念需要注意的点。C++要达到运行时额外开销,达到零成本执行代码逻辑,需要额外注意代码写法(自由的代价)。
虚函数
虚函数的代价: - 虚函数表中转,多一次寻址 - 间接调用函数,可能影响分支预测,导致降低CPU指令流水线执行效率(当然,这需要看实际场景,以及编译器优化) - 无法内联,这是最致命的 如果只是需要多态特性,如果利用编译器多态不太麻烦,那么是一种提升执行效率的方法。
隐藏拷贝
对于非平凡类型(trivial,涉及堆内存分配,不只有基础数值类型),在
member initialization 中需要注意写法 1
2
3
4
5
6class A {
public:
A(B b): b_(std::move(b)) {}
private:
B b_;
}
对于 for 循环遍历复杂类型的容器,注意使用 type&,而不是 type。
对于 lambda 捕获,注意使用 &val 或者 std::move(val) (>=C++14)。
对于复杂类型,注意发生隐式类型转换,比如: 1
2
3
4std::unordered_map<int, std::string> map;
for(const std::pair<int, std::string>& p: map){
...
}std::pair<const int, std::string>
,而不是代码中声明的
std::pair<int, std::string>
的 const &。 1.
for
循环从 map
中取出一个元素,这个元素的真实类型是
const std::pair<const int, std::string>&
。 2.
编译器发现需要将这个元素绑定到你的引用变量 p
上。 3.
但是,const std::pair<int, std::string>&
和
const std::pair<const int, std::string>&
是两种不同的类型。一个非const的引用不能绑定到一个需要类型转换的对象上。
4.
因此,编译器找不到匹配的引用绑定规则,它会尝试寻找其他方式。它发现可以创建一个临时的、类型为
std::pair<int, std::string>
的对象。 5.
为了创建这个临时对象,编译器会调用 std::pair
的拷贝构造函数,将 map
中的原始元素(std::pair<const int, std::string>
)拷贝到这个新的临时对象中。
所以在遍历时,推荐使用const
auto&,对于map类型,也可以使用结构化绑定。 结构化绑定(Structured
Binding)是 C++17 引入的一个非常方便的特性,在 python 中很常见。
1
for (const auto& [key, value] : map) { ... }
隐式析构
使用 RAII 以及作用域自动析构时,需要意识到,被自动调用的析构函数,也许会引入当前代码之外的额外开销。 比如,一个代码块中,某一个对象析构时需要做计时,资源回收等动作,那么,调用者需要时刻提醒自己,不要忘记这部分开销。如果可以缓存特殊对象,以复用,那么缓存它。
平凡析构请使用 = default
1 |
|
前一个实现中,非 default 的 nontrivial 类型析构,如果加入 {} 析构,可能导致编译器优化失效,保留了无意义的赋值指令。无论是否是编译器差异,建议使用后一种。
关于C++中指针的使用指导意见是: 1. 不涉及资源所有权时,使用裸指针。
2. 使用 unique_ptr 或者 shared_ptr 都引入资源所有权管理。 3.
除非需要共享资源所有权,否则使用 unique_ptr。 shared_ptr
的构造/复制/析构,涉及原子操作,会比裸指针和 unique_ptr 慢
10%到20%。shared_ptr 也要尽量避免拷贝。 std::shared_ptr
一定要使用std::make_shared<T>()
而不是std::shared_ptr<T>(new T)
来构造,因为后者会分配两次内存,且原子计数和数据本身的内存是不挨着的,不利于cpu缓存。
如果不是因为懒,那么使用 shared_ptr 常见场景是:缓存,异步资源析构。
std::function和std::any 代价
类型擦除类型在 C++ 中是有代价的: 1. std::function要占用32个字节,而函数指针只需要8个字节 2. std::function本质上是一个虚函数调用,因此虚函数的问题std::function都有,无法内联 3. std::function可能涉及堆内存分配,比如用lambda捕获时,用std::function封装会需要在堆上分配内存
除了作为存储不确定类型的函数,其它不推荐使用。如果需要多态函数调用,建议使用编译静态模板分发。
std::any同理,也不推荐作为不确定类型存储之外的其它用途。
std::variant和std::optional 代价
- 多余内存开销:std::optional有两个成员变量,类型分别为bool和T,由于内存对齐的原因,sizeof(std::optional)最多会是sizeof(T)的两倍。相比之下,rust语言的option实现则有null pointer optimization,即如果一个类的合法内存表示一定不会全部字节为零,比如std::reference_wrapper,那就可以零开销地表示std::optional,而C++由于需要兼容C的内存对齐,不可能实现这项优化
- gcc 8.0.0 之前 std::optional 被作为 trivial 类型,导致 T 如果是 nontrivial ,那么必须也是 nontrivial 的 std::optional 和 gcc 冲突,出现问题。
- NRVO 不友好(NRVO
是:当一个函数的返回值是当前函数内的一个局部变量,且该局部变量的类型和返回值一致时,编译器会将该变量直接在函数的返回值接收处构造,不会发生拷贝和移动)
1
2
3
4
5
6
7
8
9
10
11
12
13std::optional<A> f() {
A v = A();
return v;
}
// 应该改为
```cpp
std::optional<A> f() {
std::optional<A> v;
v = A();
return v;
}是同步执行的,而1
2
3
4
5
6
7
### std::async
如果 async 使用了默认的 std::launch::deferred policy 执行,那么不会异步执行,而是在 future.get() 是才开始延迟执行,变成同步调用。
async 返回的 future,它析构时,会同步等待函数返回结果才析构结束。所以
```cpp
std::async(std::launch::async, func1);
std::async(std::launch::async, func2);才是异步执行的。1
2auto future1 = std::async(std::launch::async, func1);
auto future2 = std::async(std::launch::async, func2);
然后,std::packaged_task 和 std::promise 构造的 std::future 却不会在析构时同步等待,需要注意。
滥用 std::move
move 在以下场景下无用: 1. 对象是 nontrivial 类型 2. 对象是常量引用
以下场景下为副作用: - 影响编译器 NRVO 1
2
3
4A f() {
A v = A();
return std::move(v); // NRVO 失效
}
尾递归优化
如果某个函数的最后一步操作是调用自身,那么编译器完全可以不用调用的指令(call),而是用跳转(jmp)回当前函数的开头,省略了新开调用栈的开销。 C++ 中,坏在运行时隐式操作,比如析构函数。
1 |
|
此处,由于 input.~string() 这个 trivial
析构的存在,不可平凡析构,导致尾递归失效(有副作用)。 如果换成
std::string_view,这个可以 平凡析构 的 nontrivial
类型,编译器根本不需要调用析构函数,则不影响尾递归优化。 1
2
3
4
5
6
7
8unsigned btd_tail(std::string_view input, int v) {
if (input.empty()) {
return v;
} else {
v = v * 2 + (input.front() - '0');
return btd_tail(input.substr(1), v);
}
}
向量化
编译器一般会利用现代CPU的向量化指令如 SSE/AVX 等 SIMD
操作指令。但是存在条件: 1. 循环内部访问连续内存 2. 没有 if 分支 3.
循环之间没有依赖 比如: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15enum Type { kAdd, kMul };
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
std::vector<int> func(std::vector<int> a, std::vector<int> b, Type t) {
std::vector<int> c(a.size());
for (int i = 0; i < a.size(); ++i) {
if (t == kAdd) {
c[i] = add(a[i], b[i]);
} else {
c[i] = mul(a[i], b[i]);
}
}
return c;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16enum Type { kAdd, kMul };
inline __attribute__((always_inline)) int add(int a, int b) { return a + b; }
inline __attribute__((always_inline)) int mul(int a, int b) { return a * b; }
template <Type t>
std::vector<int> func(std::vector<int> a, std::vector<int> b) {
std::vector<int> c(a.size());
for (int i = 0; i < a.size(); ++i) {
if constexpr (t == kAdd) {
c[i] = add(a[i], b[i]);
} else {
c[i] = mul(a[i], b[i]);
}
}
return c;
}
End
C++ 的自由度,一定程度上使得高性能没那么容易实现,但是也正是因为它的灵活性,让我更喜欢这门语言,而不是目前常用的 Rust。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!