业绩下降原因
如果分配器是,gcc 的 libstdc++ 使用某些性能改进std::allocator
. Your CustomAllocatorType
是一个不同的类型std::allocator
,意味着优化被禁用。请注意,我是not谈论编译器优化,而不是 gcc 对 C++ 标准库的实现专门针对std::allocator
。
要命名与示例代码相关的示例,std::vector::resize()
内部调用__uninitialized_default_n_a() https://github.com/gcc-mirror/gcc/blob/95874f95095f401405d3386e2e6695351b3f97b5/libstdc%2B%2B-v3/include/bits/vector.tcc#L642其中有一个特殊的过载std::allocator https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/libstdc%2B%2B-v3/include/bits/stl_uninitialized.h#L703。特殊的重载完全绕过分配器。如果你使用CustomAllocatorType
, the 通用版本 https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/libstdc%2B%2B-v3/include/bits/stl_uninitialized.h#L679使用它为每个元素调用分配器。这需要花费很多时间。另一个具有特殊定义且与您的简单代码示例相关的函数是_Destroy() https://github.com/gcc-mirror/gcc/blob/66d1e440e14377a373d0e3d67f478cca4fd14dea/libstdc%2B%2B-v3/include/bits/alloc_traits.h#L847.
换句话说,gcc 对 C++ 标准库的实现采取了一些措施,以确保在已知安全的情况下生成最佳代码。无论编译器优化如何,这都有效。
如果采用非优化的代码路径并且您启用了编译器优化(例如-O3
),编译器通常能够识别非优化代码中的模式(例如初始化连续的琐碎元素),并且可以优化所有内容,以便最终得到相同的指令(或多或少)。
C++20 与 C++17 以及为什么你的CustomAllocatorType
被打破
正如评论中指出的,使用时性能会下降CustomAllocatorType
仅出现在 C++20 中,而不出现在 C++17 中。
要理解原因,请注意 gcc 的std::vector
实施确实not使用Allocator
从声明中std::vector<T,Allocator>
作为分配器,即在您的情况下CustomAllocatorType
。相反,它使用std::allocator_traits<T>::rebind_alloc<T>
(see here https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/libstdc%2B%2B-v3/include/bits/stl_vector.h#L87 and here https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/libstdc%2B%2B-v3/include/bits/stl_vector.h#L129)。另请参阅例如这篇关于重新绑定的文章 https://stackoverflow.com/a/15225165/3740047了解更多信息。
由于您没有定义专业std::allocator_traits<CustomAllocatorType>
,它使用通用的。标准says https://en.cppreference.com/w/cpp/memory/allocator_traits:
重新绑定_分配:Alloc::rebind<T>::other
如果存在,否则Alloc<T, Args>
如果这个 Alloc 是Alloc<U, Args>
IE。如果可能的话,通用分配器会尝试委托给您的分配器。现在,你的分配器CustomAllocatorType
继承自std::allocator
。 C++17 和 C++20 之间的重要区别如下:std::allocator::rebind
was removed https://en.cppreference.com/w/cpp/memory/allocator在 C++20 中。因此:
- C++17:
CustomAllocatorType::rebind
是继承的并因此定义的并且是std::allocator
。所以,std::allocator_traits<CustomAllocatorType>::rebind_alloc
, 意思是std::vector
最终实际使用std::allocator
代替CustomAllocatorType
。如果你传入一个CustomAllocatorType
实例中的std::vector
构造函数,你最终会得到拼接。
- C++20:
CustomAllocatorType::rebind
is not定义的。因此,std::allocator_traits<CustomAllocatorType>::rebind_alloc
is CustomAllocatorType
and std::vector
最终使用CustomAllocatorType
.
所以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
(see here https://stackoverflow.com/a/54092439/3740047 and here https://stackoverflow.com/a/71875925/3740047),像这样:
template<typename T>
class CustomAllocatorType: public std::allocator<T>
{
template< class U > struct rebind {
typedef CustomAllocatorType<U> other;
};
};
当然,这样做意味着C++17版本在调试时会很慢,但实际上可以正常工作。
我认为这也再次表明了一般规则:从 C++ 标准库类型继承通常是一个坏主意。如果CustomAllocatorType
没有继承自std::allocator
,问题一开始就不会出现(而且,因为您需要考虑如何正确设置元素)。
提高绩效
假设分配器已针对 C++17 进行修复,或者您使用 C++20,则调试时的性能会很差,因为库实现使用上述函数的通用版本来填充和销毁数据。不幸的是,这一切都是一个实施细节图书馆的,这意味着有no强制生成良好代码的良好标准方法。
黑客解决方案
在您的简单示例中有效(并且可能仅在那里!)的一个技巧是定义相关函数的自定义重载,例如:
#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);
}
}
这些是从 gcc 复制粘贴的std::allocator
重载(here https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/libstdc%2B%2B-v3/include/bits/stl_uninitialized.h#L701 and here https://github.com/gcc-mirror/gcc/blob/66d1e440e14377a373d0e3d67f478cca4fd14dea/libstdc%2B%2B-v3/include/bits/alloc_traits.h#L847),但超载CustomAllocatorType
。实际应用中需要更特殊的重载(例如,is_copy_constructible and is_move_constructible https://github.com/gcc-mirror/gcc/blob/66d1e440e14377a373d0e3d67f478cca4fd14dea/libstdc%2B%2B-v3/include/bits/alloc_traits.h#L776 or __relocate_a_1 https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/libstdc%2B%2B-v3/include/bits/stl_uninitialized.h#L1005,不知道还有多少)。定义上面两个函数before的包括<vector>
为您的最小示例带来良好的调试性能。至少它对我本地使用 gcc 11.2 是这样做的。它不起作用快速板凳 https://quick-bench.com/q/4DQhSeMGIKyGcrMUaKFCUOmCF_8因为快板凳强制包含benchmark/benchmark.h https://github.com/FredTingaud/quick-bench-back-end/blob/7430344cc8457767a6952ed5d89acb4e0c73fd37/src/libquick.js#L111在你的任何代码之前,然后依次包括<vector> https://github.com/google/benchmark/blob/dc901ff9090e2b931433790cc44afc3af3b09ab2/include/benchmark/benchmark.h#L188(还要比较接下来的第二个要点)。
这个黑客在多个层面上都很糟糕:
- 绝对是非标准。它仅适用于 stdlibc++,并且可能会在库版本的任何升级或降级时中断。
- 您还需要确保定义了重载before the
<vector>
包含标头,否则它们将不会被拾取。原因是,打电话给std::__uninitialized_default_n_a() https://github.com/gcc-mirror/gcc/blob/95874f95095f401405d3386e2e6695351b3f97b5/libstdc%2B%2B-v3/include/bits/vector.tcc#L642是合格的,即是std::__uninitialized_default_n_a(arguments)
而不是__uninitialized_default_n_a(arguments)
,意味着在定义之后重载std::vector
未找到(参见例如这个帖子 https://stackoverflow.com/q/18598862/3740047 or this one https://stackoverflow.com/a/32604022/3740047)。正如上面已经解释的,这就是黑客在快速板凳上失败的原因。另外,如果你在某些地方搞砸了,你可能会违反单一定义规则(这可能会导致更多奇怪的情况)。
- 该示例 hack 假设分配和释放内存不需要使用
CustomAllocatorType
, 就像std::allocator
。我非常怀疑这是否适合你的真实情况CustomAllocatorType
执行。但也许你实际上可以实现例如__uninitialized_default_n_a()
正确且更有效地为您CustomAllocatorType
通过在分配器上调用适当的函数。
我不建议这样做。但根据用例,这可能是一个可行的解决方案。
启用-Og
编译时,我确实使用 gcc 获得了明显更好的性能一切 with -Og
。它尝试执行一些优化,但不会过多干扰调试体验。在你的小例子中,性能得到了提高慢 160 倍 https://quick-bench.com/q/ep3uyYNK6rh_6f8AGAP0zIAflAA to 慢 5 倍 https://quick-bench.com/q/6Z2BAjQYWJrCdyNwm5o-W7-WQi8相比于std::allocator
版本。因此,如果您无法更改编译器,我认为这可能是最好的方法。
使用铿锵声
切换到 clang (没有任何优化标志)似乎可以在一定程度上提高性能。对于 libstdc++,自定义分配器版本是“唯一”的慢 90 倍 https://quick-bench.com/q/8mDtjbosVRD_3uhPVdzkOdtjY5E。
令人惊讶的是,使用 libc++快速板凳 https://quick-bench.com/q/Fjvu8ZNlWeDf60YNa3ekj-B39gY报告了大致相同的性能。不幸的是,我无法在本地重现这个:libc++ 也需要很长时间。不知道为什么本地和快速板凳上的结果不同。
但我可以重现 clang 正在优化的情况-Og
比 gcc 好得多,并且与自定义分配器提供大致相同的性能。这同时适用于libstdc++ 库 https://quick-bench.com/q/hE6PkfPKuRMtHC5boBLJvOg9QG8 and libc++ https://quick-bench.com/q/aCJY_NXV_Zw8he-cgVPcQ45XbnI.
所以我的建议是使用 clang,可能与 libc++ 一起使用,并使用-Og
.
另类想法
本地启用优化(#pragma GCC optimize ("-O3")
等)是相当不可靠的。它对我不起作用。最可能的原因是优化标志没有传播到实例化std::vector
因为它的定义完全在其他地方。您可能需要通过优化来编译 C++ 标准库头本身。
另一个想法是使用不同的容器库。例如,boost https://www.boost.org/doc/libs/1_79_0/doc/html/boost_container_header_reference.html#header.boost.container.vector_hpp has a vector
班级。但我还没有检查它的调试性能是否会更好。