C99 VLA的C ++替代(目标:保持性能)

我将一些C99代码移植到C ++中,这些代码大量使用可变长度数组(VLA)。

我用一个在堆上分配内存的数组类来replaceVLA(堆栈分配)。 性能下降幅度很大,下降了3.2倍(见下面的基准)。 我可以在C ++中使用什么样的VLA快速replace? 我的目标是在重写C ++代码时将性能降到最低。

我build议的一个想法是编写一个在类中包含一个固定大小的存储(即可以堆栈分配)的数组类,并将其用于小型数组,并自动切换到大型数组的堆分配。 我的这个实现是在post的最后。 它工作得很好,但我仍然无法达到原始C99代码的性能。 要接近它,我必须增加这个固定大小的存储( MSL以下)到我不舒服的大小。 我不想在堆栈上分配太大的数组, 即使是那些不需要它的小数组,因为我担心它会触发堆栈溢出。 C99 VLA实际上不太容易这样做,因为它永远不会使用比需要更多的存储空间。

我来到std::dynarray ,但我的理解是,它没有被接受到标准(还?)。

我知道clang和gcc支持C ++中的VLA,但我也需要它与MSVC一起工作。 实际上,更好的可移植性是C ++重写的主要目标之一(另一个目标是将程序本来就是一个命令行工具)编译成可重用的库。


基准

MSLMSL组大小,高于此大小我切换到堆分配。 一维和二维数组使用不同的值。

原始C99代码:115秒。
MSL = 0(即堆分配):367秒(3.2x)。
1D-MSL = 50,2D-MSL = 1000:187秒(1.63x)。
1D-MSL = 200,2D-MSL = 4000:143秒(1.24x)。
1D-MSL = 1000,2D-MSL = 20000:131(1.14x)。

增加MSL进一步提高了性能,但最终程序将开始返回错误的结果(我认为是由于堆栈溢出)。

这些基准与OS X上的clang 3.7一样,但是gcc 5显示了非常相似的结果。


这是我现在使用的“smallvector”实现。 我需要一维和二维vector。 我切换到大小MSL以上的堆分配。

 template<typename T, size_t MSL=50> class lad_vector { const size_t len; T sdata[MSL]; T *data; public: explicit lad_vector(size_t len_) : len(len_) { if (len <= MSL) data = &sdata[0]; else data = new T[len]; } ~lad_vector() { if (len > MSL) delete [] data; } const T &operator [] (size_t i) const { return data[i]; } T &operator [] (size_t i) { return data[i]; } operator T * () { return data; } }; template<typename T, size_t MSL=1000> class lad_matrix { const size_t rows, cols; T sdata[MSL]; T *data; public: explicit lad_matrix(size_t rows_, size_t cols_) : rows(rows_), cols(cols_) { if (rows*cols <= MSL) data = &sdata[0]; else data = new T[rows*cols]; } ~lad_matrix() { if (rows*cols > MSL) delete [] data; } T const * operator[] (size_t i) const { return &data[cols*i]; } T * operator[] (size_t i) { return &data[cols*i]; } }; 

在线程本地存储中创build一个大缓冲区(MB +)。 (堆上的实际内存,TLS中的pipe理)。

允许客户端以FILO方式请求内存(类似堆栈)。 (这模仿了它在C的VLA中是如何工作的;它是有效的,因为每个请求/返回只是一个整数加/减)。

从你的VLA存储。

把它包起来很漂亮,所以你可以说stack_array<T> x(1024); (注意->~T() stack_array ->~T() ,其中Tint是一个合法的noop,而构造可以类似地是一个noop),或者使stack_array<T>包装一个std::vector<T, TLS_stack_allocator>

数据将不会像C VLA数据那样本地化,因为它将在单独的堆栈上有效。 你可以使用SBO(小caching优化),这是当地真正重要的。

一个SBO stack_array<T>可以用一个分配器和一个与std数组结合的std向量来实现,或者使用一个唯一的ptr和自定义驱逐者或者其他无数种方法来实现。 你也许可以改装你的解决scheme,用你的新的/ malloc / free / delete来replace上面的TLS存储的调用。

我说TLS去与TLS,因为它消除了同步开销的需要,同时允许multithreading的使用,并反映了堆栈本身隐式TLS的事实。

基于堆栈缓冲区的STL分配器? 是一个SO问答,至less有两个“堆栈”分配器的答案。 他们将需要一些适应,从TLS自动获得他们的缓冲区。

请注意,作为一个大缓冲区的TLS在某种意义上是一个实现细节。 你可以做大量的分配,当空间不足的时候再做一个大的分配。 你只需要跟踪每个“堆栈页”的当前容量和一个堆栈页的列表,所以当你清空一个,你可以移动到一个较早的。 这可以让你在TLS初始分配时保持一点保守,而不用担心运行OOM; 重要的部分是你是FILO和分配很less,而不是整个FILO缓冲区是一个连续的。

我想你已经在你的问题和评论中列举了大部分的选项。

  • 使用std::vector 。 这是最明显,最无忧无虑但也许是最慢的解决scheme。
  • 在提供它们的平台上使用平台特定的扩展。 例如,GCC支持C ++中的可变长度数组作为扩展。 POSIX指定了在堆栈上分配内存时广泛支持的alloca 。 即使微软Windows提供_malloca ,作为一个快速的networkingsearch告诉我。

    为了避免维护的噩梦,你真的想把这些平台依赖关系封装到一个抽象的接口中,自动地,透明地select当前平台的适当机制。 在所有平台上实现这个function都是有点用处的,但是如果这个function在报告中占了3倍的速度差异,这可能是值得的。 作为未知平台的退步,我会保留std::vector作为最后的手段。 运行缓慢但是正确运行比运行不稳定或不运行更好。

  • 构build你自己的可变大小的数组types,在你的问题中显示,实现一个“小arrays”优化embedded对象本身作为一个缓冲区。 我只会注意到,我宁愿尝试使用std::arraystd::vectorunion ,而不是滚动我自己的容器。

    一旦你有了一个自定义的types,你可以做一些有趣的分析,比如维护一个全局散列表(包含源代码位置),并在程序的压力testing中logging每个分配的大小。 然后,您可以在程序退出时转储散列表,并为各个arrays绘制分配大小的分布。 这可以帮助您微调堆栈上单独存储每个arrays的存储量。

  • 使用自定义分配器的std::vector 。 在程序启动时,分配几兆字节的内存给它一个简单的堆栈分配器。 对于一个堆栈分配器,分配只是比较和添加两个整数,并且释放就是一个简单的减法。 我怀疑编译器生成的堆栈分配可以更快。 然后你的“数组堆栈”将脉冲相关到你的“程序堆栈”。 这种devise还有一个好处,即意外的缓冲区溢出 – 同时仍然调用未定义的行为,垃圾随机数据和所有不好的东西 – 不会轻易破坏程序堆栈(返回地址),就像使用本地的VLA一样。

    C ++中的自定义分配器是一个有点肮脏的业务,但有些人确实报告他们正在成功使用它们。 (我自己没有太多的使用经验。)你可能想开始看cppreference 。 Alisdair Meredith是那些推广使用定制分配器的人之一,在CppCon'14上发表了题为“Making Allocators Work”( 第1 部分 , 第2部分 )的双重会话,您可能会感兴趣。 如果std::allocator接口对你来说太麻烦了,用你自己的分配器实现你自己的variables (而不是dynamic的 )大小的数组类也应该是可行的。

关于支持MSVC:

MSVC有_alloca分配堆栈空间。 如果有足够的可用堆栈空间,它也有_malloca分配堆栈空间,否则回落到dynamic分配。

你不能利用VLAtypes的系统,所以你将不得不改变你的代码工作在一个指向这样一个数组的第一个元素的指针。

您可能最终需要根据平台使用具有不同定义的macros。 例如,在MSVC和g ++或其他编译器上调用_alloca_alloca ,要么调用alloca (如果它们支持的话),要么创build一个VLA和一个指针。


考虑调查重写代码的方法,而不需要分配未知数量的堆栈。 一种select是分配一个固定大小的缓冲区,这是你所需要的最大值。 (如果这会导致堆栈溢出,这意味着你的代码无论如何都被窃听)。