为什么 StringCopyFromLiteral 比 StringCopyFromString 快?

Why StringCopyFromLiteral is faster than StringCopyFromString?

Quick C++ Benchmarks例子:

static void StringCopyFromLiteral(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  for (auto _ : state) {
    std::string from_literal("hello");
    // Make sure the variable is not optimized away by compiler
    benchmark::DoNotOptimize(from_literal);
  }
}
// Register the function as a benchmark
BENCHMARK(StringCopyFromLiteral);

static void StringCopyFromString(benchmark::State& state) {
  // Code before the loop is not measured
  std::string x = "hello";
  for (auto _ : state) {
    std::string from_string(x);
  }
}
// Register the function as a benchmark
BENCHMARK(StringCopyFromString);

http://quick-bench.com/IcZllt_14hTeMaB_sBZ0CQ8x2Ro

如果我懂汇编怎么办...


更多结果:

http://quick-bench.com/39fLTvRdpR5zdapKSj2ZzE3asCI

答案很简单。在从一个小字符串文字构造 std::string 的情况下,编译器通过在汇编中使用常量直接填充字符串对象的内容来优化这种情况。这避免了昂贵的循环以及用于查看是否可以应用小字符串优化 (SSO) 的测试。在这种情况下,它知道可以应用 SSO,因此编译器生成的代码只涉及将字符串直接写入 SSO 缓冲区。

注意 StringCreation 案例中的这段汇编代码:

// Populate SSO buffer (each set of 4 characters is backwards since
// x86 is little-endian)
19.63% movb   [=10=]x6f,0x4(%r15)    // "o"
19.35% movl   [=10=]x6c6c6568,(%r15) // "lleh"
// Set size
20.26% movq   [=10=]x5,0x10(%rsp)    // size = 5
// Probably set heap pointer. 0 (nullptr) = use SSO buffer
20.07% movb   [=10=]x0,0x1d(%rsp)

您正在查看那里的常量值。这不是很多代码,也不需要循环。事实上,std::string 构造函数 甚至不必调用! 编译器只是将内容放入内存中与 std::string 构造函数相同的位置.

如果编译器无法应用此优化,结果将大不相同——特别是,如果我们 "hide" 通过首先将文字复制到 char 数组来确定源是字符串文字,则结果翻转:

char x[] = "hello";
for (auto _ : state) {
  std::string created_string(x);
  benchmark::DoNotOptimize(created_string);
}

现在 "from-char-pointer" 案子 twice as long!为什么?

怀疑 这是因为 "copy from char pointer" 情况不能简单地通过查看值来检查字符串的长度。它需要知道是否可以进行小字符串优化。有几种方法可以解决这个问题:

  1. 首先测量字符串的长度,进行分配(如果需要),然后将源复制到目标。在 SSO 确实适用的情况下(几乎肯定适用于此),我预计这将花费两倍的时间,因为它必须遍历源两次——一次测量,一次复制。
  2. 逐个字符地从源中复制,附加到新字符串。这需要在每个附加操作上测试字符串现在对于 SSO 是否太长并且需要复制到堆分配的 char 数组中。如果该字符串当前位于堆分配数组中,则需要改为测试是否需要调整分配大小。这也需要相当长的时间,因为源字符串中的每个字符至少有一个测试。
  3. 以块的形式从源代码复制以减少需要执行的测试数量并避免两次遍历源代码。这将比逐个字符的方法更快,因为测试的数量会更少,而且因为源没有被遍历两次,CPU 内存缓存将更有效。这只会显示长字符串的显着速度改进,我们这里没有。对于短字符串,它的工作方式与第一种方法(测量,然后复制)大致相同。

对比一下它从另一个字符串对象复制的情况:它可以简单地查看另一个字符串的size()并立即知道它是否可以执行 SSO,如果不能执行 SSO然后它也确切知道为新字符串分配多少内存。