C++构造函数性能
C++ constructor performance
在 C++17 中,如果我们设计这样的类:
class Editor {
public:
// "copy" constructor
Editor(const std::string& text) : _text {text} {}
// "move" constructor
Editor(std::string&& text) : _text {std::move(text)} {}
private:
std::string _text;
}
看起来(至少对我来说),"移动"构造函数应该比"复制"构造函数快得多。
但是,如果我们尝试测量实际时间,我们会看到一些不同的东西:
int current_time()
{
return chrono::high_resolution_clock::now().time_since_epoch().count();
}
int main()
{
int N = 100000;
auto t0 = current_time();
for (int i = 0; i < N; i++) {
std::string a("abcdefgh"s);
Editor {a}; // copy!
}
auto t1 = current_time();
for (int i = 0; i < N; i++) {
Editor {"abcdefgh"s};
}
auto t2 = current_time();
cout << "Copy: " << t1 - t0 << endl;
cout << "Move: " << t2 - t1 << endl;
}
复制和移动时间在同一范围内。下面是其中一个输出:
Copy: 36299550
Move: 35762602
我尝试使用只要285604
个字符的字符串,结果相同。
问题:"复制"构造函数Editor(std::string& text) : _text {text} {}
为什么这么快?它实际上不是创建输入字符串的副本吗?
更新我使用以下行运行此处给出的基准测试:g++ -std=c++1z -O2 main.cpp && ./a.out
更新 2修复移动构造函数,正如@Caleth所建议的那样(从const std::string&& text
中删除const
)改进了事情!
Editor(std::string&& text) : _text {std::move(text)} {}
现在基准测试如下所示:
Copy: 938647
Move: 64
这也取决于您的优化标志。如果没有优化,你可以(我做到了!)得到更糟糕的结果:
Copy: 4164540
Move: 6344331
使用 -O2 优化运行相同的代码会给出截然不同的结果:
Copy: 1264581
Move: 791
在Wandbox上观看直播。
这就是 clang 9.0。在GCC 9.1上,-O2和-O3的区别大致相同,但复制和移动之间的差异并不那么明显:
Copy: 775
Move: 508
我猜这是一个小的字符串优化。
通常,标准库中的容器最适合优化,因为它们具有许多小函数,编译器可以在被要求时轻松内联和折叠。
同样在第一个构造函数中,根据 Herb Sutter,"如果您无论如何都要复制参数,则更喜欢按值传递只读参数,因为它可以从 rvalue 参数移动。
更新:对于非常长的字符串(300k 个字符),GCC 9.1 和优化的结果类似于上述(现在使用以毫秒为单位的std::chrono::duration
以避免 int 溢出):
Copy: 22560
Move: 1371
并且没有优化:
Copy: 22259
Move: 1404
const std::string&&
看起来像一个错字。
您无法从中移动,因此您可以获得副本。
所以你的测试实际上是在看我们必须"构建"一个字符串对象的次数。
所以在拳头测试中:
for (int i = 0; i < N; i++) {
std::string a("abcdefgh"s); // Build a string once.
Editor {a}; // copy! // Here you build the string again.
} // So basically two expensive memory
// allocations and a copying the string
在第二个测试中:
for (int i = 0; i < N; i++) {
Editor {"abcdefgh"s}; // You build a string once.
// Then internally you move the allocated
// memory (so only one expensive memory
// allocation and copying the string
}
因此,两个循环之间的区别是一个额外的字符串副本。
问题就在这里。作为一个人类,我可以发现一个简单的窥视孔优化(编译器比我好)。
for (int i = 0; i < N; i++) {
std::string a("abcdefgh"s); // This string is only used in a single
// place where it is passed to a
// function as a const parameter
// So we can optimize it out of the loop.
Editor {a};
}
因此,如果我们在循环外手动拉动字符串(相当于有效的编译器优化)。
所以这个循环具有相同的影响:
std::string a("abcdefgh"s);
for (int i = 0; i < N; i++) {
Editor {a};
}
现在这个循环只有 1 个分配和副本。所以现在两个循环在昂贵的操作方面看起来是一样的。
现在作为一个人,我不会(快速)发现所有可能的优化。我只是想在这里指出,您在这里的快速测试不会发现编译器将要做的很多优化,因此像这样的估计和计时很难。
在纸面上你是对的,但在实践中这很容易优化,所以你可能会发现编译器已经破坏了你的基准测试。
您可以在关闭"优化"的情况下进行基准测试,但这本身对现实世界几乎没有好处。也许可以通过添加一些阻止这种优化的代码来欺骗发布模式下的编译器,但在我的头顶上,我无法想象这会是什么样子。
它也是一个相对较小的字符串,现在可以非常快速地复制。
我认为你应该相信你的直觉(因为它是正确的),同时记住在实践中它实际上可能不会产生太大的区别。但此举肯定不会比复制更糟糕。
有时我们可以而且应该编写明显"更有效"的代码,而无法证明它在一周中的任何特定日期在月球/行星对齐的任何特定阶段实际上会表现得更好,因为编译器已经在尝试使你的代码尽可能快。
人们可能会告诉你,因此这是一个"过早的优化",但事实并非如此:这只是明智的代码。
- "error: no matching function for call to"构造函数错误
- C++17复制构造函数,在std::unordereded_map上进行深度复制
- 如果C++类在类方法中具有动态分配,但没有构造函数/析构函数或任何非静态成员,那么它仍然是POD类型吗
- 为什么在没有显式默认构造函数的情况下,将另一个结构封装在联合中作为成员的结构不能编译
- 为什么在C++中使用私有复制构造函数与删除复制构造函数
- 选择要调用的构造函数
- 如何委托派生类使用其父构造函数?
- 构造函数正在调用一个使用当前类类型的函数
- 没有用于初始化C++中的变量模板的匹配构造函数
- 初始化具有非默认构造函数的std::数组项的更好方法
- 为什么使用默认构造函数"{}"而不是"= default"存在性能变化?
- 通过默认复制构造函数比较 C++ 字符串是否会影响性能,原因为何?
- 性能 - 使用字符串构造函数与使用串联
- C++构造函数性能
- 课堂初始化(分配样式)与构造函数性能
- 只在模板中专门化构造函数,保持最佳性能和整洁的界面
- std::copy 和容器的复制构造函数之间是否存在任何性能差异?
- 在 C++98 中实现移动构造函数和移动赋值运算符以获得更好的性能
- 初始化方法与构造函数加赋值的方法——性能有什么不同?(C++)
- 构造函数 - 关于性能