为什么在此基准测试中 std::string::append 比 push_back 快得多?
Why is std::string::append so much faster than push_back in this benchmark?
给定以下基准:
const char* payload = "abcdefghijk";
const std::size_t payload_len = 11;
const std::size_t payload_count = 1000;
static void StringAppend(benchmark::State& state) {
for (auto _ : state) {
std::string created_string;
created_string.reserve(payload_len * payload_count + 1);
for(int i = 0 ; i < payload_count; ++i) {
created_string.append(payload, payload_len);
}
benchmark::DoNotOptimize(created_string);
}
}
BENCHMARK(StringAppend);
static void StringBackInsert(benchmark::State& state) {
for (auto _ : state) {
std::string created_string;
created_string.reserve(payload_len * payload_count + 1);
auto inserter = std::back_inserter(created_string);
for(int i = 0 ; i < payload_count; ++i) {
for(std::size_t i = 0; i < payload_len; ++i) {
*inserter = payload[i];
++inserter;
}
}
benchmark::DoNotOptimize(created_string);
}
}
BENCHMARK(StringBackInsert);
static void StringPushBack(benchmark::State& state) {
for (auto _ : state) {
std::string created_string;
created_string.reserve(payload_len * payload_count + 1);
for(int i = 0 ; i < payload_count; ++i) {
for(std::size_t i = 0; i < payload_len; ++i) {
created_string.push_back(payload[i]);
}
}
benchmark::DoNotOptimize(created_string);
}
}
BENCHMARK(StringPushBack);
我在 quickbench 上得到以下结果,它们显示出非常显着的差异:
考虑到所有需要的内存都是提前分配的,我很难接受这样的想法,即仅仅进行大小与容量检查就代表了这里的所有成本,除非可能有大量的涉及的加载命中存储或分支预测错误的数量。
http://quick-bench.com/XQ9kepYFE1_dZD8vVaQwOUSSVoE
我想了解的是:
- 编译器使用的此设置是否有某些特定的东西不一定适用于现实世界的场景?
- 如果是这样,有没有办法重新安排这个基准以使其更具代表性?
编译器并不总能成功地将一次一个字节的循环优化成不可怕的东西。您正在将已知长度 append
与调用 push_back
.
的整个内部循环进行比较
push_back
包括大小检查,因此以这种方式使用它会检查并在任何字节被压入后有条件地重新分配+复制,这可能会破坏编译器优化它的能力。
但是 append
只需要在整个块之间进行检查,我假设 clang 可以内联 11 字节的 memcpy 以使用一对加载/存储而不是 11 字节加载/字节存储。
给定以下基准:
const char* payload = "abcdefghijk";
const std::size_t payload_len = 11;
const std::size_t payload_count = 1000;
static void StringAppend(benchmark::State& state) {
for (auto _ : state) {
std::string created_string;
created_string.reserve(payload_len * payload_count + 1);
for(int i = 0 ; i < payload_count; ++i) {
created_string.append(payload, payload_len);
}
benchmark::DoNotOptimize(created_string);
}
}
BENCHMARK(StringAppend);
static void StringBackInsert(benchmark::State& state) {
for (auto _ : state) {
std::string created_string;
created_string.reserve(payload_len * payload_count + 1);
auto inserter = std::back_inserter(created_string);
for(int i = 0 ; i < payload_count; ++i) {
for(std::size_t i = 0; i < payload_len; ++i) {
*inserter = payload[i];
++inserter;
}
}
benchmark::DoNotOptimize(created_string);
}
}
BENCHMARK(StringBackInsert);
static void StringPushBack(benchmark::State& state) {
for (auto _ : state) {
std::string created_string;
created_string.reserve(payload_len * payload_count + 1);
for(int i = 0 ; i < payload_count; ++i) {
for(std::size_t i = 0; i < payload_len; ++i) {
created_string.push_back(payload[i]);
}
}
benchmark::DoNotOptimize(created_string);
}
}
BENCHMARK(StringPushBack);
我在 quickbench 上得到以下结果,它们显示出非常显着的差异:
考虑到所有需要的内存都是提前分配的,我很难接受这样的想法,即仅仅进行大小与容量检查就代表了这里的所有成本,除非可能有大量的涉及的加载命中存储或分支预测错误的数量。
http://quick-bench.com/XQ9kepYFE1_dZD8vVaQwOUSSVoE
我想了解的是:
- 编译器使用的此设置是否有某些特定的东西不一定适用于现实世界的场景?
- 如果是这样,有没有办法重新安排这个基准以使其更具代表性?
编译器并不总能成功地将一次一个字节的循环优化成不可怕的东西。您正在将已知长度 append
与调用 push_back
.
push_back
包括大小检查,因此以这种方式使用它会检查并在任何字节被压入后有条件地重新分配+复制,这可能会破坏编译器优化它的能力。
但是 append
只需要在整个块之间进行检查,我假设 clang 可以内联 11 字节的 memcpy 以使用一对加载/存储而不是 11 字节加载/字节存储。