调试时 GCC 中的自定义 C++ 分配器太慢。有解决办法吗?

jes*_*dis 12 c++ gcc allocator c++20

我正在努力解决自定义分配器的性能问题。我的问题是关于调试版本。

通常情况下,如果只有一点点下降我并不介意。但目前我正在以 4fps 播放某些内容,而如果没有自定义分配器,则播放速度为 60fps(并且可能会更快)。这使得软件开发变得更加困难。

我一直把它确定下来......基本上继承了标准分配器

请参阅“quick-bench.com”的以下结果 https://quick-bench.com/q/ep3uyYNK6rh_6f8AGAP0zIAflAA

这是一张图片: 在此输入图像描述

蓝色条很简单:

int main() {
    std::vector<uint8_t, std::vector<uint8_t>::allocator_type> buffer;
    buffer.reserve(numBytes);
    buffer.resize(numBytes);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

黄色条:

template<typename T>
class CustomAllocatorType : public std::vector<uint8_t>::allocator_type {};

int main() {
    std::vector<uint8_t, CustomAllocatorType<uint8_t>> buffer;
    buffer.reserve(numBytes);
    buffer.resize(numBytes);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

用以下内容封装自定义分配器:

#pragma GCC push_options
#pragma GCC optimize ("-O3")
// ....
#pragma GCC pop_options
Run Code Online (Sandbox Code Playgroud)

没有任何效果。我想我需要对向量实例本身执行此操作,但我不想走那么远......

有谁知道这个问题的解决方案?

Gug*_*ugi 17

业绩下降原因

如果分配器是 .gcc 的 libstdc++,则 gcc 的 libstdc++ 会使用某些性能改进std::allocator。您的CustomAllocatorType类型与 不同std::allocator,这意味着优化已禁用。请注意,我不是在谈论编译器优化,而是 GCC 对 C++ 标准库的实现专门针对std::allocator. 要命名与示例代码相关的示例,std::vector::resize() 请在内部调用__uninitialized_default_n_a()具有特殊重载的std::allocator. 特殊的重载完全绕过分配器。如果您使用CustomAllocatorType,则使用通用版本,它为每个元素调用分配器。这需要花费很多时间。另一个具有特殊定义且与您的简单代码示例相关的函数是_Destroy().

换句话说,gcc 对 C++ 标准库的实现实施了一些措施,以确保在已知安全的情况下生成最佳代码。无论编译器优化如何,这都有效。如果采用非优化的代码路径并且启用编译器优化(例如-O3),编译器通常能够识别非优化代码中的模式(例如初始化连续的琐碎元素)并可以优化所有内容,以便您最终使用相同的说明(或多或少)。

C++20 与 C++17 以及为什么你的方法CustomAllocatorType被破坏了

正如评论中所指出的,使用时的性能下降CustomAllocatorType仅发生在 C++20 中,而不是 C++17 中。要理解原因,请注意 gcc 的std::vector实现不使用fromAllocator声明std::vector<T,Allocator>作为分配器,即在您的情况下CustomAllocatorType。相反,它使用std::allocator_traits<T>::rebind_alloc<T>(参见此处此处)。另请参阅有关重新绑定的这篇文章以获取更多信息。

由于您没有定义专门化std::allocator_traits<CustomAllocatorType>,因此它使用通用的。标准

rebind_alloc<T>:Alloc::rebind<T>::other如果存在,否则Alloc<T, Args>如果此 Alloc 是Alloc<U, Args>

即,如果可能的话,通用分配器会尝试委托给您的分配器。现在,您的分配器CustomAllocatorType继承自std::allocator. 这是 C++17 和 C++20 之间的重要区别:在 C++20 中std::allocator::rebind删除。因此:

  • C++17:CustomAllocatorType::rebind被继承并因此被定义并且是std::allocator。因此,std::allocator_traits<CustomAllocatorType>::rebind_alloc,意味着std::vector最终实际使用std::allocator而不是CustomAllocatorType。如果在构造函数中传入一个CustomAllocatorType实例std::vector,最终会出现拼接。
  • C ++20:CustomAllocatorType::rebind未定义。因此,是并且最终使用.std::allocator_traits<CustomAllocatorType>::rebind_allocCustomAllocatorTypestd::vectorCustomAllocatorType

因此,C++17 版本使用std::allocator并因此享受上述基于库的优化,而 C++20 版本则不然。

您的代码根本不正确,或者至少是 C++17 版本不正确。std::vector在 C++17 中根本不使用你的分配器。buffer.get_allocator()您还可以注意到,如果您尝试在示例中调用,该示例将无法在 C++17 中编译,因为它将尝试将std::allocator(如内部使用的)转换为CustomAllocatorType.

我认为解决问题的正确方法是定义CustomAllocatorType::rebind而不是专门化std::allocator_traits(请参阅此处此处),如下所示:

template<typename T>
class CustomAllocatorType: public std::allocator<T> 
{
  template< class U > struct rebind {
    typedef CustomAllocatorType<U> other;
  };
};
Run Code Online (Sandbox Code Playgroud)

当然,这样做意味着C++17版本在调试时会很慢,但实际上可以正常工作。

我认为这也再次表明了一般规则:从 C++ 标准库类型继承通常是一个坏主意。如果CustomAllocatorType没有继承自std::allocator,那么问题一开始就不会出现(另外,因为您需要考虑如何正确设置元素)。

提高绩效

假设分配器已针对 C++17 进行修复,或者您使用 C++20,则调试时的性能会很差,因为库实现使用上述函数的通用版本来填充和销毁数据。不幸的是,所有这些都是库的实现细节,这意味着没有好的标准方法来强制生成良好的代码。

黑客解决方案

在您的小示例中有效(并且可能仅在那里!)的一个技巧是定义相关函数的自定义重载,例如:

#include <bits/stl_uninitialized.h>
#include <cstdint>
#include <cstdlib>

// Must be defined BEFORE including <vector>!
namespace std{
  template<typename _ForwardIterator, typename _Size, typename _Tp>
  inline _ForwardIterator
  __uninitialized_default_n_a(_ForwardIterator __first, _Size __n, CustomAllocatorType<_Tp>&)
  { return std::__uninitialized_default_n(__first, __n); }


  template<typename _ForwardIterator, typename _Tp>
  _GLIBCXX20_CONSTEXPR inline void
  _Destroy(_ForwardIterator __first, _ForwardIterator __last, CustomAllocatorType<_Tp>&) {
    _Destroy(__first, __last);
  }
}
Run Code Online (Sandbox Code Playgroud)

std::allocator这里的这些是从 gcc 的重载(这里这里)复制和粘贴的,但是重载了CustomAllocatorType. 实际应用中需要更多特殊的重载(例如 foris_copy_constructibleis_move_constructibleor __relocate_a_1,不知道还有多少)。在包含之前定义上述两个函数<vector>可以为您的最小示例带来良好的调试性能。至少它对我本地使用 gcc 11.2 是这样做的。它不适用于快速工作台,因为快速工作台强制benchmark/benchmark.h在任何代码之前包含,并且反过来包含<vector>(也比较接下来的第二个要点)。

这个黑客在多个层面上都很糟糕:

  • 这绝对是不标准的。它仅适用于 stdlibc++,并且可能会在库版本的任何升级或降级时中断。
  • 您还需要确保在包含标头之前定义重载<vector>,否则它们将不会被拾取。原因是对的调用std::__uninitialized_default_n_a()是限定的,即是std::__uninitialized_default_n_a(arguments)而不是,这意味着未找到__uninitialized_default_n_a(arguments)定义后的重载(参见例如这篇文章这篇文章)。正如上面已经解释的,这就是黑客在快速板凳上失败的原因。另外,如果你在某些地方搞砸了,你可能会违反单一定义规则(这可能会导致更多奇怪的情况)。std::vector
  • 该示例 hack 假设分配和释放内存不需要使用CustomAllocatorType,就像 一样std::allocator。我非常怀疑这是否适用于您的真正CustomAllocatorType实施。但也许您实际上可以通过在分配器上调用适当的函数来__uninitialized_default_n_a()正确且更有效地实现例如。CustomAllocatorType

我不建议这样做。但根据用例,这可能是一个可行的解决方案。

启用-Og

当使用 .gcc 编译所有内容时,我确实获得了明显更好的性能-Og。它尝试执行一些优化,但不会过多干扰调试体验。在您的简单示例中,与版本相比,性能从慢 160 倍提高到慢 5 倍std::allocator。因此,如果您无法更改编译器,我认为这可能是最好的方法。

使用铿锵声

切换到 clang (没有任何优化标志)似乎可以在一定程度上提高性能。使用 libstdc++,自定义分配器版本“仅”慢 90 倍。令人惊讶的是,libc++快速测试报告的性能大致相同。不幸的是,我无法在本地重现这个:libc++ 也需要很长时间。不知道为什么本地和快速板凳上的结果不同。

但我可以重现 clang 的优化效果-Og比 gcc 好得多,并且使用自定义分配器提供了大致相同的性能。这对于libstdc++libc++都适用。

所以我的建议是使用 clang,可能与 libc++ 一起使用,并使用-Og.

另类想法

在本地启用优化(#pragma GCC optimize ("-O3")等)是相当不可靠的。它对我不起作用。最可能的原因是优化标志没有传播到 的实例化,std::vector因为它的定义完全在其他地方。您可能需要通过优化来编译 C++ 标准库头本身。

另一个想法是使用不同的容器库。例如boost有一个vector类。但我还没有检查它的调试性能是否会更好。

  • 你对这个问题的深入研究让我大吃一惊。非常感谢您提供详细信息!我会看看现在是否可以只使用 clang 。对我来说似乎是最好的解决方案 (2认同)