令人惊讶的基准测试结果。

5
在观看Titus Winters的"Live at Head"演讲后,他提到StrCat()是人们最喜欢的功能之一,我决定尝试实现类似的东西,看看是否能在运行时性能方面击败std::string::append(或operator+,我认为它在内部使用append)。我的想法是,作为可变参数模板实现的strcat()函数将能够确定其所有类似字符串的参数的组合大小,并进行单个分配以存储最终结果,而不是在operator+的情况下不断重新分配,因为它没有关于调用上下文的整体知识。
然而,当我在quick-bench上将自定义实现与operator+进行比较时,我发现我的strcat()实现在最近版本的clang和gcc上比operator+慢约4倍,编译选项为-std=c++17 -O3。我已经包含了以下快速测试代码以供参考。
有人知道这里可能导致减速的原因吗?
#include <cstring>
#include <iostream>
#include <string>

// Get the size of string-like args
int getsize(const std::string& s) { return s.size(); }
int getsize(const char* s) { return strlen(s); }
template <typename S>
int strcat_size(const S& s) {
  return getsize(s);
}
template <typename S, typename... Strings>
int strcat_size(const S& first, Strings... rest) {
  if (sizeof...(Strings) == 0) {
    return 0;
  } else {
    return getsize(first) + strcat_size(rest...);
  }
}

// Populate a pre-allocated string with content from another string-like object
template <typename S>
void strcat_fill(std::string& res, const S& first) {
  res += first;
}
template <typename S, typename... Strings>
void strcat_fill(std::string& res, const S& first, Strings... rest) {
  res += first;
  strcat_fill(res, rest...);
}

template <typename S, typename... Strings>
std::string strcat(const S& first, Strings... rest) {
  int totalsize = strcat_size(first, rest...);

  std::string res;
  res.reserve(totalsize);

  strcat_fill(res, first, rest...);

  return res;
}

const char* s1 = "Hello World! ";
std::string s2 = "Here is a string to concatenate. ";
std::string s3 = "Here is a longer string to concatenate that avoids small string optimization";
const char* s4 = "How about some more strings? ";
std::string s5 = "And more strings? ";
std::string s6 = "And even more strings to use!";

static void strcat_bench(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  for (auto _ : state) {
    std::string s = strcat(s1, s2, s3, s4, s5, s6);

    benchmark::DoNotOptimize(s);
  }
}
BENCHMARK(strcat_bench);

static void append_bench(benchmark::State& state) {
  for (auto _ : state) {
    std::string s = s1 + s2 + s3 + s4 + s5 + s6;

    benchmark::DoNotOptimize(s);
  }
}
BENCHMARK(append_bench);

我可能错了,但我认为可变参数模板(就像你写的那样)会引起复制。你需要通过Strings&&... rest接受并使用std::forward,就像这个答案中所示。 - kmdreko
2
@vu1p3n0x 你说得对。完美转发使它变得更快。但在这种情况下,它似乎和通过“const”引用传递一样好。 - HolyBlackCat
1个回答

8
那是因为按值传递参数。
我改变了代码,使用折叠表达式代替了原来的方式(看起来更简洁),
并且消除了不必要的副本(`Strings... rest` 应该是一个引用)。
int getsize(const std::string& s) { return s.size(); }
int getsize(const char* s) { return strlen(s); }

template <typename ...P>
std::string strcat(const P &... params)
{
  std::string res;
  res.reserve((getsize(params) + ...));
  (res += ... += params);
  return res;
}

这个解决方案比使用append快大约30%。


在这种情况下,通过const引用传递和进行完美转发似乎没有区别。这是有道理的,因为即使它们是右值,std::string +=也不会移动它的参数。


如果您无法使用新的花式折叠表达式但仍希望获得性能,请改用“虚拟数组”技巧(在这种情况下似乎具有完全相同的性能)。

template <typename ...P>
std::string strcat(const P &... params)
{
  using dummy_array = int[]; // This is necessary because `int[]{blah}` doesn't compile.
  std::string res;
  std::size_t size = 0;
  dummy_array{(void(size += getsize(params)), 0)..., 0};
  res.reserve(size);
  dummy_array{(void(res += params), 0)..., 0};
  return res;
}

哇,你的版本简洁多了!我没有使用C++17的功能,但是只需将原始代码中的“Strings…”参数更改为const ref确实显著提高了性能,尽管我看到的改进接近[50%](http://quick-bench.com/3fW8eFEMHax8_D4KoDEWIycT1r0)。 - AUD_FOR_IUV
@AUD_FOR_IUV 也许我搞砸了我的基准测试;我编辑了答案。另外,正如我所说,你可以使用虚拟数组代替折叠表达式。 - HolyBlackCat

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