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

时间:2019-03-06 16:59:53

标签: c++ memory c++17

尝试以下关于Pablo Halpern的多态内存资源的文章Section 5.9.2 Class 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 [...]

针对这些短字符串进行了优化

我已经使用google benchmark libraryboost.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进行比较,保留内存应该归结为简单的指针算法,因为char缓冲区应该足够,所以不需要“ new”。随后,它将像std :: string那样构造每个字符。

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

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

1 个答案:

答案 0 :(得分:1)

多种因素共同导致助推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 vs. 276 ns)。这是因为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的表现不那么好,并且不能一次又一次地重复使用同一块,然后new / delete大大降低了执行速度,并且缓存位置变得相当较差的。在这种情况下,pmr + buffer是值得的。

结论:boost pmr字符串的实现比gcc的字符串慢。 pmr机制的成本比新的/删除的默认简单方案高。