令人惊讶的基准测试结果
Surprising Benchmark Result
看完 Titus Winters 的 "Live at Head" 演讲后,他提到 StrCat() 是人们最喜欢的功能之一,我决定尝试实现类似的东西,看看我是否可以击败 std::string::append (或 operator+,我认为它在内部使用 append)在运行时性能方面。我的理由是,作为可变参数模板实现的 strcat() 函数将能够确定其所有类似字符串的参数的组合大小,并进行一次分配以存储最终结果,而不必在以下情况下不断重新分配operator+,不知道调用它的总体上下文。
但是,当我将我的自定义实现与 quick-bench 上的 operator+ 进行比较时,我发现我的 strcat() 实现比使用 [=11 编译的 clang 和 gcc 的最新版本的 operator+ 慢大约 4 倍=].我在下面包含了快速基准代码以供参考。
有谁知道是什么导致了这里的减速?
#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
应该是一个参考)。
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;
}
This solution beats append
by approximately 30%.
在这种情况下,通过const
引用和做完美转发似乎没有区别。这是有道理的,因为 std::string +=
不会移动它的参数,即使它们是右值。
如果您无法使用新的花哨的折叠表达式但仍然想要性能,请改用 'dummy array' 技巧(在这种情况下似乎具有完全相同的性能)。
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;
}
看完 Titus Winters 的 "Live at Head" 演讲后,他提到 StrCat() 是人们最喜欢的功能之一,我决定尝试实现类似的东西,看看我是否可以击败 std::string::append (或 operator+,我认为它在内部使用 append)在运行时性能方面。我的理由是,作为可变参数模板实现的 strcat() 函数将能够确定其所有类似字符串的参数的组合大小,并进行一次分配以存储最终结果,而不必在以下情况下不断重新分配operator+,不知道调用它的总体上下文。
但是,当我将我的自定义实现与 quick-bench 上的 operator+ 进行比较时,我发现我的 strcat() 实现比使用 [=11 编译的 clang 和 gcc 的最新版本的 operator+ 慢大约 4 倍=].我在下面包含了快速基准代码以供参考。
有谁知道是什么导致了这里的减速?
#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
应该是一个参考)。
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;
}
This solution beats append
by approximately 30%.
在这种情况下,通过const
引用和做完美转发似乎没有区别。这是有道理的,因为 std::string +=
不会移动它的参数,即使它们是右值。
如果您无法使用新的花哨的折叠表达式但仍然想要性能,请改用 'dummy array' 技巧(在这种情况下似乎具有完全相同的性能)。
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;
}