首页 > 解决方案 > 为什么 pmr::string 在这些基准测试中这么慢?

问题描述

尝试以下 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);

以下是结果:
基准测试结果

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

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

s += s1; 
s += s2

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

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

标签: c++memoryboostc++17

解决方案


有多种因素会使 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有一个次优的实现。这是速度差的真正代价。在 GCCstd::basic_string::find用于__builtin_memchr查找可能匹配的第一个字符中,boost 是在一个大循环中完成的。显然这是主要成本,也是让 boost 运行速度比 std 慢的原因。

因此,在增加缓冲区之后,与 相比boost::container::stringboost::container::pmr::stringpmr 版本会稍微慢一些(293 ns 对 276 ns)。这是因为new这样delete的微基准测试实际上非常快,并且比 pmr 的复杂机械更快(构建只需 17 ns)。事实上,默认的 Linux/gcc new/delete 会一次又一次地重用同一个指针。这种优化有一个非常简单和快速的实现,它也适用于 CPU 缓存。

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

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

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

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

结论:boost pmr字符串的实现比gcc的字符串慢。pmr 机制比 new/delete 的默认和简单方案的成本略高。


推荐阅读