Why is std::string::append so much faster than push_back in this benchmark?

Viewed 335

Given the following 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);

I get the following on quickbench, which show a very dramatic difference: enter image description here

Considering that all the required memory is allocated ahead of time, I'm having a lot of trouble buying into the idea that just doing the size vs capacity check represents essentially all of the cost here, unless maybe there's a massive number of load-hit-store or branch misprediction involved.

http://quick-bench.com/XQ9kepYFE1_dZD8vVaQwOUSSVoE

What I'd like to understand is:

  • Is there something specific to this setup that the compiler is using that will not necessarily apply in a real-world scenario?
  • If so, is there a way to rearrange this benchmark to be more representative?
1 Answers

Compilers don't always succeed at optimizing a byte-at-a-time loop into something non-terrible. You're comparing a known-length append against a whole inner loop that calls push_back.

push_back includes a size check, so using it this way checks and conditionally reallocates+copies after any byte pushed, which probably defeats the ability of compilers to optimize it.

But append only has to check between whole chunks, and I assume clang can inline that 11-byte memcpy to use a couple loads / stores instead of 11 byte loads / byte stores.

Related