固定长度为6的整型数组中最快的排序方法

418

回答另一个Stack Overflow的问题(this one),我偶然发现了一个有趣的子问题。如何最快地对包含6个整数的数组进行排序?

由于这个问题非常低级:

  • 我们不能假定库是可用的(而且调用本身也有成本),只能使用纯C。
  • 为了避免清空指令流水线(这是一种非常高的成本),我们应该尽可能减少分支、跳转和其他任何控制流中断(例如在&&||中隐藏的那些)。
  • 空间受限,最小化寄存器和内存使用是一个问题,最好采用原地排序。

实际上,这个问题是一种高尔夫游戏,目标不是使源长度最小化,而是执行时间最短。我称之为“禅”的代码,正如Michael Abrash的书《Zen of Code optimization》及其sequels中所使用的标题。

至于为什么它很有趣,有几个层次:

  • 这个例子简单易懂,容易衡量,不需要太多的C语言技巧
  • 它展示了选择一个好的算法对问题的影响,也展示了编译器和底层硬件的影响。

这是我的参考(天真的,未经优化的)实现和测试集。

#include <stdio.h>

static __inline__ int sort6(int * d){

    char j, i, imin;
    int tmp;
    for (j = 0 ; j < 5 ; j++){
        imin = j;
        for (i = j + 1; i < 6 ; i++){
            if (d[i] < d[imin]){
                imin = i;
            }
        }
        tmp = d[j];
        d[j] = d[imin];
        d[imin] = tmp;
    }
}

static __inline__ unsigned long long rdtsc(void)
{
  unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
}

int main(int argc, char ** argv){
    int i;
    int d[6][5] = {
        {1, 2, 3, 4, 5, 6},
        {6, 5, 4, 3, 2, 1},
        {100, 2, 300, 4, 500, 6},
        {100, 2, 3, 4, 500, 6},
        {1, 200, 3, 4, 5, 600},
        {1, 1, 2, 1, 2, 1}
    };

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6 ; i++){
        sort6(d[i]);
        /*
         * printf("d%d : %d %d %d %d %d %d\n", i,
         *  d[i][0], d[i][6], d[i][7],
         *  d[i][8], d[i][9], d[i][10]);
        */
    }
    cycles = rdtsc() - cycles;
    printf("Time is %d\n", (unsigned)cycles);
}

原始结果

由于变量数量越来越多,我将它们全部收集到一个测试套件中,可以在这里找到。实际使用的测试比上面展示的要聪明一些,感谢Kevin Stock。您可以在自己的环境中编译和执行它。我对不同目标架构/编译器的行为非常感兴趣。(好的,伙计们,把它放在答案中,我会+1新结果集的每个贡献者)。

我一年前向Daniel Stutzbach(为了高尔夫)给出了答案,因为他当时是最快解决方案的源头(排序网络)。

Linux 64位,gcc 4.6.1 64位,Intel Core 2 Duo E8400,-O2

  • 直接调用qsort库函数:689.38
  • 朴素实现(插入排序):285.70
  • 插入排序(Daniel Stutzbach):142.12
  • 展开的插入排序:125.47
  • 排名排序:102.26
  • 寄存器排名排序:58.03
  • 排序网络(Daniel Stutzbach):111.68
  • 排序网络(Paul R):66.36
  • 带快速交换的排序网络12:58.86
  • 重排的排序网络12交换:53.74
  • 重排的简单交换排序网络12:31.54
  • 带快速交换的重排排序网络:31.54
  • 带快速交换的重排排序网络V2:33.63
  • 内联冒泡排序(Paolo Bonzini):48.85
  • 展开的插入排序(Paolo Bonzini):75.30

Linux 64位,gcc 4.6.1 64位,Intel Core 2 Duo E8400,-O1

  • 调用qsort库函数:705.93
  • 朴素实现(插入排序):135.60
  • 插入排序(Daniel Stutzbach):142.11
  • 展开的插入排序:126.75
  • 等级排序:46.42
  • 带寄存器的等级排序:43.58
  • 排序网络(Daniel Stutzbach):115.57
  • 排序网络(Paul R):64.44
  • 使用快速交换的12位排序网络:61.98
  • 重新排序的12位排序网络交换:54.67
  • 重新排序的12位排序网络简单交换:31.54
  • 带快速交换的重新排序排序网络:31.24
  • 带快速交换的重新排序排序网络V2:33.07
  • 内联冒泡排序(Paolo Bonzini):45.79
  • 展开的插入排序(Paolo Bonzini):80.15

我包含了-O1和-O2的结果,因为令人惊讶的是对于几个程序,O2比O1效率更低。我想知道具体哪种优化会产生这种影响?

关于提出的解决方案的评论

插入排序(Daniel Stutzbach)

如预期的那样,最小化分支确实是一个好主意。

排序网络(Daniel Stutzbach)

比插入排序更好。我想知道主要效果是否不是通过避免外部循环获得的。我尝试了展开插入排序以进行检查,确实我们得到了大致相同的数据(代码在这里)。
排序网络(Paul R)
到目前为止最好的。我用来测试的实际代码在这里。还不知道为什么它几乎比其他排序网络实现快两倍。参数传递?快速最大值?
带快速交换的12交换排序网络
正如Daniel Stutzbach建议的那样,我将他的12个交换排序网络与无分支快速交换组合在一起(代码在这里)。确实更快,迄今为止具有小幅度的优势(大约5%),因为使用1次较少的交换是可以预期的。
另外,有趣的是,分支交换似乎比在PPC架构上使用if的简单交换效率低得多(4倍)。
调用库qsort
为了再给一个参考点,我也试着按建议调用库函数qsort(代码在这里)。正如预期的那样,它慢得多:10到30倍……随着新的测试套件的出现,主要问题似乎是第一次调用后库的初始加载,而且与其他版本相比表现并不差。在我的Linux上,它只比其他版本慢3到20倍。在其他人测试时使用的某些架构上,它甚至似乎更快(我对这个结果感到非常惊讶,因为库函数qsort使用了更复杂的API)。 排名顺序 Rex Kerr提出了另一种完全不同的方法:针对数组的每个项直接计算其最终位置。这很有效,因为计算排名顺序不需要分支。但该方法的缺点是需要数组大小三倍的内存(一个数组副本和变量来存储排名顺序)。性能结果非常令人惊讶(也很有趣)。在我的32位操作系统和Intel Core2 Quad E8300参考架构上,循环计数略低于1000(像带分支交换的排序网络一样)。但是,当在我的64位系统(Intel Core2 Duo)上编译和执行时,它表现得更好:到目前为止,它成为最快的。我最终找到了真正的原因。我的32位系统使用gcc 4.4.1,而我的64位系统使用gcc 4.4.3,后者在优化这个特定代码方面要好得多(对其他提案几乎没有什么区别)。 更新: 如上面发布的数字表明,这个效果仍然受到后来版本的gcc的增强,排名顺序变得比任何其他替代方案都快两倍。

重新排序的交换排序网络12

Rex Kerr提议使用gcc 4.4.3实现的惊人效率让我想知道:一个内存使用量是无分支排序网络三倍的程序怎么可能更快呢?我的假设是它具有更少的读写依赖关系,从而允许更好地利用x86的超标量指令调度器。这给了我一个想法:重新排序交换以最小化读写依赖关系。简单来说,当你执行SWAP(1,2); SWAP(0,2);时,必须等待第一个交换完成后才能执行第二个,因为两个都访问同一内存单元。当你执行SWAP(1,2); SWAP(4,5);时,处理器可以并行执行两个操作。我尝试了一下,效果符合预期,排序网络运行速度提高了约10%。

简单交换排序网络12

在原始帖子发表一年后,Steinar H. Gunderson建议我们不要试图聪明地超越编译器,保持交换代码的简单性。这确实是个好主意,因为结果代码大约快了40%!他还提出了一种优化手动使用x86内联汇编代码的交换方法,可以节省更多的时间。最令人惊讶的是(它表明了程序员心理的很多东西),一年前我们都没有尝试过那个版本的交换。我用来测试的代码在这里。其他人建议以其他方式编写C快速交换,但使用合适的编译器时,它们与简单的方法产生相同的性能。

现在,“最佳”代码如下:

static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x) 
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
                    const int b = max(d[x], d[y]); \
                    d[x] = a; d[y] = b; }
    SWAP(1, 2);
    SWAP(4, 5);
    SWAP(0, 2);
    SWAP(3, 5);
    SWAP(0, 1);
    SWAP(3, 4);
    SWAP(1, 4);
    SWAP(0, 3);
    SWAP(2, 5);
    SWAP(1, 3);
    SWAP(2, 4);
    SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}

如果我们相信我们的测试集(是的,它相当差,它的唯一好处是简短、简单并且易于理解我们正在测量的内容),则一个排序的结果代码的平均循环次数低于40个周期(执行了6个测试)。这使得每个交换的平均时间为4个周期。我认为这非常快。还有其他改进可能吗?

2
你对整数有一些限制吗?例如,我们可以假设任何两个x、y的x-yx+y不会导致下溢或上溢吗? - Matthieu M.
3
你应该尝试将我的12次交换排序网络与Paul的无分支交换函数结合起来。他的解决方案将所有参数作为独立元素传递到堆栈上,而不是传递一个指向数组的单个指针。这可能也会有所不同。 - Daniel Stutzbach
2
请注意,在64位上正确实现rdtsc的方法是__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");,因为rdtsc将答案放在EDX:EAX中,而GCC希望它在一个64位寄存器中。您可以通过在-O3下编译来查看错误。另请参见我对Paul R关于更快速的SWAP的评论。 - Paolo Bonzini
3
@Tyler: 在汇编语言中,如果没有使用分支指令,你如何实现它? - Loren Pechtel
4
@Loren说:CMP EAX, EBX; SBB EAX, EAX会根据EAXEBX的大小关系,在EAX中放入0或0xFFFFFFFF。 SBB是"带借位减法",与ADC("带进位加法")相对应; 您所指的状态位 就是 进位位。然而,我记得在 Pentium 4 上,ADCSBB的延迟和吞吐量很差,与ADDSUB相比仍然慢两倍,并且在Core CPU上也是如此。自从80386以来,还有SETcc条件存储和CMOVcc条件移动指令,但它们也很慢。 - j_random_hacker
显示剩余37条评论
25个回答

5

我很期待尝试并学习这些示例,但首先需要从我的1.5 GHz PPC Powerbook G4 w/ 1 GB DDR RAM的一些时间开始。(我从http://www.mcs.anl.gov/~kazutomo/rdtsc.html借用了一个类似rdtsc的PPC计时器来测试时间.) 我运行了程序几次,绝对结果有所不同,但始终最快的测试是“插入排序(Daniel Stutzbach)”,其次是“未卷起的插入排序”。

以下是最后一组时间:

**Direct call to qsort library function** : 164
**Naive implementation (insertion sort)** : 138
**Insertion Sort (Daniel Stutzbach)**     : 85
**Insertion Sort Unrolled**               : 97
**Sorting Networks (Daniel Stutzbach)**   : 457
**Sorting Networks (Paul R)**             : 179
**Sorting Networks 12 with Fast Swap**    : 238
**Sorting Networks 12 reordered Swap**    : 236
**Rank Order**                            : 116

4

这是我对本帖的贡献:一个针对包含唯一值的6个整数向量(valp)进行优化的1, 4间隔希尔排序的方法。

void shellsort (int *valp)
{      
  int c,a,*cp,*ip=valp,*ep=valp+5;

  c=*valp;    a=*(valp+4);if (c>a) {*valp=    a;*(valp+4)=c;}
  c=*(valp+1);a=*(valp+5);if (c>a) {*(valp+1)=a;*(valp+5)=c;}

  cp=ip;    
  do
  {
    c=*cp;
    a=*(cp+1);
    do
    {
      if (c<a) break;

      *cp=a;
      *(cp+1)=c;
      cp-=1;
      c=*cp;
    } while (cp>=valp);
    ip+=1;
    cp=ip;
  } while (ip<ep);
}

在我的HP dv7-3010so笔记本电脑上,配备有双核Athlon M300 @ 2 Ghz处理器(DDR2内存),执行时间为165个时钟周期。这是从计时每个唯一序列(总共6!/ 720)中得出的平均值。使用OpenWatcom 1.8编译到Win32。循环本质上是一个插入排序,长16条指令/37字节。
我没有64位环境进行编译。

好的。我会将其添加到更长的测试套件中。 - kriss

3
如果插入排序在这里相当有竞争力,我建议尝试希尔排序。恐怕6个元素可能不足以成为最佳选择,但值得一试。
示例代码未经测试、未经调试等。您需要通过调整inc = 4和inc -= 3序列来找到最佳值(例如尝试inc = 2, inc -= 1)。
static __inline__ int sort6(int * d) {
    char j, i;
    int tmp;
    for (inc = 4; inc > 0; inc -= 3) {
        for (i = inc; i < 5; i++) {
            tmp = a[i];
            j = i;
            while (j >= inc && a[j - inc] > tmp) {
                a[j] = a[j - inc];
                j -= inc;
            }
            a[j] = tmp;
        }
    }
}

我认为这不会获胜,但如果有人发布一个关于对10个元素进行排序的问题,谁知道呢...

根据维基百科的说法,这甚至可以与排序网络结合使用: 普拉特(Pratt,1979)。希尔排序和排序网络(计算机科学杰出论文集)。Garland。ISBN 0-824-04406-1


随意提出一些实现方案 :-) - kriss
提案已添加。享受那些漏洞吧。 - gcp

3

我知道我很晚了,但我对尝试一些不同的解决方案感到兴趣。首先,我清理了那个粘贴板,使它编译并将其放入存储库中。我保留了一些不良解决方案作为死胡同,以便其他人不会尝试。其中包括我的第一个解决方案,它试图确保x1>x2只被计算一次。经过优化后,它的速度不比其他简单版本快。

我添加了一个循环版本的等级排序,因为我对这项研究的应用是对2-8个项目进行排序,因此由于参数数量可变,必须使用循环。这也是我忽略排序网络解决方案的原因。

测试代码没有测试重复项是否被正确处理,因此虽然现有的解决方案都是正确的,但我在测试代码中添加了特殊情况以确保重复项被正确处理。

然后,我编写了一个完全在AVX寄存器中的插入排序。在我的机器上,它比其他插入排序快25%,但比等级排序慢100%。我之所以这样做纯粹是为了实验,并没有期望这会更好,因为插入排序中存在分支。

static inline void sort6_insertion_sort_avx(int* d) {
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], 0, 0);
    __m256i index = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
    __m256i shlpermute = _mm256_setr_epi32(7, 0, 1, 2, 3, 4, 5, 6);
    __m256i sorted = _mm256_setr_epi32(d[0], INT_MAX, INT_MAX, INT_MAX,
            INT_MAX, INT_MAX, INT_MAX, INT_MAX);
    __m256i val, gt, permute;
    unsigned j;
     // 8 / 32 = 2^-2
#define ITER(I) \
        val = _mm256_permutevar8x32_epi32(src, _mm256_set1_epi32(I));\
        gt =  _mm256_cmpgt_epi32(sorted, val);\
        permute =  _mm256_blendv_epi8(index, shlpermute, gt);\
        j = ffs( _mm256_movemask_epi8(gt)) >> 2;\
        sorted = _mm256_blendv_epi8(_mm256_permutevar8x32_epi32(sorted, permute),\
                val, _mm256_cmpeq_epi32(index, _mm256_set1_epi32(j)))
    ITER(1);
    ITER(2);
    ITER(3);
    ITER(4);
    ITER(5);
    int x[8];
    _mm256_storeu_si256((__m256i*)x, sorted);
    d[0] = x[0]; d[1] = x[1]; d[2] = x[2]; d[3] = x[3]; d[4] = x[4]; d[5] = x[5];
#undef ITER
}

接着,我使用 AVX 写了一个排序算法。虽然速度与其他排序算法相当,但并没有更快。问题在于我只能使用 AVX 计算索引,然后必须制作一个索引表。这是因为计算是基于目标而非源的。详见从基于源的索引转换为基于目标的索引

static inline void sort6_rank_order_avx(int* d) {
    __m256i ror = _mm256_setr_epi32(5, 0, 1, 2, 3, 4, 6, 7);
    __m256i one = _mm256_set1_epi32(1);
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], INT_MAX, INT_MAX);
    __m256i rot = src;
    __m256i index = _mm256_setzero_si256();
    __m256i gt, permute;
    __m256i shl = _mm256_setr_epi32(1, 2, 3, 4, 5, 6, 6, 6);
    __m256i dstIx = _mm256_setr_epi32(0,1,2,3,4,5,6,7);
    __m256i srcIx = dstIx;
    __m256i eq = one;
    __m256i rotIx = _mm256_setzero_si256();
#define INC(I)\
    rot = _mm256_permutevar8x32_epi32(rot, ror);\
    gt = _mm256_cmpgt_epi32(src, rot);\
    index = _mm256_add_epi32(index, _mm256_and_si256(gt, one));\
    index = _mm256_add_epi32(index, _mm256_and_si256(eq,\
                _mm256_cmpeq_epi32(src, rot)));\
    eq = _mm256_insert_epi32(eq, 0, I)
    INC(0);
    INC(1);
    INC(2);
    INC(3);
    INC(4);
    int e[6];
    e[0] = d[0]; e[1] = d[1]; e[2] = d[2]; e[3] = d[3]; e[4] = d[4]; e[5] = d[5];
    int i[8];
    _mm256_storeu_si256((__m256i*)i, index);
    d[i[0]] = e[0]; d[i[1]] = e[1]; d[i[2]] = e[2]; d[i[3]] = e[3]; d[i[4]] = e[4]; d[i[5]] = e[5];
}

这个仓库可以在这里找到:https://github.com/eyepatchParrot/sort6/


1
您可以在整数向量上使用vmovmskps(通过强制转换以使内部函数正常工作),从而避免需要对位扫描(ffs)结果进行右移。 - Peter Cordes
1
你可以根据 cmpgt 的结果有条件地添加1,而不是使用 set1(1) 进行掩码。例如,index = _mm256_sub_epi32(index, gt) 表示 index -= -1 或 0; - Peter Cordes
1
如果eq = _mm256_insert_epi32(eq, 0, I)按原样编译(特别是对于低4个元素之外的元素),则不是零化元素的有效方法,因为vpinsrd仅适用于XMM目标;高于3的索引必须被模拟。相反,使用一个零向量的_mm256_blend_epi32(vpblendd)。vpblendd是一个单uop指令,可以在任何端口上运行,而洗牌需要Intel CPU上的端口5。(https://agner.org/optimize/) - Peter Cordes
1
此外,您可能考虑使用不同的洗牌方式从同一源生成“rot”向量,或者至少并行运行2个dep链,交替使用它们,而不是通过跨越车道的洗牌(3个周期延迟)进行单个dep链。这将增加单个排序中的ILP。 2个dep链限制了向量常数的数量,只有2个:一个用于一个旋转,另一个用于组合2个旋转步骤。 - Peter Cordes

2
这个问题已经很久了,但我最近也不得不解决同样的问题:如何快速地对小数组进行排序。我认为分享我的知识是一个好主意。虽然我一开始使用排序网络,但最终我成功地找到了其他算法,用于对6个值的每种排列进行排序时执行的总比较次数比排序网络少,而且比插入排序少。我没有计算交换的次数;我希望它大致相等(有时可能略高)。
算法“sort6”使用算法“sort4”,后者使用算法“sort3”。以下是一些轻量级C++代码实现(原始版本使用模板,以便可以与任何随机访问迭代器和任何合适的比较函数一起使用)。

对3个值进行排序

下面的算法是一个展开的插入排序。当必须执行两次交换(6次赋值)时,它使用4次赋值:
void sort3(int* array)
{
    if (array[1] < array[0]) {
        if (array[2] < array[0]) {
            if (array[2] < array[1]) {
                std::swap(array[0], array[2]);
            } else {
                int tmp = array[0];
                array[0] = array[1];
                array[1] = array[2];
                array[2] = tmp;
            }
        } else {
            std::swap(array[0], array[1]);
        }
    } else {
        if (array[2] < array[1]) {
            if (array[2] < array[0]) {
                int tmp = array[2];
                array[2] = array[1];
                array[1] = array[0];
                array[0] = tmp;
            } else {
                std::swap(array[1], array[2]);
            }
        }
    }
}

看起来有些复杂,因为排序对于数组的每种可能的排列方式都有一个分支,使用 2~3 个比较和最多 4 次赋值来对三个值进行排序。

四个值的排序

这个函数调用sort3,然后使用数组的最后一个元素执行展开插入排序:

void sort4(int* array)
{
    // Sort the first 3 elements
    sort3(array);

    // Insert the 4th element with insertion sort 
    if (array[3] < array[2]) {
        std::swap(array[2], array[3]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[1] < array[0]) {
                std::swap(array[0], array[1]);
            }
        }
    }
}

该算法进行3到6次比较,最多5次交换。插入排序很容易展开,但我们将使用另一种算法进行最后的排序...
排序6个值
这个算法使用了我称之为“双插入排序”的展开版本。这个名称并不好听,但它相当形象,下面是它的工作原理:
1. 对数组中除第一个和最后一个元素以外的所有元素进行排序。
2. 如果第一个元素大于最后一个元素,则交换它们。
3. 从前面将第一个元素插入排序序列中,然后从后面将最后一个元素插入排序序列中。
在交换后,第一个元素始终小于最后一个元素,这意味着,在将它们插入排序序列时,最坏情况下不会超过N次比较:例如,如果第一个元素已经插入到第三个位置,则最后一个元素不能插入到低于第四个位置的任何位置。
void sort6(int* array)
{
    // Sort everything but first and last elements
    sort4(array+1);

    // Switch first and last elements if needed
    if (array[5] < array[0]) {
        std::swap(array[0], array[5]);
    }

    // Insert first element from the front
    if (array[1] < array[0]) {
        std::swap(array[0], array[1]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[4] < array[3]) {
                    std::swap(array[3], array[4]);
                }
            }
        }
    }

    // Insert last element from the back
    if (array[5] < array[4]) {
        std::swap(array[4], array[5]);
        if (array[4] < array[3]) {
            std::swap(array[3], array[4]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[2] < array[1]) {
                    std::swap(array[1], array[2]);
                }
            }
        }
    }
}

我的测试结果表明,这个算法在处理任意6个值的排列时,总是需要进行6到13次比较。我没有计算出所执行的交换次数,但在最坏情况下,我不认为会超过11次。
希望这能有所帮助,尽管这个问题可能已经不再是一个实际问题了 :)
编辑后,在提供的基准测试中,它显然比大多数有趣的替代方案要慢。它往往比未展开的插入排序表现得更好,但也仅限于此。基本上,它不是最适合整数排序的算法,但对于具有昂贵比较操作的类型可能会有一些兴趣。

这很不错。由于问题的解决已经有几十年的历史,可能和C编程一样古老,现在这个问题已经快5年了,看起来并不那么相关。 - kriss
你应该看看其他答案是如何测试时间的。重点是,对于这样小的数据集,仅仅计算比较甚至比较和交换并不能真正反映出算法的快慢(基本上,对 6 个整数进行排序始终是 O(1),因为 O(6*6) 是 O(1))。而先前提出的解决方案中当前最快的方法是通过大型比较立即找到每个值的位置(由 RexKerr 提供)。 - kriss
@kriss 现在是最快的吗?从结果来看,排序网络方法是最快的,我的错。我的解决方案也来自于我的通用库,我并不总是比较整数,也不总是使用 operator< 进行比较。除了比较和交换的客观计数之外,我还适当地计时了我的算法;这个解决方案是最快的通用解决方案,但我确实错过了 @RexKerr 的解决方案。要试试它 :) - Morwenn
RexKerr(Order Rank)提出的解决方案在X86架构上成为自gcc编译器4.2.3以来最快的(并且在gcc 4.9时几乎比第二名快了两倍)。但它严重依赖于编译器优化,在其他架构上可能不成立。 - kriss
@kriss 很有趣的知识。我确实可以通过 -O3 再次发现更多的差异。我想我会采用另一种策略来设计我的排序库:提供三种算法,以便具有较少的比较、较少的交换或潜在的最佳性能。至少,读者将能够清楚地了解发生了什么。感谢您的见解 :) - Morwenn

2

我发现在我的系统上,下面定义的函数sort6_iterator()sort6_iterator_local()至少跑得和当前记录保持者一样快,而且经常会更快:

#define MIN(x, y) (x<y?x:y)
#define MAX(x, y) (x<y?y:x)

template<class IterType> 
inline void sort6_iterator(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(*(it + x), *(it + y)); \
  const auto b = MAX(*(it + x), *(it + y)); \
  *(it + x) = a; *(it + y) = b; }
  
  SWAP(1, 2) SWAP(4, 5)
  SWAP(0, 2) SWAP(3, 5)
  SWAP(0, 1) SWAP(3, 4)
  SWAP(1, 4) SWAP(0, 3)
  SWAP(2, 5) SWAP(1, 3)
  SWAP(2, 4)
  SWAP(2, 3)
#undef SWAP
}

在我的计时代码中,我将一个 std::vector 的迭代器传递给了这个函数。

编译器可以更积极地优化模板函数,这可能是速度快的原因之一。(我还怀疑使用迭代器会给 g++ 提供某些保证(否则它不会有),关于迭代器所引用的内存,这有助于优化。)如果我没记错的话,这也是为什么许多 STL 算法(例如 std::sort())通常具有如此令人难以置信的性能的部分原因。

编译器可以这样做,因为模板函数不是函数。C++从 C 中继承了许多规则,限制了编译器对代码的重排、更改和删除,以及编译器可以做出哪些假设的限制。

C++ 委员会决定,模板应遵循不同的规则,这些规则更易于优化。

例如:在void sort2(int* p, int* q){/*...*/}中,编译器不能假设pq指向不重叠的内存,也不能假设p != q(因为可能在某处调用swap2(d,d)),甚至不能假设pq不是空指针。如果它是inline的,则可能会在像swap2(d,d+1)这样的调用中弄清楚p != q。编译器还对引用有一定的保证(例如:它们永远不会为空,不像指针),因此它们比指针更容易进行优化。
此外,如果另一个源文件尝试通过函数指针调用swapMem,编译器必须将像sort2这样的函数存储在内存中。对于像template void sort2(T* p, T* q){/*...*/}这样的模板函数,不需要通过函数指针访问。有一些限制适用于不能应用于函数模板的函数inline(我见过的所有STL算法实现都有大量的方法/函数(模板)调用,但它们仍然运行得非常快,因为大多数情况下它们确实被编译器inline掉了)。
重要的是要提到,哪种代码最快可能很大程度上取决于优化器。由于不同的代码可能会被不同地优化,这可能解释了为什么sort6的变体(例如使用不同的排序网络或以不同方式定义MAX/MIN/SWAP)可能具有非常不同的运行时间。
例如,sort6_iterator()有时(再次取决于调用函数的上下文)会被以下排序函数一致地优化,该函数在排序之前将数据复制到本地变量中。1由于只定义了6个本地变量,如果这些本地变量是原语类型,则它们很可能从未实际存储在RAM中,而仅存储在CPU的寄存器中直到函数调用结束,这有助于使此排序函数快速。(编译器也知道不同的本地变量在内存中具有不同的位置,这也有助于提高性能)。
template<class IterType> 
inline void sort6_iterator_local(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  const auto b = MAX(data##x, data##y); \
  data##x = a; data##y = b; }
//DD = Define Data
#define DD1(a)   auto data##a = *(it + a);
#define DD2(a,b) auto data##a = *(it + a), data##b = *(it + b);
//CB = Copy Back
#define CB(a) *(it + a) = data##a;
  
  DD2(1,2)    SWAP(1, 2)
  DD2(4,5)    SWAP(4, 5)
  DD1(0)      SWAP(0, 2)
  DD1(3)      SWAP(3, 5)
  SWAP(0, 1)  SWAP(3, 4)
  SWAP(1, 4)  SWAP(0, 3)   CB(0)
  SWAP(2, 5)  CB(5)
  SWAP(1, 3)  CB(1)
  SWAP(2, 4)  CB(4)
  SWAP(2, 3)  CB(2)        CB(3)
#undef CB
#undef DD2
#undef DD1
#undef SWAP
}

请注意,将SWAP()定义为以下内容有时会导致略微更好的性能,尽管大多数情况下它会导致略微更差的性能或性能上的可忽略差异。
#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  data##y = MAX(data##x, data##y); \
  data##x = a; }

如果你只是想要一个在原始数据类型上进行排序的算法,无论调用排序函数的上下文是什么,gcc -O3 都非常擅长优化1。那么,根据你如何传递输入,可以尝试以下两种算法之一:
template<class T> inline void sort6(T it) {
#define SORT2(x,y) {if(data##x>data##y){auto a=std::move(data##y);data##y=std::move(data##x);data##x=std::move(a);}}
#define DD1(a)   register auto data##a=*(it+a);
#define DD2(a,b) register auto data##a=*(it+a);register auto data##b=*(it+b);
#define CB1(a)   *(it+a)=data##a;
#define CB2(a,b) *(it+a)=data##a;*(it+b)=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

或者,如果您想通过引用传递变量,则可以使用以下代码(下面的函数与上面的函数在前5行中有所不同):

template<class T> inline void sort6(T& e0, T& e1, T& e2, T& e3, T& e4, T& e5) {
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   register auto data##a=e##a;
#define DD2(a,b) register auto data##a=e##a;register auto data##b=e##b;
#define CB1(a)   e##a=data##a;
#define CB2(a,b) e##a=data##a;e##b=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

使用register关键字的原因是因为这是您知道要将这些int值放在寄存器中的少数几个时刻之一(如果可能的话)。没有register,编译器会大部分时间弄清楚这一点,但有时它不会。使用register关键字有助于解决这个问题。通常情况下,不要使用register关键字,因为它更可能减慢您的代码速度而不是加快它(例如:对于sort6使用所有6个寄存器意味着它们无法用于其他东西,这可能导致整体速度较慢)。
1. 在测试不同排序函数的时间时,我发现调用排序函数的上下文(即周围的代码)对性能有重大影响,这很可能是由于该函数被内联并进行了优化。例如,如果程序足够简单,则传递指针和传递迭代器之间的性能差异通常不大;否则,使用迭代器通常会导致明显更好的性能,并且从未(至少在我的经验中)出现过明显更差的性能。我怀疑这可能是因为g ++可以全局优化足够简单的代码。

1

我想尝试未卷起的Ford-Johnson合并插入排序,它可以实现最少可能的比较次数(ceil(log2(6!)) = 10),且没有交换。但是它的效率不高(我得到的时间略好于最差的排序网络解决方案 sort6_sorting_network_v1)。

该算法将值加载到六个寄存器中,然后执行8到10次比较,以确定它在720=6!种情况中的哪一种,然后将这些寄存器按照适当的顺序写回其中的一个(每个情况都有单独的代码)。在最后的写回之前,不会进行任何交换或重新排序。我没有查看生成的汇编代码。

static inline void sort6_ford_johnson_unrolled(int *D) {
  register int a = D[0], b = D[1], c = D[2], d = D[3], e = D[4], f = D[5];
  #define abcdef(a,b,c,d,e,f) (D[0]=a, D[1]=b, D[2]=c, D[3]=d, D[4]=e, D[5]=f)
  #define abdef_cd(a,b,c,d,e,f) (c<a ? abcdef(c,a,b,d,e,f) \
                                     : c<b ? abcdef(a,c,b,d,e,f) \
                                           : abcdef(a,b,c,d,e,f))
  #define abedf_cd(a,b,c,d,e,f) (c<b ? c<a ? abcdef(c,a,b,e,d,f) \
                                           : abcdef(a,c,b,e,d,f) \
                                     : c<e ? abcdef(a,b,c,e,d,f) \
                                           : abcdef(a,b,e,c,d,f))
  #define abdf_cd_ef(a,b,c,d,e,f) (e<b ? e<a ? abedf_cd(e,a,c,d,b,f) \
                                             : abedf_cd(a,e,c,d,b,f) \
                                       : e<d ? abedf_cd(a,b,c,d,e,f) \
                                             : abdef_cd(a,b,c,d,e,f))
  #define abd_cd_ef(a,b,c,d,e,f) (d<f ? abdf_cd_ef(a,b,c,d,e,f) \
                                      : b<f ? abdf_cd_ef(a,b,e,f,c,d) \
                                            : abdf_cd_ef(e,f,a,b,c,d))
  #define ab_cd_ef(a,b,c,d,e,f) (b<d ? abd_cd_ef(a,b,c,d,e,f) \
                                     : abd_cd_ef(c,d,a,b,e,f))
  #define ab_cd(a,b,c,d,e,f) (e<f ? ab_cd_ef(a,b,c,d,e,f) \
                                  : ab_cd_ef(a,b,c,d,f,e))
  #define ab(a,b,c,d,e,f) (c<d ? ab_cd(a,b,c,d,e,f) \
                               : ab_cd(a,b,d,c,e,f))
  a<b ? ab(a,b,c,d,e,f)
      : ab(b,a,c,d,e,f);
  #undef ab
  #undef ab_cd
  #undef ab_cd_ef
  #undef abd_cd_ef
  #undef abdf_cd_ef
  #undef abedf_cd
  #undef abdef_cd
  #undef abcdef
}

TEST(ford_johnson_unrolled,   "Unrolled Ford-Johnson Merge-Insertion sort");

通过尽可能少的比较来选择正确的变量顺序的想法也是Rank Order的基础。看起来,如果避免交换很好,那么拥有10个分支和720个代码路径并不便宜。 - kriss
@kriss 看起来有点相似,但我不认为基于排名顺序的解决方案会进行最少数量的比较,对吧?看起来其中一个需要25次比较,另一个需要15次。此外,Rank Order的结尾赋值通过间接寻址实现。当然,Rank Order仍然胜出,但我想知道我的方法是否会在未来拥有更多指令缓存或其他资源的机器上获胜。 - Don Hatch
当分支被实现为跳转时,它很可能是最昂贵的CPU特性,因为它会清空所有缓存和预期执行管道。我看不到任何进化会使它变得便宜,尤其是有720个独特的代码路径。单个测试可以很便宜,因为它可以作为条件赋值无分支实现。排序的核心思想是进行测试,但实际上没有分支。这里的问题很可能是每个最小测试的后续条件分支。但我看不到如何避免并保持比较最小。 - kriss
@kriss 我所想的“未来机器”场景就是这样的:https://en.wikipedia.org/wiki/Speculative_execution#Eager_execution 。 “在资源无限的情况下,急切执行……理论上将提供与完美分支预测相同的性能”。 - Don Hatch
我理解,但至少在硬件层面上,我不相信它的实际可行性。即使是分支预测在预测失败时也不够有效。当然,我们可以想象在相同代码上运行720个处理器,只有其中一个保留结果,但要花费如此多的资源,我们必须想象一个使用案例,其中任何小的速度提升都比使用的任何资源更重要。并且选择正确的结果成本非常小。 - kriss
如果我们采用无限的资源路径,我们可以想象所有的排序都在同一时间被测试,源代码中的比较总数并不重要。我可以想象有720个平行的代码路径,每个路径执行5个比较(以检查结果是否已排序)。这仍然是一种资源浪费,但在硬件层面上很简单(没有组合爆炸,代码专注于解决这个特定的问题)。 - kriss

1

我知道这是一个老问题。

但我刚写了一种不同的解决方案,想要分享。
仅使用嵌套的最小值和最大值操作,

它并不快,因为它使用了114次每个操作,
可以简单地将其减少到75次,如此 -> pastebin

但那时它就不是纯粹的最小/最大操作了。

可能会有用的是使用AVX同时对多个整数进行最小/最大操作。

PMINSW reference

#include <stdio.h>

static __inline__ int MIN(int a, int b){
int result =a;
__asm__ ("pminsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ int MAX(int a, int b){
int result = a;
__asm__ ("pmaxsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ unsigned long long rdtsc(void){
  unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" :
  "=A" (x));
  return x;
}

#define MIN3(a, b, c) (MIN(MIN(a,b),c))
#define MIN4(a, b, c, d) (MIN(MIN(a,b),MIN(c,d)))

static __inline__ void sort6(int * in) {
  const int A=in[0], B=in[1], C=in[2], D=in[3], E=in[4], F=in[5];

  in[0] = MIN( MIN4(A,B,C,D),MIN(E,F) );

  const int
  AB = MAX(A, B),
  AC = MAX(A, C),
  AD = MAX(A, D),
  AE = MAX(A, E),
  AF = MAX(A, F),
  BC = MAX(B, C),
  BD = MAX(B, D),
  BE = MAX(B, E),
  BF = MAX(B, F),
  CD = MAX(C, D),
  CE = MAX(C, E),
  CF = MAX(C, F),
  DE = MAX(D, E),
  DF = MAX(D, F),
  EF = MAX(E, F);

  in[1] = MIN4 (
  MIN4( AB, AC, AD, AE ),
  MIN4( AF, BC, BD, BE ),
  MIN4( BF, CD, CE, CF ),
  MIN3( DE, DF, EF)
  );

  const int
  ABC = MAX(AB,C),
  ABD = MAX(AB,D),
  ABE = MAX(AB,E),
  ABF = MAX(AB,F),
  ACD = MAX(AC,D),
  ACE = MAX(AC,E),
  ACF = MAX(AC,F),
  ADE = MAX(AD,E),
  ADF = MAX(AD,F),
  AEF = MAX(AE,F),
  BCD = MAX(BC,D),
  BCE = MAX(BC,E),
  BCF = MAX(BC,F),
  BDE = MAX(BD,E),
  BDF = MAX(BD,F),
  BEF = MAX(BE,F),
  CDE = MAX(CD,E),
  CDF = MAX(CD,F),
  CEF = MAX(CE,F),
  DEF = MAX(DE,F);

  in[2] = MIN( MIN4 (
  MIN4( ABC, ABD, ABE, ABF ),
  MIN4( ACD, ACE, ACF, ADE ),
  MIN4( ADF, AEF, BCD, BCE ),
  MIN4( BCF, BDE, BDF, BEF )),
  MIN4( CDE, CDF, CEF, DEF )
  );


  const int
  ABCD = MAX(ABC,D),
  ABCE = MAX(ABC,E),
  ABCF = MAX(ABC,F),
  ABDE = MAX(ABD,E),
  ABDF = MAX(ABD,F),
  ABEF = MAX(ABE,F),
  ACDE = MAX(ACD,E),
  ACDF = MAX(ACD,F),
  ACEF = MAX(ACE,F),
  ADEF = MAX(ADE,F),
  BCDE = MAX(BCD,E),
  BCDF = MAX(BCD,F),
  BCEF = MAX(BCE,F),
  BDEF = MAX(BDE,F),
  CDEF = MAX(CDE,F);

  in[3] = MIN4 (
  MIN4( ABCD, ABCE, ABCF, ABDE ),
  MIN4( ABDF, ABEF, ACDE, ACDF ),
  MIN4( ACEF, ADEF, BCDE, BCDF ),
  MIN3( BCEF, BDEF, CDEF )
  );

  const int
  ABCDE= MAX(ABCD,E),
  ABCDF= MAX(ABCD,F),
  ABCEF= MAX(ABCE,F),
  ABDEF= MAX(ABDE,F),
  ACDEF= MAX(ACDE,F),
  BCDEF= MAX(BCDE,F);

  in[4]= MIN (
  MIN4( ABCDE, ABCDF, ABCEF, ABDEF ),
  MIN ( ACDEF, BCDEF )
  );

  in[5] = MAX(ABCDE,F);
}

int main(int argc, char ** argv) {
  int d[6][6] = {
    {1, 2, 3, 4, 5, 6},
    {6, 5, 4, 3, 2, 1},
    {100, 2, 300, 4, 500, 6},
    {100, 2, 3, 4, 500, 6},
    {1, 200, 3, 4, 5, 600},
    {1, 1, 2, 1, 2, 1}
  };

  unsigned long long cycles = rdtsc();
  for (int i = 0; i < 6; i++) {
    sort6(d[i]);
  }
  cycles = rdtsc() - cycles;
  printf("Time is %d\n", (unsigned)cycles);

  for (int i = 0; i < 6; i++) {
    printf("d%d : %d %d %d %d %d %d\n", i,
     d[i][0], d[i][1], d[i][2],
     d[i][3], d[i][4], d[i][5]);
  }
}

编辑:
受Rex Kerr启发的排名解决方案, 比上面的混乱要快得多

static void sort6(int *o) {
const int 
A=o[0],B=o[1],C=o[2],D=o[3],E=o[4],F=o[5];
const unsigned char
AB = A>B, AC = A>C, AD = A>D, AE = A>E,
          BC = B>C, BD = B>D, BE = B>E,
                    CD = C>D, CE = C>E,
                              DE = D>E,
a =          AB + AC + AD + AE + (A>F),
b = 1 - AB      + BC + BD + BE + (B>F),
c = 2 - AC - BC      + CD + CE + (C>F),
d = 3 - AD - BD - CD      + DE + (D>F),
e = 4 - AE - BE - CE - DE      + (E>F);
o[a]=A; o[b]=B; o[c]=C; o[d]=D; o[e]=E;
o[15-a-b-c-d-e]=F;
}

1
总是很高兴看到新的解决方案。看起来有一些简单的优化是可能的。最终它可能与排序网络并没有太大的区别。 - kriss
是的,MIN和MAX的数量可能会减少,例如MIN(AB,CD)重复了几次,但我认为很难将它们大量减少。我添加了您的测试用例。 - PrincePolka
pmin/maxsw 操作的是打包的 16 位有符号整数(int16_t)。但是你的 C 函数声称它对一个 int 数组进行排序(在所有支持该 asm 语法的 C 实现中,int 都是 32 位的)。你是否只测试了仅具有高半部分为 0 的小正整数?那样可以工作... 对于 int,你需要 SSE4.1 的 pmin/maxsd(d = dword)。https://www.felixcloutier.com/x86/pminsd:pminsq 或者 pminusd 适用于 uint32_t - Peter Cordes

1

我相信你的问题有两个方面。

  • 第一个是确定最优算法。至少在这种情况下,通过循环遍历每个可能的排序(并不是那么多),可以计算出比较和交换的确切最小值、最大值、平均值和标准差。也要准备好一两个备选方案。
  • 第二个是优化算法。可以采取许多措施将教科书代码示例转换为精简高效的实际算法。如果意识到算法无法达到所需的程度,可以尝试备选方案。

我不会太担心清空流水线(假设当前为x86):分支预测已经取得了长足的进步。我担心的是确保代码和数据各自适合一个缓存行(代码可能需要两个)。一旦这样做,获取延迟就会令人耳目一新,这将弥补任何停顿。这也意味着你的内部循环可能只有十条指令左右,这正是它应该在的位置(我的排序算法中有两个不同的内部循环,它们分别为10条指令/22字节和9/22长)。假设代码不包含任何除法,你可以确信它将非常快速。


我不确定如何理解你的回答。首先,我完全不了解你提出的算法是什么,也不明白它如何优化,因为你必须循环遍历720个可能的排序方式(现有的答案所需的循环次数远少于720)。如果你有随机输入,我无法想象(即使在理论层面上)分支预测如何比50-50表现更好,除非它根本不关心输入数据。此外,大多数已经提出的解决方案已经可以使数据和代码完全缓存,可以说已经很好了。但也许我完全误解了你的回答。有没有可能展示一些代码? - kriss
我的意思是,只有720(6!)种不同的6个整数组合,通过运行它们中的所有组合来确定许多事情,正如我所提到的那样 - 这是理论部分。实践部分是微调算法,使其在尽可能少的时钟周期内运行。我对排序6个整数的起点是1、4间隔希尔排序。4个间隔为1个间隔的良好分支预测铺平了道路。 - Olof Forshell
你是不是忘记计算用于结束循环的比较次数了? - kriss
1
我不理解“正常随机分布” - 我正在测试每种可能的组合,并确定最小/平均/最大统计数据。 Shellsort是一系列插入排序,其递减增量,使得最终增量-1-所执行的工作要比纯插入排序中单独执行的工作少得多。至于时钟计数,我的算法平均需要406个时钟周期,这包括收集统计信息并对实际排序例程进行两次调用 - 每个间隔一次。这是在Athlon M300移动设备上,使用OpenWatcom编译器。 - Olof Forshell
1
“正态随机分布”意味着排序后的实际数据的每种组合可能不具有相等的概率。如果每种组合的概率不相等,则您的统计数据将出现错误,因为平均值需要考虑给定分布可能发生的次数。对于时钟计数,如果您尝试任何其他此类实现(上面提供了链接)并在测试系统上运行它,我们将有一个比较基础,并查看您选择的实现的表现如何。 - kriss
显示剩余4条评论

1

我知道派对已经结束了12年,但是还是。

我发现使用并行迭代来排序少量项目的最佳方法。

shifted = [0 sorted](1 : length(sorted));
shifted = max(shifted, new_value);
sorted = min(sorted, shifted);

sorted = ones(1, N) * max_value;开始

即使有这个,它也可以很好地进行SIMD化。

using vec4 = int32_t __attribute__((vector_size(16));

int32_t max = 0x7fffffff;

vec4 sorted_lo = { max, max, max, max }, sorted_hi = sorted_lo;
sorted_lo[0] = data[0];  // "pre-sort" the first element directly

for (int i = 1 ; i < 6; i++) {
 vec4 new_value{ data[i], data[i], data[i], data[i] };
 vec4 shifted_lo = { 0, sorted_lo[0], sorted_lo[1], sorted_lo[2]};
 vec4 shifted_hi = { sorted_lo[3],sorted_hi[0],sorted_hi[1],sorted_hi[2]};

 shifted_lo = max(shifted_lo, new_value0);
 shifted_hi = max(shifted_hi, new_value0);

 sorted_lo = max(sorted_lo, shifted_lo);
 sorted_hi = max(sorted_hi, shifted_hi);
}

许多SIMD体系结构(SSE4、ARM64)包含min/max操作,以及两个向量之间的移位和单个lane的重复。在AVX2中,不能有效地在lanes之间移位,但可以对5-8个元素的两个独立向量进行排序。

一个大大改进的版本将使用带有两个向量的双调合并排序。第一个向量使用插入排序技巧对前3个值进行排序,例如1 8 4 3 5 4变为1 4 8 inf,另一个向量按inf 5 4 3排序。

然后,双调合并阶段将对A = min(a,b); B = max(a,b)进行排序,然后并行地对A[0]<->A[2], A[1]<->A[3], B[0]<->B[2], B[1]<->B[3]进行排序,最后一步是A[0]<->A[1], A[2]<->A[3],B[0]<->B[1], B[2]<->B[3]

以示例值为例,这意味着

  A = 1 4 8 inf, B = inf 5 4 3
      <--------------->
        <---------------->
          <---------------->
             <--------------->
  A = 1 4 4 3  , B = inf 5 8 inf
      <--->          <----->
        <--->            <---->
  A = 1 3 4 4  , B = 8   5 inf inf
      <-> <->        <---> <---->
  A = 1 3 4 5  , B = 5   8 inf inf

也许最困难的步骤是A[0,2],A[1,3],B[0,2],B[1,3]阶段,需要进行一些洗牌。相比之下,至少ARM64 NEON有一条指令可以提取成对的最小/最大值。

回答评论,将数据传输到向量寄存器通常不是问题。当从SIMD寄存器或内存检索数据时,可能会出现较长的延迟。例如,对于SSE2,我会尝试将6个元素放入一个向量中。

int32_t x[8] = {inf, 0,0,0,0,0,0, inf};

这将允许快速洗牌到(pshufd / _mm_shuffle_epi32),以获取val[1]、inf、inf、inf的第一个排序向量与inf、inf、inf、val[4]相比,并且可以在XXM寄存器上广播任何车道。


排序永远不会过时。:-) 当然,它仍需要进行测量,因为对于少量项目,将数据加载到向量寄存器中可能比加速更耗费时间。 - kriss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接