Почему std:: u16string медленнее, чем массив char16_t?

После некоторых экспериментов с производительностью казалось, что использование массивов char16_t может повысить производительность иногда до 40-50%, но кажется, что использование std:: u16string без каких-либо копий и распределений должно быть таким же быстрым, как массивы C. Тем не менее, контрольные показатели показывают обратное.

Вот код, который я написал для теста (он использует Google Benchmark lib):

#include "benchmark/benchmark.h"
#include <string>

static std::u16string str;
static char16_t *str2;

static void BM_Strings(benchmark::State &state) {
    while (state.KeepRunning()) {
        for (size_t i = 0; i < str.size(); i++){
            benchmark::DoNotOptimize(str[i]);
        }
    }
}

static void BM_CharArray(benchmark::State &state) {
    while (state.KeepRunning()) {
        for (size_t  i = 0; i < str.size(); i++){
            benchmark::DoNotOptimize(str2[i]);
        }
    }
}

BENCHMARK(BM_Strings);
BENCHMARK(BM_CharArray);

static void init(){
    str = u"Various applications of randomness have led to the development of several different methods ";
    str2 = (char16_t *) str.c_str();
}

int main(int argc, char** argv) {
    init();
    ::benchmark::Initialize(&argc, argv);
    ::benchmark::RunSpecifiedBenchmarks();
}

Он показывает следующий результат:

Run on (8 X 2200 MHz CPU s)
2017-07-11 23:05:57
Benchmark             Time           CPU Iterations
---------------------------------------------------
BM_Strings         1832 ns       1830 ns     365938
BM_CharArray        928 ns        926 ns     712577

Я использую clang (Apple LLVM version 8.1.0 (clang-802.0.42)) на mac. При включенной оптимизации зазор меньше, но все же заметен:

 Benchmark             Time           CPU Iterations
---------------------------------------------------
BM_Strings          242 ns        241 ns    2906615
BM_CharArray        161 ns        161 ns    4552165

Может кто-нибудь объяснить, что происходит здесь и почему существует разница?

Обновлено (смешение заказа и добавление нескольких шагов прогрева):

Benchmark             Time           CPU Iterations
---------------------------------------------------
BM_CharArray        670 ns        665 ns     903168
BM_Strings          856 ns        854 ns     817776
BM_CharArray        166 ns        166 ns    4369997
BM_Strings          225 ns        225 ns    3149521

Кроме того, я использую следующие флаги компиляции:

/usr/bin/clang++ -I{some includes here} -O3 -std=c++14 -stdlib=libc++ -Wall -Wextra -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk -O3 -fsanitize=address -Werror -o CMakeFiles/BenchmarkString.dir/BenchmarkString.cpp.o -c test/benchmarks/BenchmarkString.cpp

Ответ 1

Из-за того, как libС++ реализует небольшую оптимизацию строк, при каждом разыменовании необходимо проверить, хранится ли содержимое строки в самом объекте строки или в куче. Поскольку индексирование завернуто в benchmark::DoNotOptimize, он должен выполнять эту проверку каждый раз, когда к персонажу обращаются. При доступе к строковым данным через указатель данные всегда являются внешними и поэтому не требуют проверки.

Ответ 2

В чистом char16_t вы напрямую обращаетесь к массиву, а в строке вы перегружаете оператор []

reference
operator[](size_type __pos)
{
    #ifdef _GLIBCXX_DEBUG_PEDANTIC
    __glibcxx_check_subscript(__pos);
#else
    // as an extension v3 allows s[s.size()] when s is non-const.
    _GLIBCXX_DEBUG_VERIFY(__pos <= this->size(),
        _M_message(__gnu_debug::__msg_subscript_oob)
        ._M_sequence(*this, "this")
        ._M_integer(__pos, "__pos")
        ._M_integer(this->size(), "size"));
#endif
    return _M_base()[__pos];
}

и _M_base():

_Base& _M_base() { return *this; }

Теперь мои догадки заключаются в следующем:

  • _M_base() может не получиться вложенным, и чем вы получите удар производительности, потому что каждое чтение требует дополнительной операции для чтения адреса функции.

или

  1. Выполняется одна из этих проверок индексов.

Ответ 3

Интересно, что я не могу воспроизвести ваши результаты. Я едва могу обнаружить разницу между ними.

Используемый здесь код (неполный) показан ниже:

hol::StdTimer timer;

using index_type = std::size_t;

index_type const N = 100'000'000;
index_type const SIZE = 1024;

static std::u16string s16;
static char16_t const* p16;

int main(int, char** argv)
{
    std::generate_n(std::back_inserter(s16), SIZE,
        []{ return (char)hol::random_number((int)'A', (int)'Z'); });

    p16 = s16.c_str();
    unsigned sum;

    {
        sum = 0;

        timer.start();
        for(index_type n = 0; n < N; ++n)
            for(index_type i = 0; i < SIZE; ++i)
                sum += s16[i];
        timer.stop();

        RESULT("string", sum, timer);
    }

    {
        sum = 0;

        timer.start();
        for(std::size_t n = 0; n < N; ++n)
            for(std::size_t i = 0; i < SIZE; ++i)
                sum += p16[i];
        timer.stop();

        RESULT("array ", sum, timer);
    }
}

Вывод:

string: (670240768) 17.575232 secs
array : (670240768) 17.546145 secs

Компилятор:

GCC 7.1 
g++ -std=c++14 -march=native -O3 -D NDEBUG