最快的固定长度6 int数组


401

在回答另一个堆栈溢出问题(这个问题)时,我偶然发现了一个有趣的子问题。对6个整数数组进行排序的最快方法是什么?

由于问题非常低:

  • 我们不能假设库可用(并且调用本身有成本),只能使用普通C语言
  • 为了避免排空指令流水线(具有非常高的成本),我们也许应该尽量减少分支机构,跳跃,和所有其他类型的控制流断裂(像那些隐藏在背后的序列点&&||)。
  • 房间受到限制,尽量减少寄存器和内存使用是一个问题,理想情况下,最好在适当的位置进行排序。

确实,这个问题是一种高尔夫,其目标不是最小化源代码长度,而是执行时间。我将其称为“ Zening”代码,如Michael Abrash所著的《Zen of Code Optimization》及其续集的书名中所用。

至于为什么有趣,它分为几层:

  • 该示例简单易懂,易于度量,不涉及太多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和-02的结果,因为出奇的好节目O2是比O1效率。我想知道具体的优化有什么作用?

对拟议解决方案的评论

插入排序(Daniel Stutzbach)

不出所料,尽量减少分支机构确实是一个好主意。

分拣网络(Daniel Stutzbach)

比插入排序更好。我想知道是否主要的效果不是避免外部循环。我尝试通过展开插入排序进行检查,实际上我们得到的数字大致相同(代码在此处)。

分类网络(Paul R)

迄今为止最好的。我用来测试的实际代码在这里。尚不知道为什么它的速度是其他排序网络实现的近两倍。参数传递 快速最大?

排序网络12 SWAP快速交换

正如Daniel Stutzbach所建议的,我将他的12交换排序网络与无分支快速交换结合在一起(代码在此处)。确实,它的速度更快,是迄今为止最好的,只有很少的保证金(大约5%),而使用较少的掉期可以预期。

还有趣的是,无分支交换似乎比在PPC体系结构上使用if的简单交换效率低(四倍)。

调用库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。

更新

如上面发布的数字所示,gcc的更高版本仍然可以增强这种效果,并且排名顺序始终是其他任何方法的两倍。

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

Rex Kerr提议与gcc 4.4.3的惊人效率使我感到奇怪:具有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
您对int有一些约束吗?例如,我们可以假设,对于任何2 X,Y x-yx+y不会造成溢或溢出?
Matthieu M.

3
您应该尝试将我的12交换排序网络与Paul的无分支交换功能结合在一起。他的解决方案将所有参数作为单独的元素传递到堆栈中,而不是将单个指针传递给数组。这也可能会有所作为。
丹尼尔·斯图兹巴赫

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 2011年

3
@Tyler:如何在没有分支的汇编级别实现它?
罗伦·佩希特尔

4
@Loren:分别根据是否大于或分别CMP EAX, EBX; SBB EAX, EAX将0或0xFFFFFFFF放入。 是“减去借项”,即(“加上借项”)的对应项;您所指的状态位进位位。再说,我记得和有可怕的延迟和吞吐量的奔腾4与和,并仍然慢一倍的核CPU。从80386开始,还提供了条件存储和条件移动指令,但它们的速度也很慢。EAXEAXEBXSBBADCADCSBBADDSUBSETccCMOVcc
j_random_hacker 2011年

Answers:


162

对于任何优化,始终最好进行测试,测试,测试。我会尝试至少排序网络和插入排序。如果我下注,我会根据过去的经验将钱投入到插入类游戏中。

您是否了解输入数据?某些算法在处理某些类型的数据时会表现更好。例如,插入排序在排序或几乎排序的dat上表现更好,因此,如果排序几乎或几乎排序的数据有高于平均水平的机会,它将是更好的选择。

您发布的算法类似于插入排序,但是看起来您已以更多比较为代价将交换次数最小化。但是,由于分支会导致指令流水线停顿,因此比较要比交换昂贵得多。

这是一个插入排序实现:

static __inline__ int sort6(int *d){
        int i, j;
        for (i = 1; i < 6; i++) {
                int tmp = d[i];
                for (j = i; j >= 1 && tmp < d[j-1]; j--)
                        d[j] = d[j-1];
                d[j] = tmp;
        }
}

这是我建立分拣网络的方式。首先,使用此站点为适当长度的网络生成最少的SWAP宏集。将其包装在函数中可以得到:

static __inline__ int sort6(int * d){
#define SWAP(x,y) if (d[y] < d[x]) { int tmp = d[x]; d[x] = d[y]; d[y] = tmp; }
    SWAP(1, 2);
    SWAP(0, 2);
    SWAP(0, 1);
    SWAP(4, 5);
    SWAP(3, 5);
    SWAP(3, 4);
    SWAP(0, 3);
    SWAP(1, 4);
    SWAP(2, 5);
    SWAP(2, 4);
    SWAP(1, 3);
    SWAP(2, 3);
#undef SWAP
}

9
+1:不错,您是通过12个交换完成此操作的,而不是上面我手工编码并根据经验得出的网络中的13个。如果可以链接到为您生成网络的网站,我会再给您+1-现在已添加书签。
Paul R

9
如果您希望大多数请求是小型数组,那么对于通用排序功能而言,这是一个绝妙的主意。使用此过程,针对要优化的情况使用switch语句;让默认情况下使用库排序功能。
马克·兰瑟姆

5
@Mark一个好的库排序功能已经具有用于小型数组的快速路径。许多现代库都将使用递归的QuickSort或MergeSort,在递归到之后,将切换到InsertionSort n < SMALL_CONSTANT
丹尼尔·斯图兹巴赫

3
@Mark好吧,C库排序函数要求您通过函数搬运器指定比较操作。每次比较调用一个函数的开销是巨大的。通常,这仍然是最干净的方法,因为这很少是程序中的关键路径。但是,如果这是关键路径,但如果我们知道要对整数进行排序的话,我们确实可以更快地排序,并且整数恰好是6。:)
丹尼尔·斯图兹巴赫

7
@tgwh:XOR交换几乎总是一个坏主意。
Paul R

63

这是使用排序网络的实现:

inline void Sort2(int *p0, int *p1)
{
    const int temp = min(*p0, *p1);
    *p1 = max(*p0, *p1);
    *p0 = temp;
}

inline void Sort3(int *p0, int *p1, int *p2)
{
    Sort2(p0, p1);
    Sort2(p1, p2);
    Sort2(p0, p1);
}

inline void Sort4(int *p0, int *p1, int *p2, int *p3)
{
    Sort2(p0, p1);
    Sort2(p2, p3);
    Sort2(p0, p2);  
    Sort2(p1, p3);  
    Sort2(p1, p2);  
}

inline void Sort6(int *p0, int *p1, int *p2, int *p3, int *p4, int *p5)
{
    Sort3(p0, p1, p2);
    Sort3(p3, p4, p5);
    Sort2(p0, p3);  
    Sort2(p2, p5);  
    Sort4(p1, p2, p3, p4);  
}

实际上,您确实需要非常有效的无分支minmax实现,因为这实际上就是该代码的本质-序列minmax操作(每个操作总共13个)。我将此留给读者练习。

请注意,此实现很容易实现矢量化(例如SIMD-大多数SIMD ISA具有向量最小/最大指令),也很容易实现GPU实现(例如CUDA-无分支,翘曲发散等均没有问题)。

另请参阅:快速算法实现,对非常小的列表进行排序


1
对于最小/最大程度的一些骇客:graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax
Rubys 2010年

1
@Paul:在真正的CUDA使用环境中,这无疑是最好的答案。我将检查它在x64高尔夫环境中是否也是(以及多少)并发布结果。
克里斯,2010年

1
Sort3如果您注意到这(a+b+c)-(min+max)是中心数字,则速度会更快(无论如何在大多数架构上)。
雷克斯·克尔

1
@Rex:我明白了-看起来不错。对于像AltiVec和SSE这样的SIMD架构,它的指令周期数是相同的(最大和最小是单周期指令,如加/减),但是对于普通的标量CPU,您的方法看起来更好。
Paul R

2
如果我让GCC使用条件移动指令来优化最小值,那么我将获得33%的加速比:#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }。在这里,我不使用?:来表示d [y],因为它的性能稍差一些,但几乎处在噪音中。
Paolo Bonzini 2011年

45

由于这些是整数并且比较快速,所以为什么不直接计算每个的排名顺序:

inline void sort6(int *d) {
  int e[6];
  memcpy(e,d,6*sizeof(int));
  int o0 = (d[0]>d[1])+(d[0]>d[2])+(d[0]>d[3])+(d[0]>d[4])+(d[0]>d[5]);
  int o1 = (d[1]>=d[0])+(d[1]>d[2])+(d[1]>d[3])+(d[1]>d[4])+(d[1]>d[5]);
  int o2 = (d[2]>=d[0])+(d[2]>=d[1])+(d[2]>d[3])+(d[2]>d[4])+(d[2]>d[5]);
  int o3 = (d[3]>=d[0])+(d[3]>=d[1])+(d[3]>=d[2])+(d[3]>d[4])+(d[3]>d[5]);
  int o4 = (d[4]>=d[0])+(d[4]>=d[1])+(d[4]>=d[2])+(d[4]>=d[3])+(d[4]>d[5]);
  int o5 = 15-(o0+o1+o2+o3+o4);
  d[o0]=e[0]; d[o1]=e[1]; d[o2]=e[2]; d[o3]=e[3]; d[o4]=e[4]; d[o5]=e[5];
}

@Rex:使用gcc -O1时,它低于1000个周期,这比分拣网络快但慢。有任何改进代码的想法吗?也许我们可以避免数组复制...
kriss

@kriss:对我来说,使用-O2的速度比分拣网络快。是否有某些原因导致-O2不能正常运行,或者您在-O2上运行速度也较慢?也许在机器架构上有所不同?
雷克斯·克尔

1
@Rex:对不起,我一眼错过了> vs> =模式。它在任何情况下都有效。
克里斯(Kriss),2010年

3
@kriss:啊哈。这并不完全令人惊讶-周围有很多变量,它们必须仔细排序并缓存在寄存器中,依此类推。
雷克斯·克尔

2
@SSpoke 0+1+2+3+4+5=15因为他们中的一个缺失,15减去剩余产量的总和缺少一个
格伦泰特鲍姆

35

看来我迟到了一年,但是现在我们去...

查看由gcc 4.5.2生成的程序集,我观察到每次交换都在完成加载和存储,这实际上是不需要的。最好将这6个值加载到寄存器中,对它们进行排序,然后将它们存储回内存中。我下令将商店中的负载尽可能地靠近最初需要和最后使用的寄存器。我还使用了Steinar H. Gunderson的SWAP宏。更新:我切换到Paolo Bonzini的SWAP宏,它将gcc转换为与Gunderson相似的东西,但是gcc可以更好地对指令进行排序,因为它们不是作为显式汇编给出的。

尽管可能会有更好的排序,但我使用了与表现最佳的重新排序交换网络相同的交换顺序。如果有更多时间,我将生成并测试一系列排列。

我更改了测试代码以考虑4000多个数组,并显示了对每个数组进行排序所需的平均循环数。在i5-650上,我得到〜34.1个循环/排序(使用-O3),而原始的重新排序排序网络得到了〜65.3个循环/排序(使用-O1,beats -O2和-O3)。

#include <stdio.h>

static inline void sort6_fast(int * d) {
#define SWAP(x,y) { int dx = x, dy = y, tmp; tmp = x = dx < dy ? dx : dy; y ^= dx ^ tmp; }
    register int x0,x1,x2,x3,x4,x5;
    x1 = d[1];
    x2 = d[2];
    SWAP(x1, x2);
    x4 = d[4];
    x5 = d[5];
    SWAP(x4, x5);
    x0 = d[0];
    SWAP(x0, x2);
    x3 = d[3];
    SWAP(x3, x5);
    SWAP(x0, x1);
    SWAP(x3, x4);
    SWAP(x1, x4);
    SWAP(x0, x3);
    d[0] = x0;
    SWAP(x2, x5);
    d[5] = x5;
    SWAP(x1, x3);
    d[1] = x1;
    SWAP(x2, x4);
    d[4] = x4;
    SWAP(x2, x3);
    d[2] = x2;
    d[3] = x3;

#undef SWAP
#undef min
#undef max
}

static __inline__ unsigned long long rdtsc(void)
{
    unsigned long long int x;
    __asm__ volatile ("rdtsc; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
    return x;
}

void ran_fill(int n, int *a) {
    static int seed = 76521;
    while (n--) *a++ = (seed = seed *1812433253 + 12345);
}

#define NTESTS 4096
int main() {
    int i;
    int d[6*NTESTS];
    ran_fill(6*NTESTS, d);

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6*NTESTS ; i+=6) {
        sort6_fast(d+i);
    }
    cycles = rdtsc() - cycles;
    printf("Time is %.2lf\n", (double)cycles/(double)NTESTS);

    for (i = 0; i < 6*NTESTS ; i+=6) {
        if (d[i+0] > d[i+1] || d[i+1] > d[i+2] || d[i+2] > d[i+3] || d[i+3] > d[i+4] || d[i+4] > d[i+5])
            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]);
    }
    return 0;
}

我更改了测试套件的修改,使其还可以报告每种排序的时钟并运行更多测试(cmp函数也已更新为处理整数溢出),这是一些不同体系结构上的结果。我尝试在AMD CPU上进行测试,但rdtsc在可用的X6 1100T上不可靠。

Clarkdale (i5-650)
==================
Direct call to qsort library function      635.14   575.65   581.61   577.76   521.12
Naive implementation (insertion sort)      538.30   135.36   134.89   240.62   101.23
Insertion Sort (Daniel Stutzbach)          424.48   159.85   160.76   152.01   151.92
Insertion Sort Unrolled                    339.16   125.16   125.81   129.93   123.16
Rank Order                                 184.34   106.58   54.74    93.24    94.09
Rank Order with registers                  127.45   104.65   53.79    98.05    97.95
Sorting Networks (Daniel Stutzbach)        269.77   130.56   128.15   126.70   127.30
Sorting Networks (Paul R)                  551.64   103.20   64.57    73.68    73.51
Sorting Networks 12 with Fast Swap         321.74   61.61    63.90    67.92    67.76
Sorting Networks 12 reordered Swap         318.75   60.69    65.90    70.25    70.06
Reordered Sorting Network w/ fast swap     145.91   34.17    32.66    32.22    32.18

Kentsfield (Core 2 Quad)
========================
Direct call to qsort library function      870.01   736.39   723.39   725.48   721.85
Naive implementation (insertion sort)      503.67   174.09   182.13   284.41   191.10
Insertion Sort (Daniel Stutzbach)          345.32   152.84   157.67   151.23   150.96
Insertion Sort Unrolled                    316.20   133.03   129.86   118.96   105.06
Rank Order                                 164.37   138.32   46.29    99.87    99.81
Rank Order with registers                  115.44   116.02   44.04    116.04   116.03
Sorting Networks (Daniel Stutzbach)        230.35   114.31   119.15   110.51   111.45
Sorting Networks (Paul R)                  498.94   77.24    63.98    62.17    65.67
Sorting Networks 12 with Fast Swap         315.98   59.41    58.36    60.29    55.15
Sorting Networks 12 reordered Swap         307.67   55.78    51.48    51.67    50.74
Reordered Sorting Network w/ fast swap     149.68   31.46    30.91    31.54    31.58

Sandy Bridge (i7-2600k)
=======================
Direct call to qsort library function      559.97   451.88   464.84   491.35   458.11
Naive implementation (insertion sort)      341.15   160.26   160.45   154.40   106.54
Insertion Sort (Daniel Stutzbach)          284.17   136.74   132.69   123.85   121.77
Insertion Sort Unrolled                    239.40   110.49   114.81   110.79   117.30
Rank Order                                 114.24   76.42    45.31    36.96    36.73
Rank Order with registers                  105.09   32.31    48.54    32.51    33.29
Sorting Networks (Daniel Stutzbach)        210.56   115.68   116.69   107.05   124.08
Sorting Networks (Paul R)                  364.03   66.02    61.64    45.70    44.19
Sorting Networks 12 with Fast Swap         246.97   41.36    59.03    41.66    38.98
Sorting Networks 12 reordered Swap         235.39   38.84    47.36    38.61    37.29
Reordered Sorting Network w/ fast swap     115.58   27.23    27.75    27.25    26.54

Nehalem (Xeon E5640)
====================
Direct call to qsort library function      911.62   890.88   681.80   876.03   872.89
Naive implementation (insertion sort)      457.69   236.87   127.68   388.74   175.28
Insertion Sort (Daniel Stutzbach)          317.89   279.74   147.78   247.97   245.09
Insertion Sort Unrolled                    259.63   220.60   116.55   221.66   212.93
Rank Order                                 140.62   197.04   52.10    163.66   153.63
Rank Order with registers                  84.83    96.78    50.93    109.96   54.73
Sorting Networks (Daniel Stutzbach)        214.59   220.94   118.68   120.60   116.09
Sorting Networks (Paul R)                  459.17   163.76   56.40    61.83    58.69
Sorting Networks 12 with Fast Swap         284.58   95.01    50.66    53.19    55.47
Sorting Networks 12 reordered Swap         281.20   96.72    44.15    56.38    54.57
Reordered Sorting Network w/ fast swap     128.34   50.87    26.87    27.91    28.02

您对寄存器变量的想法应应用于Rex Kerr的“排名顺序”解决方案。那应该是最快的,也许这样的-O3优化不会适得其反。
cdunn2001

1
@ cdunn2001我刚刚对其进行了测试,但没有看到改进(除了-O0和-Os的几个循环)。看着asm,看来gcc已经设法找出使用寄存器的方法,并且消除了对memcpy的调用。
凯文·

您介意将简单的交换版本添加到您的测试套件中,我想将它与手工优化的装配快速交换进行比较可能很有趣。
kriss

1
您的代码仍然使用Gunderson的交换,而我的则是#define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }
Paolo Bonzini 2011年

@Paolo Bonzini:是的,我打算和您一起添加一个测试用例,只是还没有时间。但是我会避免内联汇编。
kriss

15

几天前,我偶然遇到了Google提出的这个问题,因为我还需要快速对6个整数的固定长度数组进行排序。但是就我而言,我的整数只有8位(而不是32位),并且我没有严格要求仅使用C。我想我还是会分享我的发现,以防万一它们可能对某人有所帮助...

我在程序集中实现了一个网络排序的变体,该变体使用SSE尽可能地矢量化比较和交换操作。需要六次“通过”才能对数组进行完全排序。我使用一种新颖的机制将PCMPGTB(向量化比较)的结果直接转换为PSHUFB(向量化交换)的混洗参数,仅使用PADDB(向量化加法),在某些情况下还使用了PAND(按位与)指令。

这种方法还具有产生真正的无分支功能的副作用。没有任何跳转说明。

看来,此实现比当前被标记为问题中最快的选项(“使用简单交换对网络12进行排序”)快约38%。我修改了该实现以char在测试期间使用数组元素,以使比较公平。

我应该注意,这种方法可以应用于最多16个元素的任何数组大小。我希望更大的阵列相对于替代品的相对速度优势会越来越大。

该代码使用带有SSSE3的x86_64处理器的MASM编写。该函数使用“新” Windows x64调用约定。这里是...

PUBLIC simd_sort_6

.DATA

ALIGN 16

pass1_shuffle   OWORD   0F0E0D0C0B0A09080706040503010200h
pass1_add       OWORD   0F0E0D0C0B0A09080706050503020200h
pass2_shuffle   OWORD   0F0E0D0C0B0A09080706030405000102h
pass2_and       OWORD   00000000000000000000FE00FEFE00FEh
pass2_add       OWORD   0F0E0D0C0B0A09080706050405020102h
pass3_shuffle   OWORD   0F0E0D0C0B0A09080706020304050001h
pass3_and       OWORD   00000000000000000000FDFFFFFDFFFFh
pass3_add       OWORD   0F0E0D0C0B0A09080706050404050101h
pass4_shuffle   OWORD   0F0E0D0C0B0A09080706050100020403h
pass4_and       OWORD   0000000000000000000000FDFD00FDFDh
pass4_add       OWORD   0F0E0D0C0B0A09080706050403020403h
pass5_shuffle   OWORD   0F0E0D0C0B0A09080706050201040300h
pass5_and       OWORD 0000000000000000000000FEFEFEFE00h
pass5_add       OWORD   0F0E0D0C0B0A09080706050403040300h
pass6_shuffle   OWORD   0F0E0D0C0B0A09080706050402030100h
pass6_add       OWORD   0F0E0D0C0B0A09080706050403030100h

.CODE

simd_sort_6 PROC FRAME

    .endprolog

    ; pxor xmm4, xmm4
    ; pinsrd xmm4, dword ptr [rcx], 0
    ; pinsrb xmm4, byte ptr [rcx + 4], 4
    ; pinsrb xmm4, byte ptr [rcx + 5], 5
    ; The benchmarked 38% faster mentioned in the text was with the above slower sequence that tied up the shuffle port longer.  Same on extract
    ; avoiding pins/extrb also means we don't need SSE 4.1, but SSSE3 CPUs without SSE4.1 (e.g. Conroe/Merom) have slow pshufb.
    movd    xmm4, dword ptr [rcx]
    pinsrw  xmm4,  word ptr [rcx + 4], 2  ; word 2 = bytes 4 and 5


    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass1_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass1_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass2_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass2_and]
    paddb xmm5, oword ptr [pass2_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass3_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass3_and]
    paddb xmm5, oword ptr [pass3_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass4_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass4_and]
    paddb xmm5, oword ptr [pass4_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass5_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass5_and]
    paddb xmm5, oword ptr [pass5_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass6_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass6_add]
    pshufb xmm4, xmm5

    ;pextrd dword ptr [rcx], xmm4, 0    ; benchmarked with this
    ;pextrb byte ptr [rcx + 4], xmm4, 4 ; slower version
    ;pextrb byte ptr [rcx + 5], xmm4, 5
    movd   dword ptr [rcx], xmm4
    pextrw  word ptr [rcx + 4], xmm4, 2  ; x86 is little-endian, so this is the right order

    ret

simd_sort_6 ENDP

END

您可以将其编译为可执行对象,并将其链接到C项目中。有关如何在Visual Studio中执行此操作的说明,您可以阅读本文。您可以使用以下C原型从C代码中调用函数:

void simd_sort_6(char *values);

将您的建议与其他组装级别的建议进行比较将是一件令人费解的事情。比较的实施性能不包括它们。无论如何,使用SSE听起来不错。
kriss

未来研究的另一个领域是将新的Intel AVX指令应用于此问题。较大的256位向量足以容纳8个DWORD。
乔·克里维洛

1
而是pxor / pinsrd xmm4, mem, 0使用movd
彼得·科德斯

14

测试代码非常糟糕;它溢出了初始数组(这里的人不阅读编译器警告吗?),printf打印出错误的元素,出于没有充分的理由,它为rdtsc使用.byte,只有一个运行(!),没有任何检查最终结果实际上是正确的(因此很容易将其“优化”为微妙的错误),所包含的测试非常基础(没有负数?),没有什么可以阻止编译器将整个函数丢弃为无效代码。

话虽如此,在双音网络解决方案上也很容易改进。只需将min / max / SWAP内容更改为

#define SWAP(x,y) { int tmp; asm("mov %0, %2 ; cmp %1, %0 ; cmovg %1, %0 ; cmovg %2, %1" : "=r" (d[x]), "=r" (d[y]), "=r" (tmp) : "0" (d[x]), "1" (d[y]) : "cc"); }

对我来说,速度要快65%(使用-O2,amd64,Core i7的Debian gcc 4.4.5)。


好的,测试代码很差。随时进行改进。是的,您可以使用汇编代码。为什么不一直使用x86汇编器完全编码呢?它可能不太便携,但为什么要打扰呢?
kriss

感谢您注意到数组溢出,我对其进行了纠正。其他人可能没有注意到它,因为单击链接以复制/粘贴代码,没有溢出。
克里斯(Kriss)

4
实际上,您甚至不需要汇编程序。如果您只是丢下所有聪明的把戏,GCC会识别出序列并为您插入条件移动:#define min(a,b)((a <b)?a:b)#define max(a,b)( (a <b)?b:a)#定义SWAP(x,y){int a = min(d [x],d [y]); int b = max(d [x],d [y]); d [x] = a; d [y] = b; }出来的速度可能比内联asm变体慢几个百分点,但是鉴于缺乏适当的基准测试,这很难说。
Steinar H. Gunderson

3
…最后,如果您的数字是浮点数,并且您不必担心NaN等,GCC可以将其转换为minss / maxss SSE指令,但速度提高了约25%。士气:放下巧妙的摆弄技巧,让编译器完成工作。:-)
Steinar H. Gunderson

13

虽然我真的很喜欢提供的swap宏:

#define min(x, y) (y ^ ((x ^ y) & -(x < y)))
#define max(x, y) (x ^ ((x ^ y) & -(x < y)))
#define SWAP(x,y) { int tmp = min(d[x], d[y]); d[y] = max(d[x], d[y]); d[x] = tmp; }

我看到了一个改进(好的编译器可能会做出的改进):

#define SWAP(x,y) { int tmp = ((x ^ y) & -(y < x)); y ^= tmp; x ^= tmp; }

我们记下最小和最大的工作方式,并显式提取公共子表达式。这样可以完全消除最小和最大宏。


这使它们倒退,请注意d [y]获得最大值,即x ^(公共子表达式)。
凯文·

我注意到同一件事;我认为您想要的实现是正确的,d[x]而不是x(与相同y),并且d[y] < d[x]这里的不等式(是的,不同于最小/最大代码)。
泰勒(Tyler)

我尝试了您的交换,但是局部优化在更大的程度上具有负面影响(我猜它会引入依赖关系)。结果比其他交换慢。但是,正如您所看到的,通过提出的新解决方案,确实有很多性能可以实现优化交换。
克里斯(Kriss)

12

在没有基准测试并查看实际编译器生成的程序集的情况下,切勿优化最小/最大。如果我让GCC使用条件移动指令来优化最小值,那么我将获得33%的加速:

#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }

(测试代码中的280个周期与420个周期)。使用?:进行max几乎相同,几乎消失在噪音中,但是上述操作要快一些。使用GCC和Clang时,此SWAP更快。

编译器在寄存器分配和别名分析方面也做得非常出色,可以有效地将d [x]提前移入局部变量,最后只复制回内存。实际上,它们的效果甚至比完全使用局部变量(例如d0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5])更好。我之所以这样写,是因为您假设要进行强大的优化,但仍试图以最小/最大的价格超过编译器。:)

顺便说一句,我尝试了Clang和GCC。它们执行相同的优化,但是由于排程的差异,两者的结果会有一些差异,因此不能说到底是快还是慢。GCC在排序网络上更快,Clang在二次排序上。

仅出于完整性考虑,展开气泡排序和插入排序也是可能的。这是气泡排序:

SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4); SWAP(4,5);
SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4);
SWAP(0,1); SWAP(1,2); SWAP(2,3);
SWAP(0,1); SWAP(1,2);
SWAP(0,1);

这是插入排序:

//#define ITER(x) { if (t < d[x]) { d[x+1] = d[x]; d[x] = t; } }
//Faster on x86, probably slower on ARM or similar:
#define ITER(x) { d[x+1] ^= t < d[x] ? d[x] ^ d[x+1] : 0; d[x] = t < d[x] ? t : d[x]; }
static inline void sort6_insertion_sort_unrolled_v2(int * d){
    int t;
    t = d[1]; ITER(0);
    t = d[2]; ITER(1); ITER(0);
    t = d[3]; ITER(2); ITER(1); ITER(0);
    t = d[4]; ITER(3); ITER(2); ITER(1); ITER(0);
    t = d[5]; ITER(4); ITER(3); ITER(2); ITER(1); ITER(0);

这种插入排序比Daniel Stutzbach的插入排序要快,并且特别适合在具有谓词的GPU或计算机上使用,因为ITER只能用3条指令来完成(对于SWAP,则是4条)。例如,这是t = d[2]; ITER(1); ITER(0);ARM汇编中的行:

    MOV    r6, r2
    CMP    r6, r1
    MOVLT  r2, r1
    MOVLT  r1, r6
    CMP    r6, r0
    MOVLT  r1, r0
    MOVLT  r0, r6

对于六个元素,插入排序与排序网络具有竞争性(12个交换vs.15个迭代平衡4条指令/交换与3条指令/迭代);泡沫当然较慢。但是随着大小的增加,情况并非如此,因为插入排序为O(n ^ 2),而排序网络为O(n log n)。


1
或多或少相关:我向GCC 提交了一份报告,以便它可以直接在编译器中实现优化。不知道它会完成,但是至少您可以了解它的发展过程。
Morwenn 2015年

11

我将测试套件移植到了我无法识别的PPC架构机器上(不必触摸代码,只需增加测试的迭代次数,使用8个测试用例来避免用mod污染结果并替换x86特定的rdtsc):

直接调用qsort库函数:101

天真的实现(插入排序):299

插入排序(Daniel Stutzbach) :108

展开插入排序 :51

分类网络(Daniel Stutzbach) :26

分类网络(Paul R) :85

快速交换的排序网络12 :117

分类网络12重新排序了互换 :116

排名 :56


1
十分有趣。看起来无分支交换在PPC上是个坏主意。也可能是与编译器有关的效果。使用了哪一个?
kriss

它是gcc编译器的一个分支-最小,最大逻辑可能不是无分支的-我将检查反汇编并告知您,但除非编译器足够聪明,包括x <y之类的东西,否则if仍然不会成为分支-在x86上/ x64 CMOV指令可能会避免这种情况,但是PPC上没有针对固定点值的指令,只有浮点型。我明天可能会涉猎此事,并让您知道-我记得Winamp AVS源代码中的无分支最小/最大值要简单得多,但是iirc仅用于浮点数-但这可能是迈向真正无分支方法的良好起点。
jheriko 2011年

4
这是具有无符号输入的PPC的无分支最小/最大值:subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3。r3 / r4是输入,r5 / r6是暂存寄存器,在输出r3上获得最小值,而r4上获得最大值。它应该手工安排得体。我使用GNU超级优化器找到了它,它从4条指令的最小和最大序列开始,然后手动寻找可以组合的两个。对于带符号的输入,您当然可以在所有元素的开头添加0x80000000,然后在末尾再减去它,然后就像未签名一样工作。
Paolo Bonzini 2011年

7

XOR交换在交换功能中可能很有用。

void xorSwap (int *x, int *y) {
     if (*x != *y) {
         *x ^= *y;
         *y ^= *x;
         *x ^= *y;
     }
 }

if可能会导致代码之间的差异太大,但是如果可以保证所有int都是唯一的,则可能会很方便。


1
xor交换也适用于相等的值... x ^ = y将x设置为0,y ^ = x将y
保留

11
当它不起作用x,将y指向同一位置。
hobbs

无论如何,当与排序网络一起使用时,我们永远不会调用x和y都指向同一位置。仍然有一种方法可以避免测试更大,以获得与无分支交换相同的效果。我有一个实现的想法。
kriss

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间隙shellsort。

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);
}

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

我没有要编译的64位环境。


很好 我将其添加到更长的测试套件中
kriss 2012年

3

如果插入排序在这里具有相当的竞争力,我建议您尝试使用shellsort。恐怕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,V(1979)。Shellsort和排序网络(计算机科学领域的杰出论文)。花环。书号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在整数向量上使用(使用强制转换以使内部函数满意),而无需将bitcan(ffs)结果右移。
彼得·科德斯

1
您可以根据cmpgt结果有条件地减去 1 ,而不是使用对其进行屏蔽set1(1)。如index = _mm256_sub_epi32(index, gt)确实index -= -1 or 0;
彼得·科德斯

1
eq = _mm256_insert_epi32(eq, 0, I)如果元素按编写顺序进行编译,则不是将元素归零的有效方法(尤其是对于低4以外的元素,因为vpinsrd仅适用于XMM目标;必须模拟大于3的索引)。相反,_mm256_blend_epi32vpblendd)具有零向量。 vpblendd是在任何端口上运行的单指令,而在Intel CPU上需要5端口的洗牌。(agner.org/optimize)。
彼得·科德斯

1
另外,您可能会考虑rot从同一来源生成具有不同洗牌的向量,或者至少并行运行交替使用的2条dep链,而不是通过车道穿越洗牌(3个循环等待时间)的单个dep链。这将在单一种类中增加ILP。2个dep链将向量常数的数量限制为一个合理的数量,即每旋转一圈只需2:1,并且将两个旋转步长合为一个。
彼得·科德斯

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个分配来对三个值进行排序。

排序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个值

此版本使用了我称为双重插入排序的展开版本。这个名字虽然不怎么好,但是描述性很强,这是它的工作原理:

  • 对除数组的第一个和最后一个元素以外的所有内容进行排序。
  • 如果第一个大于最后一个,则交换数组的第一个和元素。
  • 从前面将第一个元素插入已排序的序列,然后从后面将最后一个元素插入。

交换之后,第一个元素总是小于最后一个元素,这意味着,在将它们插入已排序的序列中时,在最坏的情况下插入两个元素的比较不会超过N个。第一个元素已插入到第3个位置,然后最后一个元素不能插入到低于第4个位置。

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年的历史了,它似乎没有太大的意义。
克里斯,2015年

您应该看看其他答案的计时方式。关键是,使用如此小的数据集计数比较甚至比较和交换并不能真正说明算法的速度(基本上对6个整数进行排序始终为O(1),因为O(6 * 6)为O(1))。当前最快的先前提出的解决方案是使用大比较(由RexKerr)立即找到每个值的位置。
克里斯(Kriss)

@kriss现在最快吗?根据我对结果的了解,排序网络方法是最快的,这很糟糕。的确,我的解决方案来自我的通用库,而且我并不总是比较整数,也不总是operator<用于比较。除了比较和交换的客观计数外,我还对算法进行了适当的计时。该解决方案是最快的通用解决方案,但我确实错过了@RexKerr的解决方案。要尝试:)
Morwenn 2015年

自从gcc编译器4.2.3以来,RexKerr的解决方案(订单等级)成为X86架构上最快的解决方案(而从gcc 4.9开始,它的速度几乎是第二好的编译器的两倍)。但这在很大程度上取决于编译器的优化,在其他架构上可能并非如此。
克里斯,2015年

@kriss这很有趣。而且我的确可以与再次达成更多的分歧-O3。我想我那时将为排序库采用另一种策略:提供三种算法,以使比较次数少,交换次数少或性能可能最佳。至少,发生的事情对读者是透明的。感谢您的见解:)
Morwenn 2015年

1

我相信您的问题分为两部分。

  • 首先是确定最佳算法。至少在这种情况下,这是通过遍历所有可能的顺序(没有那么多)来完成的,该顺序使您可以计算比较和交换的精确最小,最大,平均和标准偏差。并获得一两个亚军。
  • 二是优化算法。将教科书代码示例转换为有意义的精益现实算法可以做很多事情。如果您发现无法将算法优化到所需的程度,请尝试获得亚军。

我不必担心清空管道(假设当前使用x86):分支预测已经走了很长一段路。我担心的是确保代码和数据分别适合一个缓存行(对于代码来说可能两个)。到那里后,取回延迟会刷新得很低,这将弥补任何延迟。这也意味着您的内部循环可能大约是十条指令(应该是正确的位置)(我的排序算法中有两个不同的内部循环,它们分别是10条指令/ 22字节和9/22长)。假设代码不包含任何div,则可以确定它会令人眼花fast乱。


我不确定如何理解您的答案。首先,我根本不了解您提出什么算法?以及如果必须遍历720种可能的顺序(现有答案所花费的时间少于720个周期),如何将其最佳化。如果您有随机输入,则我无法想象(即使在理论水平上)分支预测的性能如何比50-50更好,除非它不在乎所有输入数据。同样,已经提出的大多数好的解决方案可能已经可以完全在缓存中使用数据和代码。但是也许我完全误解了你的答案。介意显示一些代码?
克里斯(Kriss)2012年

我的意思是,只有720个(6!)6个整数的不同组合,并且通过候选算法运行所有整数,您可以确定很多事情,这就是我所提到的-这就是理论部分。实际的部分是对该算法进行微调,使其在尽可能少的时钟周期内运行。我排序6个整数的起点是1、4间隔的shellsort。4间隙为1间隙中的良好分支预测铺平了道路。
Olof Forshell

1、4间隔shellsort可容纳6个!唯一组合(从012345开始,以543210结尾)的最佳情况是7个比较和0个交换,最差的14个比较和10个交换。平均情况是大约11.14比较和6交换。
Olof Forshell 2012年

1
我没有“常规随机分布”-我正在做的是测试每种可能的组合并确定最小/平均/最大统计量。Shellsort是一系列递减的增量排序,因此最终增量-1-的工作量要比纯插入排序中单独执行时少得多。至于时钟计数,我的算法平均需要406个时钟周期,这包括收集统计信息并对实际的排序例程进行两次调用-每个间隔一次。这是在Athlon M300移动编译器OpenWatcom上。
Olof Forshell

1
“规则随机分布”是指排序的实际数据的每种组合都可能具有不相等的概率。如果每种组合的概率都不相等,则由于平均需要考虑给定分布可能发生多少次,因此统计数据将被破坏。对于时钟计数,如果您尝试使用这种类型的任何其他实现(上面提供的链接)并在您的测试系统上运行,我们将有一个比较的基础,并查看您选择的性能如何。
克里斯,2012年

1

我知道这是一个老问题。

但是我只是写了一种我想分享的解决方案。
只使用嵌套的MIN MAX,

它不是很快,因为它每个使用114个,
就像这样可以将其减少到75个-> pastebin

但这不再是纯粹的最小最大。

可能的工作是使用AVX一次对多个整数进行最小/最大运算

PMINSW参考

#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实现中为32位)。您是否仅使用在其高半部分中只有0的小的正整数进行了测试?那将起作用...因为int您需要SSE4.1 pmin/maxsd(d = dword)。 felixcloutier.com/x86/pminsd:pminsqpminusduint32_t
彼得·科德斯

1

我发现至少在我的系统上,下面的功能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 ++并不会发生在内存G ++一定的保证更好地优化排序代码(例如,使用指针,编译器不能确保所有指针都指向不同的内存位置)。如果我没有记错,这也是一部分的原因为什么有这么多的STL算法,如std::sort(),一般有这样的猥亵不错的表现。

此外,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; }

如果您只希望基于原始数据类型的排序算法,无论在什么上下文中调用1出现gcc -O3始终擅长优化,那么根据传递输入的方式,尝试以下两种方法之一算法:

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关键字的原因是因为这是您知道要在寄存器中使用这些值的少数情况之一。如果不使用register,编译器将在大多数情况下解决此问题,但有时并非如此。使用register关键字有助于解决此问题。但是,通常不要使用register关键字,因为它可能比加速代码更慢代码的速度。

另外,请注意模板的使用。这样做是有目的的,因为即使使用inline关键字,模板功能通常也比普通C函数更积极地由gcc优化(这与需要处理普通C函数的函数指针而不是模板函数的gcc有关)。

  1. 在安排各种排序功能的时间时,我注意到在其中调用排序功能的上下文(即,周围的代码)对性能有重大影响,这很可能是由于该函数被内联然后进行了优化。例如,如果程序足够简单,那么在传递排序函数的指针与传递迭代器的指针之间,性能通常不会有太大区别。否则,使用迭代器通常会带来明显更好的性能,并且(至少就我目前为止的经验而言)绝不会出现任何明显的性能下降。我怀疑这可能是因为g ++可以全局优化足够简单的代码。

0

尝试“合并排序列表”排序。:)使用两个数组。大小阵列最快。
如果隐瞒,则仅检查插入位置。您不需要比较其他较大的值(cmp = ab> 0)。
对于4个数字,您可以使用系统4-5 cmp(〜4.6)或3-6 cmp(〜4.9)。气泡排序使用6 cmp(6)。大量的cmp会使代码变慢。
此代码使用5 cmp(不是MSL排序):
if (cmp(arr[n][i+0],arr[n][i+1])>0) {swap(n,i+0,i+1);} if (cmp(arr[n][i+2],arr[n][i+3])>0) {swap(n,i+2,i+3);} if (cmp(arr[n][i+0],arr[n][i+2])>0) {swap(n,i+0,i+2);} if (cmp(arr[n][i+1],arr[n][i+3])>0) {swap(n,i+1,i+3);} if (cmp(arr[n][i+1],arr[n][i+2])>0) {swap(n,i+1,i+2);}

主MSL 9 8 7 6 5 4 3 2 1 0 89 67 45 23 01 ... concat two sorted lists, list length = 1 6789 2345 01 ... concat two sorted lists, list length = 2 23456789 01 ... concat two sorted lists, list length = 4 0123456789 ... concat two sorted lists, list length = 8

js代码

function sortListMerge_2a(cmp)	
{
var step, stepmax, tmp, a,b,c, i,j,k, m,n, cycles;
var start = 0;
var end   = arr_count;
//var str = '';
cycles = 0;
if (end>3)
	{
	stepmax = ((end - start + 1) >> 1) << 1;
	m = 1;
	n = 2;
	for (step=1;step<stepmax;step<<=1)	//bounds 1-1, 2-2, 4-4, 8-8...
		{
		a = start;
		while (a<end)
			{
			b = a + step;
			c = a + step + step;
			b = b<end ? b : end;
			c = c<end ? c : end;
			i = a;
			j = b;
			k = i;
			while (i<b && j<c)
				{
				if (cmp(arr[m][i],arr[m][j])>0)
					{arr[n][k] = arr[m][j]; j++; k++;}
				else	{arr[n][k] = arr[m][i]; i++; k++;}
				}
			while (i<b)
				{arr[n][k] = arr[m][i]; i++; k++;
}
			while (j<c)
				{arr[n][k] = arr[m][j]; j++; k++;
}
			a = c;
			}
		tmp = m; m = n; n = tmp;
		}
	return m;
	}
else
	{
	// sort 3 items
	sort10(cmp);
	return m;
	}
}


0

使用用法cmp == 0排序4个项目。cmp的数量为〜4.34(FF本机为〜4.52),但花费的时间是合并列表的3倍。但是,如果您有大量数字或大文本,则最好不要执行cmp操作。编辑:修复错误

在线测试http://mlich.zam.slu.cz/js-sort/x-sort-x2.htm

function sort4DG(cmp,start,end,n) // sort 4
{
var n     = typeof(n)    !=='undefined' ? n   : 1;
var cmp   = typeof(cmp)  !=='undefined' ? cmp   : sortCompare2;
var start = typeof(start)!=='undefined' ? start : 0;
var end   = typeof(end)  !=='undefined' ? end   : arr[n].length;
var count = end - start;
var pos = -1;
var i = start;
var cc = [];
// stabilni?
cc[01] = cmp(arr[n][i+0],arr[n][i+1]);
cc[23] = cmp(arr[n][i+2],arr[n][i+3]);
if (cc[01]>0) {swap(n,i+0,i+1);}
if (cc[23]>0) {swap(n,i+2,i+3);}
cc[12] = cmp(arr[n][i+1],arr[n][i+2]);
if (!(cc[12]>0)) {return n;}
cc[02] = cc[01]==0 ? cc[12] : cmp(arr[n][i+0],arr[n][i+2]);
if (cc[02]>0)
    {
    swap(n,i+1,i+2); swap(n,i+0,i+1); // bubble last to top
    cc[13] = cc[23]==0 ? cc[12] : cmp(arr[n][i+1],arr[n][i+3]);
    if (cc[13]>0)
        {
        swap(n,i+2,i+3); swap(n,i+1,i+2); // bubble
        return n;
        }
    else    {
    cc[23] = cc[23]==0 ? cc[12] : (cc[01]==0 ? cc[30] : cmp(arr[n][i+2],arr[n][i+3]));  // new cc23 | c03 //repaired
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    }
else    {
    if (cc[12]>0)
        {
        swap(n,i+1,i+2);
        cc[23] = cc[23]==0 ? cc[12] : cmp(arr[n][i+2],arr[n][i+3]); // new cc23
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    else    {
        return n;
        }
    }
return n;
}

1
用例与问题的初始上下文略有不同。对于固定长度的排序,细节很重要,仅计算交换的cmp是不够的。我什至都不惊讶,因为它根本不是真的会浪费时间的排序,而是在init中调用typeof()完全不同的方法。我不知道如何使用Javascript执行实际的时钟时间测量。也许与节点?
kriss

0

也许我迟到了,但至少我的贡献是一种的方法。

  • 该代码确实应该内联
  • 即使内联,分支也太多
  • 分析部分基本上是O(N(N-1)),对于N = 6似乎可以
  • 如果的成本swap会更高(与的成本成正比compare),则代码可能会更有效
  • 我相信内联的静态函数。
  • 该方法与等级排序有关
    • 代替等级,使用相对等级(偏移)。
    • 任何置换组中每个循环的秩总和为零。
    • SWAP()跟踪两个周期,而不是两个元素,只需要一个临时变量,一个交换(寄存器->寄存器)(新的<-旧的)。

更新:更改了一下代码,有些人使用C ++编译器来编译C代码...

#include <stdio.h>

#if WANT_CHAR
typedef signed char Dif;
#else
typedef signed int Dif;
#endif

static int walksort (int *arr, int cnt);
static void countdifs (int *arr, Dif *dif, int cnt);
static void calcranks(int *arr, Dif *dif);

int wsort6(int *arr);

void do_print_a(char *msg, int *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", *arr);
        }
fprintf(stderr,"\n");
}

void do_print_d(char *msg, Dif *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", (int) *arr);
        }
fprintf(stderr,"\n");
}

static void inline countdifs (int *arr, Dif *dif, int cnt)
{
int top, bot;

for (top = 0; top < cnt; top++ ) {
        for (bot = 0; bot < top; bot++ ) {
                if (arr[top] < arr[bot]) { dif[top]--; dif[bot]++; }
                }
        }
return ;
}
        /* Copied from RexKerr ... */
static void inline calcranks(int *arr, Dif *dif){

dif[0] =     (arr[0]>arr[1])+(arr[0]>arr[2])+(arr[0]>arr[3])+(arr[0]>arr[4])+(arr[0]>arr[5]);
dif[1] = -1+ (arr[1]>=arr[0])+(arr[1]>arr[2])+(arr[1]>arr[3])+(arr[1]>arr[4])+(arr[1]>arr[5]);
dif[2] = -2+ (arr[2]>=arr[0])+(arr[2]>=arr[1])+(arr[2]>arr[3])+(arr[2]>arr[4])+(arr[2]>arr[5]);
dif[3] = -3+ (arr[3]>=arr[0])+(arr[3]>=arr[1])+(arr[3]>=arr[2])+(arr[3]>arr[4])+(arr[3]>arr[5]);
dif[4] = -4+ (arr[4]>=arr[0])+(arr[4]>=arr[1])+(arr[4]>=arr[2])+(arr[4]>=arr[3])+(arr[4]>arr[5]);
dif[5] = -(dif[0]+dif[1]+dif[2]+dif[3]+dif[4]);
}

static int walksort (int *arr, int cnt)
{
int idx, src,dst, nswap;

Dif difs[cnt];

#if WANT_REXK
calcranks(arr, difs);
#else
for (idx=0; idx < cnt; idx++) difs[idx] =0;
countdifs(arr, difs, cnt);
#endif
calcranks(arr, difs);

#define DUMP_IT 0
#if DUMP_IT
do_print_d("ISteps ", difs, cnt);
#endif

nswap = 0;
for (idx=0; idx < cnt; idx++) {
        int newval;
        int step,cyc;
        if ( !difs[idx] ) continue;
        newval = arr[idx];
        cyc = 0;
        src = idx;
        do      {
                int oldval;
                step = difs[src];
                difs[src] =0;
                dst = src + step;
                cyc += step ;
                if(dst == idx+1)idx=dst;
                oldval = arr[dst];
#if (DUMP_IT&1)
                fprintf(stderr, "[Nswap=%d] Cyc=%d Step=%2d Idx=%d  Old=%2d New=%2d #### Src=%d Dst=%d[%2d]->%2d <-- %d\n##\n"
                        , nswap, cyc, step, idx, oldval, newval
                        , src, dst, difs[dst], arr[dst]
                        , newval  );
                do_print_a("Array ", arr, cnt);
                do_print_d("Steps ", difs, cnt);
#endif

                arr[dst] = newval;
                newval = oldval;
                nswap++;
                src = dst;
                } while( cyc);
        }

return nswap;
}
/*************/
int wsort6(int *arr)
{
return walksort(arr, 6);
}

看起来像泡沫排序。对于最慢的实现来说,这可能是一个很好的竞争者,但是,知道在代码上进行的工作是否有很大的不同,仍然可能会引起一些兴趣。请以与其他代码相同的格式放置您的代码,这样我们就可以在其上运行基准测试。
克里斯,

@kriss en.wikipedia.org/wiki/Permutation_group当然不是冒泡排序:代码检测给定排列中的循环,并遍历这些循环,将每个元素放置在最终位置。最终wsort6()功能具有正确的接口。
2012年

@joop:我的糟糕,确实没有泡沫。话虽这么说,但我仍然希望代码比当前任何其他实现都要差得多。顺便说一下,对于掉期数量,排名顺序解决方案是最佳的,因为它可以直接找到每个项目的最终位置。还不清楚当我们删除所有排序数字都不同的假设时,walksort是否还可以工作。为了对代码进行基准测试,我们应该跟踪代码。同样,正如我通常在C ++编译器上进行编译一样,由于OP将变量称为“新”变量,因此代码将无法正常工作(多数民众赞成在语法突出显示方面就没有了)。
kriss

该方法非常接近等级顺序,只有最终分配就位。除了等级之外o1..o5,不需要第二个临时e[6]数组。并且:在C ++编译器上编译C代码,并归咎于代码?
乔普

@greybeard:谢谢,我在之前添加了一个空格#include。已修正
wildplasser '18

0
//Bruteforce compute unrolled count dumbsort(min to 0-index)
void bcudc_sort6(int* a)
{
    int t[6] = {0};
    int r1,r2;

    r1=0;
    r1 += (a[0] > a[1]);
    r1 += (a[0] > a[2]);
    r1 += (a[0] > a[3]);
    r1 += (a[0] > a[4]);
    r1 += (a[0] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[0];

    r2=0;
    r2 += (a[1] > a[0]);
    r2 += (a[1] > a[2]);
    r2 += (a[1] > a[3]);
    r2 += (a[1] > a[4]);
    r2 += (a[1] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[1];

    r1=0;
    r1 += (a[2] > a[0]);
    r1 += (a[2] > a[1]);
    r1 += (a[2] > a[3]);
    r1 += (a[2] > a[4]);
    r1 += (a[2] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[2];

    r2=0;
    r2 += (a[3] > a[0]);
    r2 += (a[3] > a[1]);
    r2 += (a[3] > a[2]);
    r2 += (a[3] > a[4]);
    r2 += (a[3] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[3];

    r1=0;
    r1 += (a[4] > a[0]);
    r1 += (a[4] > a[1]);
    r1 += (a[4] > a[2]);
    r1 += (a[4] > a[3]);
    r1 += (a[4] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[4];

    r2=0;
    r2 += (a[5] > a[0]);
    r2 += (a[5] > a[1]);
    r2 += (a[5] > a[2]);
    r2 += (a[5] > a[3]);
    r2 += (a[5] > a[4]);
    while(t[r2]){r2++;} 
    t[r2] = a[5];

    a[0]=t[0];
    a[1]=t[1];
    a[2]=t[2];
    a[3]=t[3];
    a[4]=t[4];
    a[5]=t[5];
}

static __inline__ void sort6(int* a)
{
    #define wire(x,y); t = a[x] ^ a[y] ^ ( (a[x] ^ a[y]) & -(a[x] < a[y]) ); a[x] = a[x] ^ t; a[y] = a[y] ^ t;
    register int t;

    wire( 0, 1); wire( 2, 3); wire( 4, 5);
    wire( 3, 5); wire( 0, 2); wire( 1, 4);
    wire( 4, 5); wire( 2, 3); wire( 0, 1); 
    wire( 3, 4); wire( 1, 2); 
    wire( 2, 3);

    #undef wire
}

无论速度如何,您确定它都能正常工作吗?用蛮力排序,您的循环是不确定的。在我看来,如果我们的排序值为零,它们将无法工作。
克里斯,

1
t [6]数组初始化为0x0。因此,无论在何处以及是否写入0x0值的密钥都没有关系。
FranG

-1

好吧,如果只有6个元素并且您可以利用并行性,希望将条件分支降至最低等,那么为什么不生成所有组合并测试顺序呢?我敢冒险说,在某些体系结构中,它可以非常快(只要您预先分配了内存)


9
订单有720个,而快速版本的订购时间少于100个周期。即使可以利用大规模并行处理,在这么小的时间尺度上,创建和同步线程的成本也可能超过仅在一个内核上对阵列进行排序的成本。
凯文·

-3

这是代表三种不同类别的排序算法的三种典型排序方法:

Insertion Sort: Θ(n^2)

Heap Sort: Θ(n log n)

Count Sort: Θ(3n)

但是,请查看Stefan Nelsson关于最快排序算法的讨论吗?在那儿,他讨论了一个解决方案,该方案可用于O(n log log n)..检查其在C中的实现

1995年,一篇论文提出了这种半线性排序算法:

A. Andersson,T。Hagerup,S。Nilsson和R. Raman。按线性时间排序?在第27届ACM关于计算理论的年度研讨会论文集中,第427-436页,1995年。


8
这很有趣,但是很重要。大θ旨在隐藏常数因子,并在问题大小(n)变大时显示趋势。这里的问题完全是关于一个固定的问题大小(n = 6),并考虑了恒定因素。
kriss

@kriss您说得对,我的比较是渐近的,因此实际比较将显示该情况下是否更快
Khaled.K 2013年

4
您不能得出结论,因为每种不同的算法都隐藏着不同的K乘法常数(以及C加法常数)。即:k0,c0用于插入排序,k1,c1用于堆排序,依此类推。所有这些常数实际上是不同的(您可以用物理术语说,每种算法都有其自己的“摩擦系数”),您不能得出结论,在这种情况下(或任何固定的n情况下)算法实际上更快。
kriss 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.