关于算法:最快的固定长度6 int数组排序

Fastest sort of fixed length 6 int array

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

由于问题非常低:

  • 我们不能假设库是可用的(调用本身也有它的成本),只有普通的C语言
  • 为了避免清空指令管道(成本很高),我们应该尽量减少分支、跳跃和其他所有类型的控制流中断(如&&||中隐藏在序列点后面的那些)。
  • 房间是受限的,最小化寄存器和内存使用是一个问题,理想的就地排序可能是最好的。

实际上,这个问题是一种高尔夫,目标不是最小化源长度,而是执行时间。我称之为"Zening"代码,正如MichaelAbrash在《代码优化的禅宗》一书及其续集中所使用的。

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

  • 这个例子很简单,易于理解和衡量,不需要太多的C技能。
  • 它显示了选择一个好算法的效果,同时也显示了编译器和底层硬件的效果。

这是我的参考(幼稚的,未优化的)实现和测试集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#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}
    };

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

原始结果

随着变体数量的增加,我将它们全部收集到一个测试套件中,可以在这里找到。由于KevinStock的帮助,实际使用的测试比上面显示的要简单一些。您可以在自己的环境中编译和执行它。我对不同的目标体系结构/编译器的行为非常感兴趣。(好的,伙计们,把它放在答案里,我会+1每个新结果集的贡献者)。

一年前,我给了丹尼尔·斯图茨巴赫答案(打高尔夫球),因为他是当时最快解决方案的来源(分类网络)。

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
  • 重新排序排序网络w/fast swap 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
  • 重新排序排序网络w/fast swap v2:33.07
  • 内联气泡排序(paolo bonzini):45.79
  • 展开插入排序(paolo bonzini):80.15

我把-o1和-o2的结果都包括在内,因为令人惊讶的是,对于一些程序来说,o2的效率比o1低。我想知道什么特定的优化有这种效果?

对建议解决方案的意见

插入排序(Daniel Stutzbach)

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

分类网络(Daniel Stutzbach)

优于插入排序。我想知道主要的影响是否来自于避免外部循环。我尝试了展开插入排序来检查,实际上我们得到了大致相同的数字(代码在这里)。

分类网络(Paul R)

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

排序网络12交换与快速交换

正如DanielStutzbach所建议的,我将他的12交换排序网络与无分支快速交换(代码在这里)结合起来。它是无限的


对于任何优化,最好是测试、测试、测试。我会尝试至少排序网络和插入排序。如果我下注,我会根据过去的经验把我的钱放在插入排序上。

你知道输入数据吗?有些算法对某些数据的处理效果会更好。例如,插入排序在排序或几乎排序的数据上表现得更好,因此如果几乎排序的数据的概率高于平均值,则是更好的选择。

您发布的算法类似于插入排序,但看起来您已经最小化了交换的数量,而代价是进行更多的比较。不过,比较要比交换昂贵得多,因为分支会导致指令管道停滞。

下面是插入排序实现:

1
2
3
4
5
6
7
8
9
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;
        }
}

下面是我如何建立一个分类网络。首先,使用此站点为适当长度的网络生成一组最小的交换宏。在函数中对其进行包装可以得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
}


下面是一个使用排序网络的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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都有vector min/max指令)以及gpu实现(例如,cuda-无分支,没有翘曲发散等问题)。

另见:快速算法实现对非常小的列表进行排序


由于这些都是整数,比较也很快,为什么不直接计算每个的排名顺序:

1
2
3
4
5
6
7
8
9
10
11
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];
}


看来我晚了一年才去参加聚会,但我们走了……

在查看GCC4.5.2生成的程序集时,我注意到每个交换都在进行加载和存储,这确实是不需要的。最好将6个值加载到寄存器中,对它们进行排序,然后将它们存储回内存中。我命令商店里的货物尽可能靠近那里。首先需要寄存器,最后使用寄存器。我还使用了SteinarH.Gunderson的交换宏。更新:我切换到paolo bonzini的swap宏,gcc将其转换为类似于gunderson的宏,但是gcc能够更好地对指令进行排序,因为它们不是作为显式汇编提供的。

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

我修改了测试代码以考虑超过4000个数组,并显示了对每个数组进行排序所需的平均周期数。在i5-650上,我得到了约34.1个周期/排序(使用-o3),相比之下,原始重新排序的排序网络得到了约65.3个周期/排序(使用-o1、beats-o2和-o3)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#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
", (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
", 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在X61100上不可靠,我没有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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


测试代码非常糟糕;它溢出了初始数组(这里的人难道不阅读编译器警告吗?),printf打印出错误的元素,它使用.byte作为rdtsc,没有好的理由,只有一个运行(!),没有什么可以检查最终结果是否是正确的(所以很容易"优化"成一些微妙的错误),包括的测试非常简单(没有负数?)没有什么可以阻止编译器将整个函数作为死代码丢弃。

也就是说,改进bitonic网络解决方案也很容易;只需将min/max/swap的内容更改为

1
#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%(Debian GCC4.4.5和-O2,AMD64,核心I7)。


几天前,我在谷歌上偶然发现了这个问题,因为我还需要快速排序6个整数的固定长度数组。然而,在我的例子中,我的整数只有8位(而不是32位),并且我没有严格的要求只使用C。我想无论如何我都会分享我的发现,以防它们对某人有帮助…

我在程序集中实现了一个网络排序的变体,它使用SSE尽可能地向量化比较和交换操作。对数组进行完全排序需要6次"传递"。我使用了一种新的机制来直接将pcmpgtb(矢量化比较)的结果转换为pshufb(矢量化交换)的无序参数,只使用paddb(矢量化加法),在某些情况下还使用pand(位与)指令。

这种方法也有产生真正无分支函数的副作用。没有任何跳转指令。

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

我需要注意的是,这种方法可以应用于任何大小不超过16个元素的数组。我预计,与其他方案相比,对于更大的阵列,相对速度优势会更大。

代码是用masm为带有ssse3的x86_64处理器编写的。函数使用"new"Windows x64调用约定。这是…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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代码调用函数:

1
void simd_sort_6(char *values);


虽然我非常喜欢所提供的交换宏:

1
2
3
#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; }

我看到了一个改进(一个好的编译器可以做到这一点):

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

我们注意到最小值和最大值是如何工作的,并显式地提取公共子表达式。这完全消除了最小和最大宏。


在没有基准测试和查看实际编译器生成的程序集的情况下,永远不要优化最小/最大值。如果我让GCC使用条件移动指令优化最小值,我会得到33%的加速:

1
#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个循环)。和麦克斯一起做?:大致相同,几乎在噪声中丢失,但上面的速度稍快一点。这种交换在GCC和Clang中都更快。

编译器也在寄存器分配和别名分析方面做了一项特殊的工作,有效地将d[x]移到前面的局部变量中,并且只在最后复制回内存。事实上,它们比完全使用局部变量(如d0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5])更好。我写这篇文章是因为您假定进行了强优化,但却试图在最小/最大值上胜过编译器::)

顺便说一下,我试过Clang和GCC。他们做了同样的优化,但由于调度的差异,两者的结果有一定的差异,不能说真的哪个更快或更慢。GCC在排序网络上的速度更快,在二次排序上会发生碰撞。

就完整性而言,展开的气泡排序和插入排序也是可能的。下面是气泡排序:

1
2
3
4
5
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);

下面是插入排序:

1
2
3
4
5
6
7
8
9
10
//#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);线入臂组件:

1
2
3
4
5
6
7
    MOV    r6, r2
    CMP    r6, r1
    MOVLT  r2, r1
    MOVLT  r1, r6
    CMP    r6, r0
    MOVLT  r1, r0
    MOVLT  r0, r6

对于六个元素,插入排序与排序网络具有竞争性(12个交换对15个迭代,平衡4个指令/交换对3个指令/迭代);气泡排序当然较慢。但当大小增大时,情况就不可能是这样了,因为插入排序是O(n^2),而排序网络是O(n logn)。


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

直接调用qsort库函数:101

幼稚实现(插入排序):299

插入排序(Daniel Stutzbach):108

插入排序展开:51

分类网络(Daniel Stutzbach):26

分类网络(Paul R):85

使用快速交换对网络12进行排序:117

排序网络12重新排序交换:116

军衔:56


XOR交换在交换函数中可能很有用。

1
2
3
4
5
6
7
void xorSwap (int *x, int *y) {
     if (*x != *y) {
         *x ^= *y;
         *y ^= *x;
         *x ^= *y;
     }
 }

if可能会在代码中引起太多的分歧,但是如果您有一个保证,即所有的int都是唯一的,那么这就很方便了。


期待着尝试我的手,并从这些例子中学习,但首先从我的1.5 GHz PPC PowerBook G4 w/1 GB DDR RAM的一些时间。(我从http://www.mcs.anl.gov/~kazutoo/rdtsc.html中借用了一个类似于RDTSC的计时器来计时。)我运行了程序几次,绝对结果各不相同,但始终最快的测试是"插入排序(daniel stutzbach)",接近一秒时"插入排序展开"。

以下是最后一组时间:

1
2
3
4
5
6
7
8
9
**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

这是我对这个线程的贡献:为包含唯一值的6成员int向量(valp)优化了1,4间隙shellsort。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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位环境。


如果插入排序在这里相当有竞争力,我建议尝试shellsort。恐怕6种元素太少了,不可能是最好的元素之一,但也许值得一试。

示例代码、未测试、未调试等。要调整inc=4和inc-=3序列以找到最佳序列(例如,尝试inc=2,inc-=1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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个元素的问题,谁知道…

根据维基百科,这甚至可以与分类网络相结合:普拉特,V(1979)。ShellSort和Sorting Networks(计算机科学优秀论文)。Garland。国际标准书号0-824-04406-1


这个问题已经很老了,但我现在不得不解决同样的问题:快速排序小数组。我认为分享我的知识是个好主意。当我第一次使用排序网络时,我终于找到了其他算法,对6个值的每一排列进行排序时所执行的比较总数小于排序网络,并且小于插入排序。我没有计算交换的数量;我希望它大致相等(有时可能会高一点)。

算法sort6使用算法sort4使用算法sort3。这里是在一些轻量级C++表单中的实现(原版是模板重的,以便它可以与任何随机访问迭代器和任何合适的比较函数一起工作)。

排序3个值

下面的算法是展开插入排序。当必须执行两个交换(6个分配)时,它使用4个分配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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然后使用数组的最后一个元素执行展开的插入排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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个:例如,如果第一个元素已插入到第三个位置,则最后一个元素不能插入到第四个位置以下n.名词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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次。

我希望这有帮助,即使这个问题可能不再代表一个实际问题:)

编辑:把它放在提供的基准中之后,它明显比大多数有趣的替代方法慢。它的性能往往比展开的插入排序要好一点,但差不多就是这样。基本上,它不是整数的最佳排序,但对于具有昂贵比较操作的类型来说可能很有趣。


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

我添加了一个秩序排序的循环版本,因为我自己的应用是对2-8个项目进行排序,所以由于参数的数量可变,所以需要一个循环。这也是我忽略排序网络解决方案的原因。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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计算指数,然后我必须做一个指数表。这是因为计算是基于目标而不是基于源的。请参见从基于源的索引转换为基于目标的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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/


我发现,至少在我的系统中,下面定义的sort6_iterator()sort6_iterator_local()函数运行的速度至少和上面的当前记录持有者一样快,而且经常显著地快:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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++能够更好地优化排序代码(如果我记得正确的话,这也是为什么这么多std算法的原因之一,例如S.std::sort()的性能一般都非常好。然而,当计时时,我注意到调用排序函数的上下文(即周围的代码)对性能有重大影响,这可能是由于函数是内联的,然后进行了优化。例如,如果程序足够简单,那么传递排序函数指针与传递迭代器之间的性能通常没有太大差别;否则,使用迭代器通常会导致显著的性能提高,而且(至少在我的经验中)从来没有显著的性能降低。我怀疑这可能是因为G++可以全局优化足够简单的代码。

此外,sort6_iterator()有时(同样,取决于调用函数的上下文)的性能始终优于以下排序函数,后者在对数据进行排序之前将数据复制到局部变量中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()定义为以下内容有时会导致性能稍好,尽管大多数情况下会导致性能稍差或性能差异可忽略不计。

1
2
3
#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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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行与上述不同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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优化(这与gcc需要处理普通的C函数的函数指针,而不是模板函数有关)。


我知道这是个老问题。

但我只是写了一个我想分享的不同的解决方案。只使用嵌套的最小-最大值,

它不是很快,因为它用了114个,可以简单地将其减少到75->Pastebin

但它不再是纯粹的最小-最大值了。

可能的工作是使用avx同时对多个整数执行最小/最大值

PMINSW参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#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
", (unsigned)cycles);

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

编辑:从Rex-Kerr提出的等级排序解决方案,比上面的混乱要快得多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}


我相信你的问题有两部分。

  • 一是确定最优算法。至少在这种情况下,这是通过循环遍历每个可能的顺序(没有那么多)来完成的,这允许您计算比较和交换的精确最小值、最大值、平均值和标准偏差。还有一两个亚军也在手边。
  • 二是优化算法。将教科书中的代码示例转换为平均和精益的现实算法可以做很多工作。如果你意识到一个算法不能被优化到需要的程度,那就去尝试一个亚军。

我不会太担心清空管道(假设是当前的x86):分支预测有很长的路要走。我担心的是确保代码和数据适合每个缓存行(代码可能有两个缓存行)。一旦出现回迁延迟,回迁的时间就会低得令人耳目一新,这将弥补任何拖延。它还意味着您的内部循环可能是10条左右的指令,这就是它应该在的地方(在我的排序算法中有两个不同的内部循环,它们分别是10条指令/22字节和9/22长)。假设代码不包含任何分隔符,您可以确定它将是盲目快速的。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//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
}


尝试"合并排序列表"排序。:)使用两个数组。小阵列和大阵列最快。如果是concating,则只检查插入位置。其他不需要比较的较大值(cmp=a-b>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码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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;
    }
}


使用cmp==0对4个项目进行排序。CMP的数目约为4.34(FF本机的数目约为4.52),但比合并列表花费3倍的时间。但如果有大数字或大文本,最好减少CMP操作。编辑:修复的Bug

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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;
}


也许我参加晚会迟到了,但至少我的贡献是一种新的方式。

  • 代码真的应该是内联的
  • 即使是内联的,也有太多的分支
  • 分析部分基本上是O(n(n-1)),对于n=6来说似乎可以。
  • 如果swap的成本更高(irt为compare的成本),代码可能更有效。
  • 我相信静态函数是内联的。
  • 该方法与等级排序有关
    • 使用相对列组(偏移)代替列组。
    • 对于任何排列组中的每个周期,列组的和都是零。
    • SWAP()中的两个元素不同,循环被追逐,只需要一个temp和一个(register->register)交换(new<-old)。

更新:改变代码一点,有些人用C++编译器编译C代码…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#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,"
");
}

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

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
##
"
                        , 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);
}


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


下面是三种典型的排序方法,代表三种不同的排序算法:

1
2
3
4
5
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年。