为什么std :: u16string比char16_t的数组慢?

时间:2017-07-11 20:14:59

标签: c++ arrays performance c++11 stl

经过一些性能实验后,似乎使用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

我在mac上使用了clang(Apple LLVM版本8.1.0(clang-802.0.42))。随着优化的开启,差距变小但仍然明显:

 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

3 个答案:

答案 0 :(得分:27)

由于libc ++实现小字符串优化的方式,在每个取消引用时,它需要检查字符串内容是存储在字符串对象本身还是存储在堆上。由于索引包含在benchmark::DoNotOptimize中,因此每次访问字符时都需要执行此检查。当通过指针访问字符串数据时,数据总是在外部,因此不需要检查。

答案 1 :(得分:1)

有趣的是,我无法重现您的结果。我几乎察觉不到两者之间的差异。

我使用的(不完整)代码如下所示:

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

答案 2 :(得分:0)

在纯char16_t中,您可以直接访问数组,而在字符串中,您重载了operator []

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; }

现在,我的猜测是:

  1. _M_base()可能不会被内联,因为每次读取都需要额外的操作才能读取功能地址。
    1. 其中一个下标检查发生。