412

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

由于问题非常低级:

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

实际上,这个问题是一种高尔夫,其目标不是最小化源长度而是最小化执行时间。我将其称为“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 和 -O2 结果都包括在内,因为令人惊讶的是,对于几个程序来说,O2 的效率低于O1。我想知道什么具体的优化有这个效果?

对提议的解决方案的评论

插入排序 (Daniel Stutzbach)

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

排序网络 (Daniel Stutzbach)

比插入排序好。我想知道主要效果是否不是来自避免外部循环。我通过展开插入排序进行了尝试,确实我们得到了大致相同的数字(代码在这里)。

排序网络 (Paul R)

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

排序网络 12 带有快速交换的交换

正如 Daniel Stutzbach 所建议的,我将他的 12 交换排序网络与无分支快速交换相结合(代码在此处)。它确实更快,是迄今为止最好的,利润率很小(大约 5%),正如使用少掉 1 次交换所预期的那样。

有趣的是,无分支交换似乎比在 PPC 架构上使用 if 的简单交换效率低很多(4 倍)。

调用库 qsort

为了提供另一个参考点,我还按照建议尝试调用库 qsort(代码在此处)。正如预期的那样,它要慢得多:慢了 10 到 30 倍......随着新的测试套件变得很明显,主要问题似乎是第一次调用后库的初始加载,它与其他的相比并没有那么差版本。在我的 Linux 上,它只慢了 3 到 20 倍。在其他人用于测试的某些架构上,它似乎甚至更快(我对此感到非常惊讶,因为库 qsort 使用更复杂的 API)。

排序

Rex Kerr 提出了另一种完全不同的方法:对数组中的每一项直接计算其最终位置。这是有效的,因为计算排名顺序不需要分支。这种方法的缺点是它需要三倍于数组的内存量(一份数组和变量的副本来存储排名顺序)。性能结果非常令人惊讶(也很有趣)。在我的 32 位操作系统和 Intel Core2 Quad E8300 的参考架构上,循环计数略低于 1000(如使用分支交换对网络进行排序)。但是当在我的 64 位机器(Intel Core2 Duo)上编译和执行时,它的表现要好得多:它成为迄今为止最快的。我终于找到了真正的原因。我的 32 位盒子使用 gcc 4.4.1 和我的 64 位盒子 gcc 4.4。

更新

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

使用重新排序的交换对网络 12 进行排序

使用 gcc 4.4.3 的 Rex Kerr 提案的惊人效率让我想知道:内存使用量是 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 个周期。我称之为惊人的快。还有其他可能的改进吗?

4

24 回答 24

165

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

你对输入数据有什么了解吗?对于某些类型的数据,一些算法会表现得更好。例如,插入排序在已排序或几乎已排序的数据上表现更好,因此如果几乎排序数据的机会高于平均水平,它将是更好的选择。

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

这是一个插入排序实现:

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
}
于 2010-05-07T15:02:00.970 回答
65

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

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 - 无分支,没有扭曲发散等问题)。

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

于 2010-05-07T07:37:30.750 回答
47

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

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];
}
于 2010-05-07T23:19:00.370 回答
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,节拍 -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
于 2011-08-24T15:29:18.580 回答
15

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

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

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

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

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

代码是用 MASM 编写的,用于带有 SSSE3 的 x86_64 处理器。该函数使用“新的”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);
于 2012-12-02T03:18:17.007 回答
15

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

话虽如此,改进双音网络解决方案也很容易;只需将 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%(Debian gcc 4.4.5 with -O2, amd64, Core i7)。

于 2011-08-24T16:10:21.187 回答
13

While I really like the swap macro provided:

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

I see an improvement (which a good compiler might make):

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

We take note of how min and max work and pull the common sub-expression explicitly. This eliminates the min and max macros completely.

于 2011-08-24T14:18:42.217 回答
12

永远不要在没有基准测试和查看实际编译器生成的程序集的情况下优化最小值/最大值。如果我让 GCC 使用条件移动指令优化 min,我将获得 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]。我写这篇文章是因为你假设有很强的优化,但试图在 min/max 上超越编译器。:)

顺便说一句,我尝试了 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 次交换与 15 次迭代平衡 4 条指令/交换与 3 条指令/迭代);冒泡排序当然更慢。但是当大小增加时它就不是真的了,因为插入排序是 O(n^2) 而排序网络是 O(n log n)。

于 2011-08-24T20:40:42.127 回答
11

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

直接调用 qsort 库函数:101

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

插入排序 (Daniel Stutzbach) : 108

插入排序展开 :51

排序网络 (Daniel Stutzbach) : 26

排序网络(Paul R) :85

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

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

排名顺序 :56

于 2011-08-24T13:15:06.827 回答
7

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

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

if 可能会在您的代码中导致太多的分歧,但如果您保证所有的 int 都是唯一的,这可能会很方便。

于 2011-08-24T11:36:20.647 回答
5

期待在此尝试并从这些示例中学习,但首先是我的 1.5 GHz PPC Powerbook G4 w/1 GB DDR RAM 的一些时序。(我从http://www.mcs.anl.gov/~kazutomo/rdtsc.html为 PPC 借了一个类似 rdtsc 的计时器用于计时。)我运行了几次程序,绝对结果各不相同,但始终如一最快的测试是“插入排序 (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
于 2011-08-25T02:49:06.720 回答
4

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

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 位环境。

于 2012-03-14T11:14:14.800 回答
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

于 2011-08-24T14:37:49.483 回答
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/

于 2016-09-05T19:59:56.253 回答
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 次比较:例如,如果第一个元素已插入第三个位置,那么最后一个元素不能插入低于第四个位置。

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。

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

编辑:将其放入提供的基准测试后,它显然比大多数有趣的替代方案要慢。它的性能往往比展开插入排序好一点,但仅此而已。基本上,它不是整数的最佳排序,但对于具有昂贵比较操作的类型可能会很有趣。

于 2015-10-07T09:57:51.520 回答
2

我发现至少在我的系统上,下面定义的函数sort6_iterator()sort6_iterator_local()上面的当前记录保持者的运行速度至少一样快,而且通常明显快得多:

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

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

  SWAP(1, 2) SWAP(4, 5)
  SWAP(0, 2) SWAP(3, 5)
  SWAP(0, 1) SWAP(3, 4)
  SWAP(1, 4) SWAP(0, 3)
  SWAP(2, 5) SWAP(1, 3)
  SWAP(2, 4)
  SWAP(2, 3)
#undef SWAP
}

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

我怀疑(来自像this和其他地方的评论)使用迭代器为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; }

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

template<class T> inline void sort6(T it) {
#define SORT2(x,y) {if(data##x>data##y){auto a=std::move(data##y);data##y=std::move(data##x);data##x=std::move(a);}}
#define DD1(a)   register auto data##a=*(it+a);
#define DD2(a,b) register auto data##a=*(it+a);register auto data##b=*(it+b);
#define CB1(a)   *(it+a)=data##a;
#define CB2(a,b) *(it+a)=data##a;*(it+b)=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

或者,如果您想通过引用传递变量,请使用它(下面的函数与上面的前 5 行不同):

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

使用register关键字的原因是因为这是您知道要将这些值放在寄存器中的少数几次之一。如果没有register,编译器大部分时间都会解决这个问题,但有时不会。使用register关键字有助于解决此问题。但是,通常不要使用register关键字,因为它更有可能减慢代码速度而不是加快速度。

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

  1. 在对各种排序函数进行计时时,我注意到调用排序函数的上下文(即周围代码)对性能有重大影响,这可能是由于函数被内联然后优化。例如,如果程序足够简单,那么传递排序函数指针和传递迭代器之间的性能通常不会有太大差异。否则,使用迭代器通常会带来明显更好的性能,并且永远不会(至少以我目前的经验)任何明显更差的性能。我怀疑这可能是因为 g++ 可以全局优化足够简单的代码。
于 2017-06-03T03:45:39.850 回答
1

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

  • 首先是确定最优算法。这是通过循环遍历每个可能的排序(没有那么多)来完成的,这允许您计算比较和交换的精确最小值、最大值、平均值和标准偏差。也有一个或两个亚军。
  • 二是优化算法。可以做很多事情来将教科书代码示例转换为平均和精益的现实生活算法。如果您意识到算法无法优化到所需的程度,请尝试获得亚军。

我不会太担心清空管道(假设当前的 x86):分支预测已经走了很长一段路。我要担心的是确保代码和数据分别适合一个缓存行(可能两个用于代码)。一旦获取延迟非常低,这将弥补任何停顿。这也意味着您的内部循环可能是十条左右的指令,这是正确的(我的排序算法中有两个不同的内部循环,它们分别是 10 条指令/22 ​​字节和 9/22 长)。假设代码不包含任何 div,您可以确定它会非常快。

于 2012-03-06T10:33:07.123 回答
1

我想我会尝试展开的Ford-Johnson 合并插入排序,它可以实现最小可能的比较次数 (ceil(log2(6!)) = 10) 并且没有交换。不过,它不会竞争(我比最差的排序网络解决方案稍微好一点sort6_sorting_network_v1)。

它将值加载到六个寄存器中,然后执行 8 到 10 次比较以确定 720=6!它所在的情况下,然后将寄存器写回这 720 个命令中的适当一个(每种情况下的单独代码)。在最终回写之前,没有任何交换或重新排序。我还没有查看生成的汇编代码。

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

TEST(ford_johnson_unrolled,   "Unrolled Ford-Johnson Merge-Insertion sort");
于 2021-11-23T17:52:50.323 回答
1

我知道这是一个老问题。

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

它并不快,因为它每个使用 114 个,
可以像这样简单地将其减少到 75 个 -> pastebin

但是,它不再是纯粹的 min max 了。

可能有用的是使用 AVX 一次对多个整数执行 min/max

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;
}
于 2017-09-28T19:56:12.233 回答
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
}
于 2019-02-20T14:46:39.323 回答
0

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

  • 代码确实应该内联
  • 就算内联了,分支也太多了
  • 分析部分基本上是 O(N(N-1)) ,对于 N=6 来说似乎没问题
  • 如果成本swap更高(irt 成本compare),代码可能会更有效
  • 我相信静态函数是内联的。
  • 该方法与rank-sort有关
    • 代替等级,使用相对等级(偏移量)。
    • 对于任何排列组中的每个循环,秩和为零。
    • 而不是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);
}
于 2018-02-01T01:00:50.510 回答
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;
	}
}

于 2017-01-12T14:26:11.563 回答
-1

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

于 2011-08-24T15:04:33.423 回答
-1

使用 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;
}
于 2017-01-13T08:55:32.307 回答