为什么 pmr::string 在这些基准测试中如此缓慢?

Quo*_*inh 3 c++ memory boost c++17

尝试Pablo Halpern 撰写的以下关于多态内存资源的文章的第 5.9.2 节类 monotonic_buffer_resource中的示例:

文档编号:N3816
日期:2013-10-13
作者:Pablo Halpern
phalpern@halpernwightsoftware.com
多态内存资源 - r1
(原为 N3525 - 多态分配器)

文章声称:

monotonic_buffer_resource 类设计用于在内存用于构建一些对象然后在这些对象超出范围时立即释放的情况下非常快速的内存分配。

然后 :

monotonic_buffer_resource 的一个特别好的用途是为容器或字符串类型的局部变量提供内存。例如,以下代码将两个字符串连接起来,在连接后的字符串中查找单词“hello”,然后在找到或未找到该单词后丢弃连接后的字符串。连接的字符串预计不超过 80 字节长,因此使用小的 monotonic_buffer_resource [...] 为这些短字符串优化了代码。

我已经使用谷歌基准库boost.container 1.69 的多态资源对示例进行了基准测试,编译并链接到在 Ubuntu 18.04 LTS hyper-v 虚拟机上使用 g++-8 发布的二进制文件,代码如下:

// overload using pmr::string
static bool find_hello(const boost::container::pmr::string& s1, const boost::container::pmr::string& s2)
{
    using namespace boost::container;

    char buffer[80];
    pmr::monotonic_buffer_resource m(buffer, 80);
    pmr::string s(&m);
    s.reserve(s1.length() + s2.length());
    s += s1;
    s += s2;
    return s.find("hello") != pmr::string::npos;
}

// overload using std::string
static bool find_hello(const std::string& s1, const std::string& s2)
{
    std::string s{};
    s.reserve(s1.length() + s2.length());
    s += s1;
    s += s2;
    return s.find("hello") != std::string::npos;
}

static void allocator_local_string(::benchmark::State& state)
{
    CLEAR_CACHE(2 << 12);

    using namespace boost::container;
    pmr::string s1(35, 'c'), s2(37, 'd');

    for (auto _ : state)
    {
        ::benchmark::DoNotOptimize(find_hello(s1, s2));
    }
}

// pmr::string with monotonic buffer resource benchmark registration
BENCHMARK(allocator_local_string)->Repetitions(5);

static void allocator_global_string(::benchmark::State& state)
{
    CLEAR_CACHE(2 << 12);

    std::string s1(35, 'c'), s2(37, 'd');

    for (auto _ : state) 
    {
        ::benchmark::DoNotOptimize(find_hello(s1, s2));
    }
}

// std::string using std::allocator and global allocator benchmark registration
BENCHMARK(allocator_global_string)->Repetitions(5);
Run Code Online (Sandbox Code Playgroud)

结果如下:
基准测试结果

与 std::string 相比,pmr::string 基准测试为何如此缓慢?

我假设 std::string 的 std::allocator 应该在保留调用中使用“new”,然后在调用时构造每个字符:

s += s1; 
s += s2
Run Code Online (Sandbox Code Playgroud)

与使用持有 monotonic_buffer_resource 的多态分配器的 pmr::string 相比,保留内存应该归结为简单的指针算术,不需要“new”,因为 char 缓冲区就足够了。随后,它会像 std::string 那样构造每个字符。

因此,考虑到 find_hello 的 pmr::string 版本和 find_hello 的 std::string 版本之间唯一不同的操作是对保留内存的调用,其中 pmr::string 使用堆栈分配,而 std::string 使用堆分配:

  • 我的基准是错误的吗?
  • 我对分配应该如何发生的解释是错误的吗?
  • 为什么 pmr::string 基准测试比 std::string 基准测试慢大约 5 倍?

Mic*_*ler 9

有多种因素会使 boostpmr::basic_string变慢:

  1. 的构建pmr::monotonic_buffer_resource有一些成本(这里是 17 纳秒)。
  2. pmr::basic_string::reserve储备不止一个需要。在这种情况下,它保留了 96 个字节,这比您拥有的 80 个字节还多。
  3. 保留pmr::basic_string不是免费的,即使缓冲区足够大(此处额外 8 纳秒)。
  4. 字符串的连接代价高昂(此处额外 64 ns)。
  5. pmr::basic_string::find有一个次优的实现。这是速度差的真正代价。在GCC的std::basic_string::find用途__builtin_memchr找到的第一个字符可能匹配,这是提振做这一切在一个大循环。显然这是主要成本,也是什么使 boost 运行比 std 慢。

因此,增加了缓冲,并比较后boost::container::stringboost::container::pmr::string,在PMR版本出现稍慢(293纳秒与276纳秒)。这是因为,newdelete实际上是相当快的,例如微基准测试,并且比PMR(仅为17纳秒建设)的复杂的机器快。事实上,默认的 Linux/gcc new/delete 一次又一次地重复使用相同的指针。这种优化有一个非常简单和快速的实现,它也适用于 CPU 缓存。

作为证明,试试这个(没有优化):

for (int i=0 ; i < 10 ; ++i)
{
  char * ptr = new char[96];
  std::cout << (void*) ptr << '\n';
  delete[] ptr;
}
Run Code Online (Sandbox Code Playgroud)

这一次又一次地打印相同的指针。

理论是,在真实的程序中,new/delete 的行为不是那么好,并且不能一次又一次地重用同一个块,然后 new/delete 会减慢执行速度,并且缓存局部性变得很差。在这种情况下,pmr+buffer 是值得的。

结论:boost pmr string的实现比gcc的string要慢。pmr 机制比新建/删除的默认和简单方案的成本略高。