【问题标题】:Sort objects of dynamic size对动态大小的对象进行排序
【发布时间】:2012-07-18 17:07:20
【问题描述】:

问题

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

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

目前的想法

我已经想到了几种可能的方法来实现这一点,但每种方法似乎都会产生一些相当不幸的后果。您不必阅读所有这些内容。 我尝试用粗体打印每种方法的中心问题。 如果您要建议其中一种方法,那么您的答案应该是回应以及相关的问题。

1。 C 快速排序

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

2。间接使用指向数据的对象

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

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

我写了一个类作为内存范围的迭代器。取消引用这个迭代器不会产生一个引用,而是一个新构造的对象来保存指向数据的指针和在迭代器构造时给出的大小 s。所以可以比较这些对象,我什至有一个std::swap 的实现。不幸的是,std::swap 似乎对std::sort 来说还不够。在过程的某些部分,我的 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);

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

所以我不仅需要我的类作为参考,还需要一个类来保存临时值。由于我的对象的大小是动态的,我必须在堆上分配它,这意味着内存分配在 recursion 树的叶子上。也许另一种选择是具有静态大小的 vaue 类型,该大小应该足够大以容纳我目前打算支持的大小的对象。但这意味着迭代器类的reference_typevalue_type 之间的关系会更加骇人听闻。这意味着我必须将我的应用程序的大小更新为有朝一日支持更大的对象。丑。

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

4。自定义排序

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

5。完全不同的算法

我知道那里有很多种排序算法。其中一些可能更适合我的问题。 基数排序首先出现在我的脑海中,但我还没有真正考虑过这一点。 如果您能提出更适合我的问题的排序算法,请这样做。最好有实施,但即使没有。

问题

所以基本上我的问题是这样的:
“如何有效地对堆内存中动态大小的对象进行排序?”

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

【问题讨论】:

    标签: c++ sorting stl g++ stl-algorithm


    【解决方案1】:

    最实用的解决方案是使用你提到的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 内联比较调用,您可能无法获得最快的排序。如果您的系统所做的只是排序,那么添加代码以使自定义迭代器工作可能是值得的。但是,如果您的系统大部分时间都在做排序以外的事情,那么您获得的额外收益可能只是对整个系统的干扰。

    【讨论】:

    • 使用编译时参数S 意味着我必须为每个可能的大小s 实例化事物。虽然这对于我在可预见的将来能够解决的问题是可能的,但我想在这种情况下我宁愿使用qsort_r 以便我可以将大小作为运行时参数传递。是的,这个应用程序将读取输入,对其进行排序,进行一些重复处理并写入输出,因此排序将构成大部分操作。
    • @MvG:好点。我编辑了我的答案,以便大小是对象的特征,而不是对my_sort 的调用。这允许您在模板中实现一次比较,尽管会创建函数的多个实例。
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

      • 内存分配是否内联?如果没有,我仍然担心函数调用开销。自定义分配机制似乎是个好主意;我什至可以将状态放入迭代器,将机器放入对值转换运算符的引用中,因为我更愿意避免使用静态变量并且排序算法不采用分配器对象。
      • @MvG 我不相信内存分配通常是内联的,但处理器将能够应用间接分支预测,这应该会减少开销。
      【解决方案4】:

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

      正如 cmets 中所讨论的,您可以根据一些固定大小的模板来选择可能的大小。您必须在运行时从这些类型中进行选择(例如,使用 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;
      }
      

      【讨论】:

      • 在您的方法中,对象大小在编译时是固定的。大到足以容纳我指定的数据量,但是当与较小的对象一起使用时,可能会浪费大量内存,因为每个分配的对象实际上只有一小部分会被使用。如果我想在将来支持更大的数据项,我必须重新编译应用程序并更改对象类的大小。总体而言:一种可能但非常静态的方法。
      • 啊,我没有意识到项目大小在运行时可能会有所不同。你有一套可以使用的实用尺寸吗?您可以使用一组定义的 template&lt;int&gt; 类来处理大多数情况...
      • 我有一个无限可能的大小序列,每个都对应于比以前更大的一类问题。理想情况下,我想解决其中任何一个问题,但实际上我将仅限于今天硬件的前几个。但是今天不可行的东西明天很可能会变得实用,我讨厌不得不编辑我的代码以适应这一事实,即使我当然可以。
      • 如果您编辑您对该模板样式的答案,那么我会给您一个赞成票:在任何情况下使用 64 位整数来传输数据似乎都是一个好主意,而且我可以覆盖很大范围一些模板实例化的可能大小。我想这至少可以满足未来几年的需求。
      • 我已经更新了,希望它至少有一点帮助。模板版本需要更多的转换,所以看起来不那么漂亮,但它仍然“有效”。
      【解决方案5】:

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

      使用预编译的头文件,编译时间实际上可能并没有那么糟糕。整个 &lt;algorithm&gt; 标头不会改变,你的包装逻辑也不会改变。您只需要每次重新编译一个谓词。而且由于它是您获得的单一功能,因此链接是微不足道的。

      【讨论】:

      • 嗯,我所知道的几乎所有排序算法都使用递归,并且大多数库都使用函数递归来实现。由于您不能内联递归调用的函数,因此存在限制。除非编译器真的很聪明,或者您自己管理调用堆栈。 动态代码生成是指生成我需要的大小的代码?我有点担心在运行时需要完整的编译器。
      • 澄清一下,我的意思是将谓词内联到排序函数本身。递归调用本身不太可能内联太深。但是,是的,[g++] 标签是建议这样做的原因——你可以分发 GCC。
      【解决方案6】:
      #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);
      

      【讨论】:

      • 所以你会选择我的方法#2,排序指针而不是实际的对象数据块。绝对是最简单的解决方案,所以我将在等待更多答案的同时将其用于原型设计,但我对内存局部性的担忧仍然存在,您还没有解决这些问题。另请注意,OBJECT_SIZE 宏的恒定性质并不能真正反映我的问题的动态方面。
      • @MvG 如果对象数据的大小很小,那么您最好不要使用指针并进行复制,但是您的 's' 是动态的。也许您应该考虑使用 MapReduce 来解决您的问题。
      • 我以前不知道 map-reduce,但现在我看它,似乎我已经想到了这种 kinf od 设置,并将在这里使用这种排序作为一步过程。
      猜你喜欢
      • 2018-01-24
      • 2019-04-27
      • 1970-01-01
      • 1970-01-01
      • 2013-12-05
      • 1970-01-01
      • 2017-02-15
      • 2011-10-04
      • 1970-01-01
      相关资源
      最近更新 更多