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),编译器通常能够识别非优化代码中的模式(例如初始化连续的琐碎元素)并可以优化所有内容,以便您最终使用相同的说明(或多或少)。
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被删除。因此:
CustomAllocatorType::rebind被继承并因此被定义并且是std::allocator。因此,std::allocator_traits<CustomAllocatorType>::rebind_alloc,意味着std::vector最终实际使用std::allocator而不是CustomAllocatorType。如果在构造函数中传入一个CustomAllocatorType实例std::vector,最终会出现拼接。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_constructible和is_move_constructibleor __relocate_a_1,不知道还有多少)。在包含之前定义上述两个函数<vector>可以为您的最小示例带来良好的调试性能。至少它对我本地使用 gcc 11.2 是这样做的。它不适用于快速工作台,因为快速工作台强制benchmark/benchmark.h在任何代码之前包含,并且反过来包含<vector>(也比较接下来的第二个要点)。
这个黑客在多个层面上都很糟糕:
<vector>,否则它们将不会被拾取。原因是对的调用std::__uninitialized_default_n_a()是限定的,即是std::__uninitialized_default_n_a(arguments)而不是,这意味着未找到__uninitialized_default_n_a(arguments)定义后的重载(参见例如这篇文章或这篇文章)。正如上面已经解释的,这就是黑客在快速板凳上失败的原因。另外,如果你在某些地方搞砸了,你可能会违反单一定义规则(这可能会导致更多奇怪的情况)。std::vectorCustomAllocatorType,就像 一样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类。但我还没有检查它的调试性能是否会更好。
| 归档时间: |
|
| 查看次数: |
493 次 |
| 最近记录: |