最快的固定长度6 int数组

kri*_*iss 396 sorting algorithm optimization gpgpu sorting-network

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

由于问题是非常低的水平:

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

真的这个问题是一种高尔夫,其目标不是最小化源长度而是执行时间.我把它叫做"Zening"代码在本书的标题中的代码优化禅迈克尔·亚伯拉什及其续集.

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

  • 这个例子很简单,易于理解和衡量,并没有太多的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);
}
Run Code Online (Sandbox Code Playgroud)

原始结果

随着变体的数量变得越来越大,我将它们全部收集在一个可以在这里找到的测试套件中.由于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
  • 重新排序网络w /快速交换: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
  • 重新排序网络w /快速交换:31.24
  • 重新排序网络w /快速交换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架构的简单交换(4倍)效率低.

调用库qsort

为了给出另一个参考点,我也按照建议尝试调用库qsort(代码在这里).正如预期的那样速度慢得多:慢了10到30倍......随着新测试套件变得明显,主要问题似乎是第一次调用后库的初始加载,并且它与其他调用的差别不大版.它在我的Linux上慢了3到20倍.在一些用于其他人测试的体系结构上,它似乎更快(我真的很惊讶,因为库qsort使用更复杂的API).

排序

Rex Kerr提出了另一种完全不同的方法:对于数组的每个项目直接计算其最终位置.这是有效的,因为计算等级顺序不需要分支.这种方法的缺点是它需要三倍的数组内存量(数组的一个副本和存储排名顺序的变量).性能结果非常令人惊讶(而且很有趣).在我的32位操作系统和英特尔酷睿2四核E8300的参考架构上,循环计数略低于1000(就像分支交换的排序网络一样).但是当我在我的64位盒(英特尔酷睿2双核处理器)上编译和执行时,它表现得更好:它成为目前为止最快的.我终于找到了真正的原因.我的32位框使用gcc 4.4.1和我的64位框gcc 4.4.3,最后一个框似乎更好地优化了这个特定的代码(其他提议几乎没有差别).

更新:

正如上面公布的数据所示,gcc的后续版本仍然增强了这种效果,而Rank Order的速度始终是其他任何替代品的两倍.

使用重新排序的交换对网络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
}
Run Code Online (Sandbox Code Playgroud)

如果我们相信我们的测试集(并且,它是非常差的,它的好处是简短且易于理解我们正在测量的内容),一种类型的结果代码的平均周期数低于40个周期(执行6次测试).这使得每次交换平均为4个周期.我打电话的速度非常快.还有其他改进吗?

Dan*_*ach 160

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

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

您发布的算法类似于插入排序,但看起来您已经以更多比较为代价减少了掉期数量.然而,比较远比交换更昂贵,因为分支可能导致指令管道停滞.

这是一个插入排序实现:

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;
        }
}
Run Code Online (Sandbox Code Playgroud)

这是我如何建立一个分拣网络.首先,使用此站点为适当长度的网络生成一组最小的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
}
Run Code Online (Sandbox Code Playgroud)

  • +1:很好,你用12个交换机做到了,而不是我上面手工编码和经验衍生网络中的13个交换机.如果我可以链接到为您生成网络的网站,我会给你另一个+1 - 现在加入书签. (9认同)
  • 如果您希望大多数请求是小型数组,那么这对于通用排序功能来说是个绝妙的想法.对于要优化的案例,请使用switch语句,使用此过程; 让默认情况使用库排序功能. (9认同)
  • @tgwh:XOR交换几乎总是一个坏主意. (7认同)
  • @Mark A*good*库排序函数已经有了小数组的快速路径.许多现代库将使用递归的QuickSort或MergeSort,在递归到'n <SMALL_CONSTANT`之后切换到InsertionSort. (5认同)
  • @Mark嗯,C库排序函数要求您通过函数移植器指定比较操作.为每次比较调用函数的开销很大.通常,这仍然是最干净的方式,因为这很少是程序中的关键路径.但是,如果它是关键路径,如果我们知道我们正在排序整数和其中的6个,我们真的可以更快地排序.:) (3认同)

Pau*_*l R 60

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

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);  
}
Run Code Online (Sandbox Code Playgroud)

你真的需要非常有效的无分支minmax实现,因为这实际上是这个代码归结为 - 一系列minmax操作(总共13个).我将此作为练习留给读者.

请注意,此实现很容易实现向量化(例如SIMD - 大多数SIMD ISA具有向量最小/最大指令)以及GPU实现(例如CUDA - 无分支,没有扭曲分歧等问题).

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

  • 如果我让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; }`.在这里我不使用?:对于d [y],因为它的性能略差,但它几乎在噪音中. (2认同)

Rex*_*err 44

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

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];
}
Run Code Online (Sandbox Code Playgroud)

  • @kriss:啊哈.这并不奇怪 - 有很多变量浮动,它们必须仔细排序并缓存在寄存器中等等. (3认同)
  • @SSpoke`0 + 1 + 2 + 3 + 4 + 5 = 15`由于其中一个缺失,15减去其余的总和缺少一个 (2认同)

Kev*_*ock 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)


小智 15

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

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

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

看起来这个实现比目前被标记为问题中最快的选项("使用简单交换排序网络12")的实现快约38%.我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
Run Code Online (Sandbox Code Playgroud)

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

void simd_sort_6(char *values);
Run Code Online (Sandbox Code Playgroud)


小智 14

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

话虽这么说,改进比特网络解决方案也很容易; 只需将最小/最大/ 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"); }
Run Code Online (Sandbox Code Playgroud)

它对我来说快了大约65%(Debian gcc 4.4.5 with -O2,amd64,Core i7).

  • 实际上,你甚至不需要汇编程序; 如果你只是放弃所有聪明的技巧,GCC将识别序列并为你插入条件移动:#define min(a,b)((a <b)?a:b)#define max(a,b)( (a <b)?b:a)#define SWAP(x,y){int a = min(d [x],d [y]); int b = max(d [x],d [y]); d [x] = a; d [y] = b; 它出来的速度可能比内联asm变种慢几个百分点,但由于缺乏适当的基准测试,这很难说. (4认同)
  • ......最后,如果您的数字是浮点数,而您不必担心NaN等,GCC可以将其转换为minss/maxss SSE指令,这比指令快25%.士气:放下聪明的苦涩技巧,让编译器完成它的工作.:-) (3认同)

phk*_*ler 13

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

#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; }
Run Code Online (Sandbox Code Playgroud)

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

#define SWAP(x,y) { int tmp = ((x ^ y) & -(y < x)); y ^= tmp; x ^= tmp; }
Run Code Online (Sandbox Code Playgroud)

我们注意min和max如何工作并明确地拉出公共子表达式.这完全消除了最小和最大宏.


Pao*_*ini 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; }
Run Code Online (Sandbox Code Playgroud)

(测试代码中280次与420次循环).使用?:做最大值或多或少相同,几乎在噪声中丢失,但上面的速度要快一些.对于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);
Run Code Online (Sandbox Code Playgroud)

这是插入排序:

//#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);
Run Code Online (Sandbox Code Playgroud)

这种插入排序比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
Run Code Online (Sandbox Code Playgroud)

对于六个元素,插入排序与排序网络竞争(12次交换与15次迭代平衡4次指令/交换与3次指令/迭代); 冒泡当然比较慢.但是当尺寸增大时,它不会成立,因为插入排序是O(n ^ 2),而排序网络是O(n log n).


jhe*_*iko 11

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

直接调用qsort库函数:101

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

插入排序(Daniel Stutzbach) :108

插入排序已展开 :51

排序网络(Daniel Stutzbach) :26

排序网络(Paul R) :85

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

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

排名顺序 :56

  • 这是带有无符号输入的PPC的无分支最小值/最大值:`subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; 添加r4,r6,r4; subf r3,r6,r3`.r3/r4是输入,r5/r6是暂存寄存器,输出r3得到最小值,r4得到最大值.它应该可以手工调度.我发现它是使用GNU superoptimizer,从4个指令的最小和最大序列开始,并手动查看可以组合的两个.对于带符号的输入,您当然可以在开头的所有元素中添加0x80000000,并在结尾处再次减去它,然后就像它们是无符号的一样工作. (4认同)

小智 7

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

void xorSwap (int *x, int *y) {
     if (*x != *y) {
         *x ^= *y;
         *y ^= *x;
         *x ^= *y;
     }
 }
Run Code Online (Sandbox Code Playgroud)

if可能会导致您的代码过多分歧,但如果您保证所有的int都是唯一的,那么这可能很方便.

  • 当*不工作时,`x`和`y`指向同一位置. (11认同)

小智 5

期待尝试这一点,并从这些例子中学习,但首先是我的1.5 GHz PPC Powerbook G4 w/1 GB DDR RAM的一些时间.(我从http://www.mcs.anl.gov/~kazutomo/rdtsc.html为PPC借了一个类似rdtsc的计时器来计算时间.)我运行了几次程序,绝对结果各不相同但是一致最快的测试是"插入排序(Daniel Stutzbach)",其中"Insertion Sort Unrolled"紧随其后.

这是最后一次:

**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
Run Code Online (Sandbox Code Playgroud)