C++构造函数性能

C++ constructor performance

本文关键字:性能 构造函数 C++      更新时间:2023-10-16

在 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 个分配和副本。所以现在两个循环在昂贵的操作方面看起来是一样的。

现在作为一个人,我不会(快速)发现所有可能的优化。我只是想在这里指出,您在这里的快速测试不会发现编译器将要做的很多优化,因此像这样的估计和计时很难。

在纸面上你是对的,但在实践中这很容易优化,所以你可能会发现编译器已经破坏了你的基准测试。

您可以在关闭"优化"的情况下进行基准测试,但这本身对现实世界几乎没有好处。也许可以通过添加一些阻止这种优化的代码来欺骗发布模式下的编译器,但在我的头顶上,我无法想象这会是什么样子。

它也是一个相对较小的字符串,现在可以非常快速地复制。

我认为你应该相信你的直觉(因为它是正确的),同时记住在实践中它实际上可能不会产生太大的区别。但此举肯定不会比复制更糟糕。

有时我们可以而且应该编写明显"更有效"的代码,而无法证明它在一周中的任何特定日期在月球/行星对齐的任何特定阶段实际上会表现得更好,因为编译器已经在尝试使你的代码尽可能快。

人们可能会告诉你,因此这是一个"过早的优化",但事实并非如此:这只是明智的代码。