c++ - 为什么 pmr::string 在这些基准测试中这么慢?

标签 c++ memory boost c++17

试用 Section 5.9.2 Class monotonic_buffer_resource 中的示例Pablo Halpern 撰写的以下关于多态内存资源的文章:

文件编号:N3816
日期:2013-10-13
作者:巴勃罗·哈尔彭
phalpern@halpernwightsoftware.com
多态内存资源 - r1
(最初是 N3525 – 多态分配器)

文章声称:

The monotonic_buffer_resource class is designed for very fast memory allocations in situations where memory is used to build up a few objects and then is released all at once when those objects go out of scope.

还有那个:

A particularly good use for a monotonic_buffer_resource is to provide memory for a local variable of container or string type. For example, the following code concatenates two strings, looks for the word “hello” in the concatenated string, and then discards the concatenated string after the word is found or not found. The concatenated string is expected to be no more than 80 bytes long, so the code is optimized for these short strings using a small monotonic_buffer_resource [...]

我已经使用 google benchmark library 对示例进行了基准测试和 boost.container 1.69's polymorphic resources ,使用以下代码在 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);

结果如下:
Benchmark Results

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

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

s += s1; 
s += s2

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

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

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

最佳答案

有多种因素组合使得 boost pmr::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 在一个大循环中完成所有操作。显然这是主要成本,也是使 boost 运行速度比 std 慢的原因。

因此,在增加缓冲区并将 boost::container::stringboost::container::pmr::string 进行比较后,pmr 版本略有增加较慢(293 ns 与 276 纳秒)。这是因为 newdelete 实际上对于这样的微基准测试来说是相当快的,并且比 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 表现不佳,并且不能一次又一次地重用同一个 block ,然后 new/delete 会大大减慢执行速度,并且缓存局部性变得相当贫穷的。在这种情况下,pmr+buffer 是值得的。

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

关于c++ - 为什么 pmr::string 在这些基准测试中这么慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55028447/

相关文章:

c++ - `boost::signals2` 中的信号签名是如何实现的?

c++ - 在 FPS 风格游戏中使用鼠标输入时运动意外加速

c++ - 增加进程的内存使用

android - QT Creator,命令行 Android 应用程序

memory - 评估 GNAT 2012 中裸板的内存使用情况

linux - 进程的 PSS(Proportional Set Size)数字是否包括内核代码消耗的内存?

c++ - 'boost::make_shared':对重载函数的模糊调用

iphone - UIViewControllers ...使用 subview 进行内存管理

c++ - Xcode 静态库 - 路径异常

c++ - 循环遍历 mpl::map