为什么std::copy比std::string构造函数更快?

7
我尝试了这些代码,来比较std::copy和std::string的构造函数。
#include <chrono>
#include <iostream>
#include <vector>

void construct_test() {
  std::vector<uint8_t> raw_data;
  for (int i = 0; i < 1000 * 1024; i++) {
    raw_data.push_back(i % 256);
  }

  auto start = std::chrono::high_resolution_clock::now();
  std::string target_data;
  target_data = std::string(raw_data.begin(), raw_data.end());
  auto finish = std::chrono::high_resolution_clock::now();
  std::cout << "construct: " << std::chrono::duration_cast<std::chrono::microseconds>(finish -
                                                                     start)
                   .count()
            << "us" << std::endl;
}

void copy_test() {
  std::vector<uint8_t> raw_data;
  for (int i = 0; i < 1000 * 1024; i++) {
    raw_data.push_back(i % 256);
  }

  auto start = std::chrono::high_resolution_clock::now();
  std::string target_data;
  target_data.resize(raw_data.size());
  std::copy(raw_data.begin(), raw_data.end(), target_data.begin());
  auto finish = std::chrono::high_resolution_clock::now();
  std::cout << "copy: " << std::chrono::duration_cast<std::chrono::microseconds>(finish -
                                                                     start)
                   .count()
            << "us" << std::endl;
}

int main() {
  construct_test();
  copy_test();

  return 0;
}


我得到了结果:
construct: 6245us
copy: 1087us

std::copy的速度快了6倍!

这是否符合预期?如果是,原因是什么?
我搜索了很多将向量转换为字符串的方法,但没有人提到std::copy的方式。我应该使用这种方式吗?有什么缺点吗?


8
编写良好的性能测试很困难。主要的陷阱是as-if规则。这个规则可以使代码更快,但是测试小而紧密的代码性能变得困难,因为现在你必须与优化器作斗争,并确保优化器没有删除被测试的代码。所以,在读取您的代码之前,我非常确定这是主要问题。 - Marek R
4
你是怎么编译的?请注意,high_resolution_clock 不应该用于基准测试。请使用 steady_clock - 463035818_is_not_an_ai
3
在这两种情况下,您并未使用target_data来产生任何可观察的效果。当开启优化时,预计这些调用将被优化掉。 - 463035818_is_not_an_ai
3
construct_test()copy_test()的顺序调换一下。现在复制的速度变慢了。https://ideone.com/aemRow - VLL
4
就此而言,有 https://quick-bench.com/ 这个网站,但即使是使用它也需要一些经验才能正确使用,否则可能会产生误导性的结果。 - 463035818_is_not_an_ai
显示剩余8条评论
1个回答

8

正如评论者所指出的那样,您的测试方法是严重有缺陷的。通常情况下,为了获得有意义的结果,您必须运行多次操作(可能是数百万或数十亿次)。否则,运行基准测试的顺序、调度等可能会给您带来截然不同的结果。

  • @463035818_is_not_an_ai指出,在基准测试中应使用steady_clock而不是high_resolution_clock。(尽管在这种情况下这不太可能产生重大影响)
  • @VLL指出,仅改变construct_test()copy_test()的顺序就可以使一个函数运行得比另一个函数快

您可以使用google/benchmark(被QuickBench使用)来获得更有意义的结果。

除了您使用的两种方法外,还至少有两种方法来创建/覆盖字符串:

// BenchmarkInit
std::string target_data = std::string(raw_data.begin(), raw_data.end());

// BenchmarkAssignmentOp
std::string target_data;
target_data = std::string(raw_data.begin(), raw_data.end());

// BenchmarkAssign
std::string target_data;
target_data.assign(raw_data.begin(), raw_data.end());

// BenchmarkCopy
std::string target_data;
target_data.resize(raw_data.size());
std::copy(raw_data.begin(), raw_data.end(), target_data.begin());

我们对clang 15、libstdc++和-O3进行了以下基准测试结果enter image description here
  • 无论是否存在不必要的默认初始化,使用std::string构造函数都是最好的选择。前两种方法在内部使用std::memcpy,这应该是最快的内存复制方式。
  • std::copy较慢,可能是因为.resize()需要先将内存清零,并且没有被优化成memcpy,而是被优化成矢量化内存操作。
  • .assign明显较慢,可能是因为与std::copy相比,循环展开较少,所以除了复制内存之外还有很多额外开销。

即使进行了适当的基准测试,你仍然会看到意想不到且显著的差异,只有在查看汇编代码时才能理解其中的原因。

更新

我已经提交了一个错误报告,这个性能问题已经由@JonathanWakely修复。


1
你可以尝试使用reserve而不是resize,并在copy调用中使用back_inserter。有趣的是,看看这样做是否能使其达到与构造函数和赋值相同的水平。 - 463035818_is_not_an_ai
首先,我回顾了我的测试环境。我在VS Code中使用Code Runner扩展来进行测试,它的命令行只是g++ $filename,没有任何编译选项。而且我不知道它使用的是哪个版本。然后我尝试了评论者提供的编译选项,结果发现std::copystd::string()相似。现在我明白了,即使我使用相同的编译器、相同的选项、相同的电脑,它们的表现也可能不同。我还有很长的路要探索。 - Jason.Pu
1
@Jason.Pu -- g++ $filename -- 对一个未经优化的构建进行计时是没有意义的。从一开始,由于你构建程序的方式,你的计时是毫无意义的。 - PaulMcKenzie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接