最快的固定长度6 int数组

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

由于问题非常低:

  • 我们不能假定图书馆可用(并且通话本身有其成本),只有普通的C
  • 为避免清空指令流水线(成本非常高),我们应该尽量减少分支,跳转以及其他任何类型的控制流程中断(例如隐藏在&&||序列点后面的流程)。
  • 空间有限,尽量减少寄存器和内存的使用是一个问题,理想情况下,排序可能是最好的。
  • 真的这个问题是一种高尔夫球,其目标不是尽量减少源长度而是缩短执行时间。 我将它称为'Zening'代码,用于Michael Abrash及其续集的“代码优化之禅”一书的标题中。

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

  • 这个例子很简单,易于理解和衡量,并没有涉及太多的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 %dn", 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 %dn", (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
  • 重新排序的排序网络w /快速交换:31.54
  • 重新排序的排序网络/快速交换V2:33.63
  • 内衬泡沫排序(保罗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%),可以预计使用1次掉期。

    有趣的是,注意到无网分支交换似乎比使用PPC体系结构的简单交换效率差很多(4倍)。

    调用库qsort

    为了给出另一个参考点,我还尝试建议只调用库qsort(代码在这里)。 正如预期的那样,速度要慢得多:慢10到30倍......随着新测试套件的出现,主要问题似乎是第一次调用后库的初始加载,并且与其他软件比较起来并不那么糟糕版。 在我的Linux上,它只是慢了3到20倍。 在一些用于其他测试的体系结构上,它似乎甚至更快(我真的很惊讶,因为库qsort使用更复杂的API)。

    排序

    雷克斯克尔提出了另一种完全不同的方法:对阵列中的每一项直接计算其最终位置。 这是有效的,因为计算等级顺序不需要分支。 这种方法的缺点是它需要三倍的数组内存量(一个数组副本和变量来存储等级顺序)。 表现的结果非常令人惊讶(也很有趣)。 在我使用32位操作系统和Intel Core2 Quad E8300的参考体系结构中,周期数略低于1000(例如使用分支交换进行排序的网络)。 但是,当我在64位盒(Intel Core2 Duo)上编译和执行时,它表现更好:它成为迄今为止最快的。 我终于找出真正的原因。 我的32位盒子使用gcc 4.4.1和我的64位盒子gcc 4.4.3,最后一个在优化这个特定代码方面似乎更好(其他提议没有什么区别)。

    更新:

    正如上面公布的数据显示,gcc的后续版本对这种影响仍然有所增强,秩序秩序一直比任何其他选择的速度快两倍。

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

    使用gcc 4.4.3的雷克斯克尔建议的惊人效率让我怀疑:3倍内存使用率的程序如何比无分类分类网络更快? 我的假设是,它在写入之后对类型读取的依赖性较小,可以更好地使用x86的超标量指令调度器。 这给了我一个想法:重新排序交换以最小化写入依赖关系之后的读取。 更简单地说:当你做SWAP(1, 2); SWAP(0, 2); SWAP(1, 2); SWAP(0, 2); 在执行第二个交换之前,您必须等待第一个交换完成,因为它们都访问共同的存储单元。 当你做SWAP(1, 2); SWAP(4, 5); 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个周期。 我称之为非常快。 任何其他可能的改进?


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

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

    您发布的算法与插入排序类似,但看起来您已将交换次数最小化,但需要花费更多的比较费用。 不过,比较远比交换更昂贵,因为分支可能导致指令管道停顿。

    这是一个插入排序实现:

    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
    }
    

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

    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--无分支,不存在经向偏差等问题)。

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


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

    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];
    }
    
    链接地址: http://www.djcxy.com/p/36347.html

    上一篇: Fastest sort of fixed length 6 int array

    下一篇: Measuring memory bandwidth from the dot product of two arrays