【问题标题】:Sorting by blocks of elements with std::sort()使用 std::sort() 按元素块排序
【发布时间】:2010-12-11 14:16:46
【问题描述】:

我有一个边数组,它被定义为 C 风格的双精度数组,其中每 4 个双精度定义一个边,如下所示:

double *p = ...;
printf("edge1: %lf %lf %lf %lf\n", p[0], p[1], p[2], p[3]);
printf("edge2: %lf %lf %lf %lf\n", p[4], p[5], p[6], p[7]);

所以我想使用std::sort() 按边长对其进行排序。如果是struct Edge { double x1, y1, x2, y2; }; Edge *p;,我会很高兴。

但是在这种情况下,双精度数组的块大小不是由指针类型表示的。 qsort() 允许您显式指定块大小,但 std::sort() 通过指针类型推断块大小

出于性能原因(内存使用和 CPU),假设不希望创建新数组或以某种方式转换数组。再次出于性能原因,假设我们确实想使用std::sort() 而不是qsort()

是否可以调用std::sort() 而不会浪费一个 CPU 周期来转换数据?

可能的方法:

一个明显的方法是尝试强制转换指针:

double *p = ...;
struct Edge { double arr[4]; };
Edge *p2 = reinterpret_cast<Edge*>(p);
std::sort(...);

但是如何确保数据正确对齐?另外,我如何确保它始终在所有平台和架构上正确对齐?

或者我可以使用typedef double[4] Edge;吗?

【问题讨论】:

  • 为什么 std::sort 会更快?
  • @Nick Johnson:据我所知,它使用了更好的算法。
  • 经验上是这样,大概是因为它可以内联比较。但是,值得首先分析您的应用程序以确定排序是一个值得优化的瓶颈 - qsort 运行良好。
  • 拥有 L 全局不会影响线程安全。事实上,如您所见,qsort 接收该参数,但它会将其复制到内部。顺便说一句,std::sort 仅在某些情况下更快,但肯定不是在普通 C 数组上。
  • 添加了关于 qsort 的注释,因为有很多错误答案。

标签: c++ c arrays sorting


【解决方案1】:

有一个重新排序向量怎么样?您用 1..N/L 初始化向量,传递 std::sort 一个比较器,将元素 i1*L..i1*L+L 与 i2*L..i2*L+L 进行比较,并且当您的向量正确排序时,按照新的顺序对C数组重新排序。

回应评论:是的,事情变得复杂了,但它可能只是很好的复杂性!看看here

【讨论】:

  • 如果你不使用额外的数组,事情会变得复杂(我不会)。
【解决方案2】:

您可以为此使用“步幅迭代器”。 “步幅迭代器”包含另一个迭代器和一个整数步长。这是一个简单的草图:

template<typename Iter>
class stride_iterator
{
    ...

    stride_iterator(Iter it, difference_type step = difference_type(1))
    : it_(it), step_(step) {}

    stride_iterator& operator++() {
        std::advance(it_,step_);
        return *this;
    }

    Iter base() const { return it_; }

    difference_type step() const { return step_; }

    ...

private:
    Iter it_;
    difference_type step_;
};

还有,像这样的辅助函数

template<typename Iter>
stride_iterator<Iter> make_stride_iter(
    Iter it,
    typename iterator_traits<Iter>::difference_type step)
{
    return stride_iterator<Iter>(it,step);
}

template<typename Iter>
stride_iterator<Iter> make_stride_iter(
    stride_iterator<Iter> it,
    typename iterator_traits<Iter>::difference_type step)
{
    return stride_iterator<Iter>(it.base(),it.step() * step);
}

应该让跨步迭代器的使用变得相当容易:

int array[N*L];
std::sort( make_stride_iter(array,L),
           make_stride_iter(array,L)+N );

自己(使用所有运算符)实现迭代器适配器可能不是一个好主意。正如 Matthieu 指出的那样,例如,如果您使用 Boost 的 iterator adapter 工具,您可以避免大量打字。

编辑: 我刚刚意识到这并不能满足您的要求,因为 std::sort 只会交换每个块的第一个元素。我不认为有一个简单和便携的解决方案。我看到的问题是,使用 std::sort 时不能(轻松)自定义交换“元素”(您的块)。您可以编写迭代器以返回具有 special 交换函数的 special 引用类型,但我不确定 C++ 标准是否保证 std::sort 将使用通过 ADL 查找的交换函数。您的实现可能会将其限制为 std::swap。

我想最好的答案仍然是:“Just use qsort”。

【讨论】:

  • && 不是 C++0x 的东西吗?似乎是一个很好的解决方案,我会检查一下。
  • “&& 不是 C++0x 的东西吗?” - 对,我的错。我改了。
  • 哎呀,那是代码膨胀。 boost::iterator_adaptor:boost.org/doc/libs/1_40_0/libs/iterator/doc/…
  • 是的,使用一些迭代器适配器库似乎是个好主意。就此而言,我认为跨步迭代器应该是 std:: 或至少 boost:: 的一部分。 ;)
【解决方案3】:

对于新问题,我们需要传入sort() 一种迭代器,它不仅可以让我们比较正确的事物(即确保每次通过double[] 执行4 个步骤而不是1 个)但也要交换正确的东西(即交换 4 个doubles 而不是一个)。

我们可以通过简单地重新解释我们的双精度数组来实现这两者,就好像它是一个由 4 个双精度数组成的数组。这样做:

typedef double Edge[4];

不起作用,因为您无法分配数组,而swap 将需要分配。但是这样做:

typedef std::array<double, 4> Edge;

或者,如果不是 C++11:

struct Edge {
    double vals[4];
};

满足这两个要求。因此:

void sort(double* begin, double* end) {
    typedef std::array<double, 4> Edge;

    Edge* edge_begin = reinterpret_cast<Edge*>(begin);
    Edge* edge_end = reinterpret_cast<Edge*>(end);

    std::sort(edge_begin, edge_end, compare_edges);
}

bool compare_edges(const Edge& lhs, const Edge& rhs) {
    // to be implemented
}

如果您担心对齐,总是可以断言没有额外的填充:

static_assert(sizeof(Edge) == 4 * sizeof(double), "uh oh");

【讨论】:

  • 谢谢,我刚看到答案。我们能确定这样的结构总是正确对齐吗?它是由标准定义的,还是由实施决定的?另外,static_assert 也需要 C++11,不是吗?
  • @sashoalm 我相信是这样,但我不能给你一个报价。 Edge 应该与 double 具有相同的对齐方式 - 所以不应该有任何填充。如果不是,我真的很想知道为什么不。你可以改用BOOST_STATIC_ASSERT()
  • '#pragma pack' 受 VC++ 和 GCC 支持——不过,如果可能的话,我不会使用它
【解决方案4】:

我不记得具体怎么做,但是如果你可以伪造匿名函数,那么你可以创建一个 comp(L) 函数,它返回长度为 L 的数组的 comp 版本......这样 L 就变成了一个参数,不是全局的,你可以使用 qsort.正如其他人所提到的,除了您的数组已经排序或向后排序的情况外,qsort 将与任何其他算法一样快。 (毕竟它被称为快速排序是有原因的......)

【讨论】:

    【解决方案5】:

    它不是任何 ANSI、ISO 或 POSIX 标准的一部分,但某些系统提供 qsort_r() 函数,它允许您将额外的上下文参数传递给比较函数。然后你可以这样做:

    int comp(void *thunk, const void *a, const void *b)
    {
        int L = (int)thunk;
        // compare a and b as you would normally with a qsort comparison function
    }
    
    qsort_r(array, N, sizeof(int) * L, (void *)L, comp);
    

    或者,如果您没有 qsort_r,您可以使用 ffcall 库中的 callback(3) 包在运行时创建闭包。示例:

    #include <callback.h>
    void comp_base(void *data, va_alist alist)
    {
        va_start_int(alist);  // return type will be int
    
        int L = (int)data;
        const void *a = va_arg_ptr(alist, const void*);
        const void *b = va_arg_ptr(alist, const void*);
    
        // Now that we know L, compare
        int return_value = comp(a, b, L);
    
        va_return_int(alist, return_value);  // return return_value
    }
    
    ...    
    
    // In a function somewhere
    typedef int (*compare_func)(const void*, const void*);
    
    // Create some closures with different L values
    compare_func comp1 = (compare_func)alloc_callback(&comp_base, (void *)L1);
    compare_func comp2 = (compare_func)alloc_callback(&comp_base, (void *)L2);
    ...
    // Use comp1 & comp2, e.g. as parameters to qsort
    ...
    free_callback(comp1);
    free_callback(comp2);
    

    请注意,callback 库是线程安全的,因为所有参数都在堆栈或寄存器中传递。该库负责分配内存,确保内存是可执行的,并在必要时刷新指令缓存以允许在运行时执行动态生成的代码(即闭包)。它应该适用于各种各样的系统,但它也很可能不适用于您的系统,无论是由于错误还是缺乏实现。

    还请注意,这会增加函数调用的一些开销。上面对comp_base() 的每次调用都必须从传递给它的列表中解包其参数(这是高度依赖于平台的格式)并将其返回值填回。大多数情况下,这种开销是微不足道的,但为了比较实际执行的工作非常小并且在调用qsort() 期间会被多次调用的函数,开销非常大。

    【讨论】:

      【解决方案6】:
      std::array< std::array<int, L>, N > array;
      // or std::vector< std::vector<int> > if N*L is not a constant
      std::sort( array.begin(), array.end() );
      

      【讨论】:

      • 然后使用qsort,使用std::sort并不是更快的方法。
      【解决方案7】:

      我不确定您是否可以在不做更多工作的情况下获得相同的结果。 std::sort() 用于对由两个随机访问迭代器定义的元素序列进行排序。不幸的是,它从迭代器中确定了元素的类型。例如:

      std::sort(&array[0], &array[N + L]);
      

      将对array 的所有元素进行排序。问题在于它假定迭代器的下标、递增、递减和其他 索引 运算符会遍历序列的元素。我相信您可以对数组切片进行排序的唯一方法(我认为这就是您所追求的)是编写一个基于L 索引的迭代器。这就是sellibitze has done in the stride_iterator answer

      【讨论】:

        【解决方案8】:
        namespace
        {
            struct NewCompare
            {
                bool operator()( const int a, const int b ) const
                {
                    return a < b;
                }
        
            };
        }
        
        std::sort(array+start,array+start+L,NewCompare);
        

        使用std::stable_sort() 在真实数据集上进行测试 - 对于某些数据混合速度要快得多!

        在许多编译器 (GCC iirc) 上都有一个令人讨厌的问题:std::sort() 模板通过测试比较器来断言比较器是正确的 TWICE,一旦反转,确保结果反转!这绝对会完全破坏正常构建中中等数据集的性能。解决方案是这样的:

        #ifdef NDEBUG
          #define WAS_NDEBUG
          #undef NDEBUG
        #endif
        #define NDEBUG
        #include <algorithm>
        #ifdef WAS_NDEBUG
          #undef WAS_NDEBUG
        #else
          #undef NDEBUG
        #endif
        

        改编自这篇优秀的博文:http://www.tilander.org/aurora/2007/12/comparing-stdsort-and-qsort.html

        【讨论】:

        • 我不明白。这如何处理数组的块性质?
        【解决方案9】:

        Arkadiy 的想法是正确的。如果您创建一个指针数组并对其进行排序,则可以就地排序:

        #define NN 7
        #define LL 4
        
        int array[NN*LL] = {
            3, 5, 5, 5,
            3, 6, 6, 6,
            4, 4, 4, 4,
            4, 3, 3, 3,
            2, 2, 2, 2,
            2, 0, 0, 0,
            1, 1, 1, 1
        };
        
        struct IntPtrArrayComp {
            int length;
            IntPtrArrayComp(int len) : length(len) {}
            bool operator()(int* const & a, int* const & b) {
                for (int i = 0; i < length; ++i) {
                    if (a[i] < b[i]) return true;
                    else if (a[i] > b[i]) return false;
                }
                return false;
            }
        };
        
        void sortArrayInPlace(int* array, int number, int length)
        {
            int** ptrs = new int*[number];
            int** span = ptrs;
            for (int* a = array; a < array+number*length; a+=length) {
                *span++ = a;
            }
            std::sort(ptrs, ptrs+number, IntPtrArrayComp(length));
            int* buf = new int[number];
            for (int n = 0; n < number; ++n) {
                int offset = (ptrs[n] - array)/length;
                if (offset == n) continue;
        
                // swap
                int* a_n = array+n*length;
                std::move(a_n, a_n+length, buf);
                std::move(ptrs[n], ptrs[n]+length, a_n);
                std::move(buf, buf+length, ptrs[n]);
        
                // find what is pointing to a_n and point it 
                // to where the data was move to
                int find = 0;
                for (int i = n+1; i < number; ++i) {
                    if (ptrs[i] == a_n) {
                        find = i;
                        break;
                    }
                }
                ptrs[find] = ptrs[n];
            }
            delete[] buf;
            delete[] ptrs;
        }
        
        int main()
        {
            for (int n = 0; n< NN; ++n) {
                for (int l = 0; l < LL; ++l) {
                    std::cout << array[n*LL+l];
                }
                std::cout << std::endl;
            }
            std::cout << "----" << std::endl;
            sortArrayInPlace(array, NN, LL);
            for (int n = 0; n< NN; ++n) {
                for (int l = 0; l < LL; ++l) {
                    std::cout << array[n*LL+l];
                }
                std::cout << std::endl;
            }
            return 0;
        }
        

        输出:

        3555
        3666
        4444
        4333
        2222
        2000
        1111
        ----
        1111
        2000
        2222
        3555
        3666
        4333
        4444
        

        【讨论】:

        • 替换 int* buf = new int[number]; with int* buf = new int[length]; Arkady 的回答是正确的,但我想要一个没有第二个数组的解决方案。我将接受他的回答作为最佳解决方案,因为他提出了最初的想法。
        【解决方案10】:

        这些答案中的很多看起来都有些矫枉过正。如果你真的必须做 C++ 风格,使用 jmucchiello 的例子:

        template <int Length>
        struct Block
        {
            int n_[Length];
        
            bool operator <(Block const &rhs) const
            {
                for (int i(0); i < Length; ++i)
                {
                    if (n_[i] < rhs.n_[i])
                        return true;
                    else if (n_[i] > rhs.n_[i])
                        return false;
                }
                return false;
            }
        };
        

        然后排序:

        sort((Block<4> *)&array[0], (Block<4> *)&array[NN]);
        

        不必再复杂了。

        【讨论】:

        • 这需要在编译时知道长度。
        • 不难修改。从我基于它的示例来看,它是。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-05-26
        • 1970-01-01
        • 1970-01-01
        • 2017-09-12
        • 1970-01-01
        • 2012-11-03
        • 1970-01-01
        相关资源
        最近更新 更多