对动态大小的对象进行排序

Sort objects of dynamic size

本文关键字:排序 对象 动态      更新时间:2023-10-16

问题

假设我有一个包含一些数据的大字节数组(最高可达4GB)。这些字节对应于不同的对象,使得每个s字节(想想s多达32个)都将构成一个对象。一个重要的事实是,这个大小s对于所有对象都是相同的,不是存储在对象本身中,并且在编译时是未知的。

目前,这些对象只是逻辑实体,而不是编程语言中的对象。我对这些对象进行了比较,其中包括对大多数对象数据的字典式比较,以及使用剩余数据打破联系的一些不同功能。现在我想高效地对这些对象进行排序(这确实会成为应用程序的瓶颈)。

到目前为止的想法

我想了几种可能的方法来实现这一点,但每种方法似乎都会产生一些相当不幸的后果。你不一定要阅读所有这些我试着把每种方法的中心问题都用粗体打印出来如果你要建议其中一种方法,那么的答案也应该回答相关问题。

1.C快速排序

当然,C快速排序算法在C++应用程序中也可用。它的签名几乎完全符合我的要求。但是,使用该函数将禁止比较函数的内联,这意味着每个比较都会带来函数调用开销。我曾希望有办法避免这种情况任何关于qsort_r在性能方面与STL相比的经验都将非常受欢迎

2.使用指向数据的对象进行定向

编写一组包含指向其各自数据的指针的对象是很容易的。然后可以对它们进行分类。这里有两个方面需要考虑。一方面,只移动指针而不是所有数据将意味着更少的内存操作。另一方面,不移动对象可能会破坏内存局部性,从而影响缓存性能。更深层次的快速排序递归实际上可以从几个缓存页面访问所有数据的机会几乎完全消失。相反,每个缓存的内存页在被替换之前只会产生非常少的可用数据项如果有人能提供一些关于复制和内存局部性之间权衡的经验,我会非常高兴

3.自定义迭代器、引用和值对象

我写了一个类,它在内存范围内充当迭代器。取消引用这个迭代器不会产生引用,而是产生一个新构建的对象,用于保存指向数据的指针和迭代器构造时给出的大小s。所以这些对象可以进行比较,我甚至有一个std::swap的实现。不幸的是,对于std::sort来说,std::swap似乎还不够。在这个过程的某些部分,我的gcc实现使用插入排序(如文件stl_alog.h中的__insertion_sort中所实现的),它将一个值从序列中移出,将一些项目移动一步,然后将第一个值移回适当位置的序列中:

typename iterator_traits<_RandomAccessIterator>::value_type
__val = _GLIBCXX_MOVE(*__i);
_GLIBCXX_MOVE_BACKWARD3(__first, __i, __i + 1);
*__first = _GLIBCXX_MOVE(__val);

您知道不需要值类型但可以单独使用交换操作的标准排序实现吗

所以我不仅需要我的类作为引用,而且还需要一个类来保存临时值。由于我的对象的大小是动态的,我必须在堆上分配,这意味着内存分配在recosrion树的叶子上。也许有一种选择是vaue类型,它的静态大小应该足够大,可以容纳我目前想要支持的大小的对象。但这意味着在迭代器类的reference_typevalue_type之间的关系中会有更多的技巧。这意味着我必须更新我的应用程序的大小,以便有一天能支持更大的对象。丑陋的

如果你能想出一种干净的方法,让上面的代码在不必动态分配内存的情况下操作我的数据,那将是一个很好的解决方案我已经在使用C++11功能,所以使用移动语义或类似的功能不会有问题。

4.自定义排序

我甚至考虑过重新实现所有的快速排序。也许我可以利用这样一个事实,即我的比较主要是字典比较,即我可以按第一个字节对序列进行排序,只有当所有元素的第一个字节相同时,我才能切换到下一个字节。我还没有弄清楚这方面的细节,但如果有人能提出一个引用、一个实现,甚至一个规范名称,作为这种字节字典排序的关键字,我会很高兴我仍然不相信,只要我付出合理的努力,我就能击败STL模板实现的性能。

5.完全不同的算法

我知道有很多类型的排序算法。其中一些可能更适合我的问题<我首先想到的是Radix排序,但我还没有真正想清楚>如果你能提出一个更适合我的问题的排序算法,请这样做。最好有实现,但即使没有

问题

因此,基本上我的问题是:
"如何有效地对堆内存中动态大小的对象进行排序?">

对于这个问题,任何适用于我的情况的答案都是好的,无论它是否与我自己的想法有关。用粗体标记的个别问题的答案,或任何其他可能帮助我在备选方案之间做出决定的见解,也会很有用,尤其是在没有找到单一方法的确切答案的情况下。

最实用的解决方案是使用您提到的C风格的qsort

template <unsigned S>
struct my_obj {
enum { SIZE = S; };
const void *p_;
my_obj (const void *p) : p_(p) {}
//...accessors to get data from pointer
static int c_style_compare (const void *a, const void *b) {
my_obj aa(a);
my_obj bb(b);
return (aa < bb) ? -1 : (bb < aa);
}
};
template <unsigned N, typename OBJ>
void my_sort (const char (&large_array)[N], const OBJ &) {
qsort(large_array, N/OBJ::SIZE, OBJ::SIZE, OBJ::c_style_compare);
}

(或者,如果您愿意,您可以调用qsort_r。)由于STLsort内联比较调用,您可能无法获得尽可能快的排序。如果您的系统所做的只是排序,那么添加代码以使自定义迭代器工作可能是值得的。但是,如果你的系统大部分时间都在做排序之外的事情,那么你获得的额外增益可能只是对整个系统的干扰。

由于只有31种不同的对象变体(1到32个字节),因此可以很容易地为每个对象创建一个对象类型,并根据switch语句选择对std::sort的调用。每个调用都将进行内联和高度优化。

某些对象大小可能需要自定义迭代器,因为编译器会坚持填充本机对象以与地址边界对齐。指针在其他情况下可以用作迭代器,因为指针具有迭代器的所有属性。

我同意std::sort使用自定义迭代器、引用和值类型;最好尽可能使用标准的机器。

您担心内存分配,但现代内存分配器在分配小块内存方面非常有效,尤其是在重复使用时。您还可以考虑使用自己的(有状态)分配器,从一个小池中分配长度s的块。

如果可以将对象覆盖到缓冲区上,那么只要覆盖类型是可复制的,就可以使用std::sort。(在本例中,为4个64位整数)。使用4GB的数据,您将需要大量内存。

正如评论中所讨论的,您可以根据一些固定大小的模板来选择可能的大小。您必须在运行时从这些类型中进行选择(例如,使用switch语句)。下面是一个具有各种大小的模板类型的示例,以及对64位大小进行排序的示例。

这里有一个简单的例子:

#include <vector>
#include <algorithm>
#include <iostream>
#include <ctime>
template <int WIDTH>
struct variable_width
{
unsigned char w_[WIDTH];
};
typedef variable_width<8> vw8;
typedef variable_width<16> vw16;
typedef variable_width<32> vw32;
typedef variable_width<64> vw64;
typedef variable_width<128> vw128;
typedef variable_width<256> vw256;
typedef variable_width<512> vw512;
typedef variable_width<1024> vw1024;
bool operator<(const vw64& l, const vw64& r)
{
const __int64* l64 = reinterpret_cast<const __int64*>(l.w_);
const __int64* r64 = reinterpret_cast<const __int64*>(r.w_);
return *l64 < *r64;
}
std::ostream& operator<<(std::ostream& out, const vw64& w)
{
const __int64* w64 = reinterpret_cast<const __int64*>(w.w_);
std::cout << *w64;
return out;
}
int main()
{
srand(time(NULL));
std::vector<unsigned char> buffer(10 * sizeof(vw64));
vw64* w64_arr = reinterpret_cast<vw64*>(&buffer[0]);
for(int x = 0; x < 10; ++x)
{
(*(__int64*)w64_arr[x].w_) = rand();
}
std::sort(
w64_arr,
w64_arr + 10);
for(int x = 0; x < 10; ++x)
{
std::cout << w64_arr[x] << 'n';
}
std::cout << std::endl;
return 0;
}

考虑到巨大的大小(4GB),我会认真考虑动态代码生成。将自定义排序编译到共享库中,并动态加载它。唯一的非内联调用应该是对库的调用。

对于预编译的头,编译时间实际上可能并没有那么糟糕。整个<algorithm>标头不会更改,包装器逻辑也不会更改。您只需要每次重新编译一个谓词。由于它是一个单独的函数,所以链接是微不足道的。

#define OBJECT_SIZE 32
struct structObject
{
unsigned char* pObject;
bool operator < (const structObject &n) const
{
for(int i=0; i<OBJECT_SIZE; i++)
{
if(*(pObject + i) != *(n.pObject + i))
return (*(pObject + i) < *(n.pObject + i));
}
return false;       
}
};
int _tmain(int argc, _TCHAR* argv[])
{       
std::vector<structObject> vObjects;
unsigned char* pObjects = (unsigned char*)malloc(10 * OBJECT_SIZE); // 10 Objects

for(int i=0; i<10; i++)
{
structObject stObject;
stObject.pObject = pObjects + (i*OBJECT_SIZE);      
*stObject.pObject = 'A' + 9 - i; // Add a value to the start to check the sort
vObjects.push_back(stObject);
}
std::sort(vObjects.begin(), vObjects.end());

free(pObjects);

跳过#define

struct structObject
{
unsigned char* pObject; 
};
struct structObjectComparerAscending 
{
int iSize;
structObjectComparerAscending(int _iSize)
{
iSize = _iSize;
}
bool operator ()(structObject &stLeft, structObject &stRight)
{ 
for(int i=0; i<iSize; i++)
{
if(*(stLeft.pObject + i) != *(stRight.pObject + i))
return (*(stLeft.pObject + i) < *(stRight.pObject + i));
}
return false;       
}
};
int _tmain(int argc, _TCHAR* argv[])
{   
int iObjectSize = 32; // Read it from somewhere
std::vector<structObject> vObjects;
unsigned char* pObjects = (unsigned char*)malloc(10 * iObjectSize);
for(int i=0; i<10; i++)
{
structObject stObject;
stObject.pObject = pObjects + (i*iObjectSize);      
*stObject.pObject = 'A' + 9 - i; // Add a value to the start to work with something...  
vObjects.push_back(stObject);
}
std::sort(vObjects.begin(), vObjects.end(), structObjectComparerAscending(iObjectSize));

free(pObjects);