假设我有一个包含一些数据的大量字节(最多4GB)。这些字节对应于不同的对象,每个 s 字节(想想 s 最多32个)将构成一个对象。一个重要的事实是,这个大小 s 对于所有对象都是相同的,不存储在对象本身中,并且在编译时不知道。
目前,这些对象只是逻辑实体,而不是编程语言中的对象。我对这些对象进行了比较,其中包括对大多数对象数据的字典对比,以及使用剩余数据打破关系的一些不同功能。现在我想有效地对这些对象进行排序(这实际上是应用程序的瓶颈)。
我已经想到了实现这一目标的几种可能方法,但每种方法似乎都有一些相当不幸的后果。你不必阅读所有这些内容。 我尝试以粗体显示每种方法的核心问题。 如果您将建议采用其中一种方法,那么您的答案应该回应还有相关的问题。
当然,C快速排序算法也可用于C ++应用程序。它的标志几乎完美符合我的要求。但是使用该函数将禁止内联比较函数的事实将意味着每个比较都带有函数调用开销。我曾希望有办法避免这种情况。 非常欢迎任何关于C qsort_r
在性能方面与STL相比的经验。
编写一堆包含各自数据指针的对象会很容易。那么人们就可以对它们进这里有两个方面需要考虑。一方面,仅仅移动指针而不是所有数据意味着更少的内存操作。另一方面,不移动对象可能会破坏内存局部性,从而破坏缓存性能。有可能实际上从一些缓存页面访问所有数据的更快的快速排序递归水平几乎完全消失。相反,每个缓存的内存页面在被替换之前只会产生非常少的可用数据项。 如果有人能提供一些关于复制和记忆位置之间权衡的经验,我会非常高兴。
我写了一个类作为内存范围的迭代器。取消引用此迭代器不会产生引用,而是一个新构造的对象来保存指向数据的指针和大小 s ,它是在构造迭代器时给出的。所以可以比较这些对象,我甚至可以为这些对象实现std::swap
。不幸的是,似乎std::swap
对std::sort
来说还不够。在该过程的某些部分,我的gcc实现使用插入排序(在文件__insertion_sort
中的stl_alog.h
中实现),它将值移出序列,将数字项移动一步,然后移动第一个值返回到适当位置的序列:
typename iterator_traits<_RandomAccessIterator>::value_type
__val = _GLIBCXX_MOVE(*__i);
_GLIBCXX_MOVE_BACKWARD3(__first, __i, __i + 1);
*__first = _GLIBCXX_MOVE(__val);
您是否知道标准排序实施并不需要值类型但可以单独使用交换?
所以我不仅需要我的课程作为参考,但我还需要一个课程来保存临时值。由于我的对象的大小是动态的,我必须在堆上分配它,这意味着在recusrion树的叶子上分配内存。也许一种替代方案是具有静态大小的vaue类型,该大小应足够大以容纳我当前打算支持的大小的对象。但这意味着迭代器类的reference_type
和value_type
之间的关系会更加骇人。这意味着我必须为我的应用程序更新该大小,以便有一天支持更大的对象。难看。
如果你能想出一个干净的方法来让上面的代码操作我的数据而不必动态分配内存,那将是一个很好的解决方案。我使用的是C ++已有11个功能,因此使用移动语义或类似功能不会成为问题。
我甚至考虑重新实现所有的快速排序。也许我可以利用这样一个事实,即我的比较主要是词典比较,即我可以按第一个字节对序列进行排序,只有当所有元素的firt字节相同时才切换到下一个字节。我还没有弄清楚这方面的细节,但如果有人可以建议一个引用,一个实现甚至一个规范的名称作为这种字节顺序词典排序的关键词,我&#39我非常高兴。我仍然不相信,通过我的合理努力,我可以击败STL模板实施的表现。
我知道有许多种类的排序算法。其中一些可能更适合我的问题。我首先想到了 Radix sort ,但我还没有想到这一点。 如果您可以建议更适合我的问题的排序算法,请执行此操作。最好是实施,但即使没有。
所以基本上我的问题是:
“您如何在堆内存中有效地对动态大小的对象进行排序?”
适用于我的情况的这个问题的任何答案都是好的,无论它是否与我自己的想法有关。以粗体标记的个别问题的答案,或任何其他可能帮助我在我的选择之间做出决定的见解,也是有用的,特别是如果没有对单一方法的明确答案出现。
答案 0 :(得分:2)
最实用的解决方案是使用您提到的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
。)由于STL sort
内联比较调用,您可能无法获得最快的排序。如果您的所有系统都进行了排序,那么添加代码以使自定义迭代器工作可能是值得的。但是,如果大多数时候你的系统正在做除分类以外的其他事情,你获得的额外收益可能只是整个系统的噪音。
答案 1 :(得分:1)
如果您可以将对象叠加到缓冲区上,则可以使用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;
}
答案 2 :(得分:1)
我同意std::sort
使用自定义迭代器,引用和值类型;最好尽可能使用标准机械。
您担心内存分配,但现代内存分配器在分发小块内存时非常有效,特别是在重复使用时。您还可以考虑使用自己的(有状态的)分配器,从一个小池中分发长度 s 块。
答案 3 :(得分:1)
考虑到巨大的尺寸(4GB),我会认真考虑动态代码生成。将自定义排序编译到共享库中,并动态加载它。唯一的非内联调用应该是调用库。
使用预编译头文件,编译时间实际上可能并不那么糟糕。整个<algorithm>
标题不会改变,包装逻辑也不会改变。您只需要每次重新编译一个谓词。而且由于它是一个单一的功能,链接是微不足道的。
答案 4 :(得分:1)
由于只有31种不同的对象变体(1到32个字节),因此您可以轻松地为每个变体创建一个对象类型,并根据switch语句选择对std::sort
的调用。每个电话都会内联并高度优化。
某些对象大小可能需要自定义迭代器,因为编译器将坚持填充本机对象以对齐地址边界。由于指针具有迭代器的所有属性,因此指针可用作其他情况下的迭代器。
答案 5 :(得分:0)
#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);