在回答另一个堆栈溢出问题(这一个)时,我偶然发现了一个有趣的子问题。对6个整数的数组进行排序的最快方法是什么?
因为问题层次很低:
&&
或||
序列点后面的那些)。实际上,这个问题是一种Golf,其目标不是最小化源长度,而是最小化执行时间。我称其为“Zening”代码,如书名中使用的代码优化禅宗由迈克尔个和它的续集。
至于为什么它有趣,有几个层面:
下面是我的参考(简单的,不是优化的)实现和测试集。
#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
Linux 64位,gcc 4.6.1 64位,Intel Core 2 Duo E8400, -O1
我包括了-O1和-O2的结果,因为令人惊讶的是,对于几个程序,O2比O1有效少。我想知道什么具体的优化有这种效果?
插入排序(丹尼尔·斯图茨巴赫)
正如预期的那样,最小化分支确实是一个好主意。
排序网络(丹尼尔·斯图茨巴赫)
比插入排序好。我想知道主要的效果是不是避免外部循环。我尝试通过展开插入排序来检查,确实我们得到了大致相同的数字(代码是在这里)。
排序网络(保罗R)
迄今为止最好的。我用来测试的实际代码是在这里。目前还不知道为什么它的速度几乎是其他排序网络实现的两倍。参数传递?快速max ?
排序网络12 SWAP与快速交换
根据Daniel Stutzbach的建议,我将他的12交换排序网络与无分支快速交换(代码为在这里)结合起来。它确实更快,到目前为止最好的,只有很小的利润率(大约5%),因为可以使用更少的交换。
同样有趣的是,无分支交换似乎比在PPC架构上使用if的简单交换效率低得多(4倍)。
调用库qsort
为了提供另一个参考点,我还尝试按建议调用库qsort(代码是在这里)。正如预期的那样,它要慢得多:慢了10到30倍……随着新测试套件的出现,主要问题似乎是第一次调用后库的初始负载,与其他版本相比,它并没有那么差。它在我的Linux上只慢了3到20倍。在其他人用于测试的某些架构上,它似乎更快(我真的很惊讶,因为库qsort使用了更复杂的API)。
等级次序
Rex Kerr提出了另一种完全不同的方法:对数组中的每一项直接计算其最终位置。这是有效的,因为计算排序不需要分支。这种方法的缺点是它需要三倍于数组的内存(数组和变量的一个副本来存储排序顺序)。性能结果非常令人惊讶(也很有趣)。在我使用32位操作系统和英特尔Core2 Quad E8300的参考架构上,循环计数略低于1000(就像使用分支交换的排序网络)。但是当在我的64位机(Intel Core2 Duo)上编译和执行时,它的表现要好得多:它是迄今为止最快的。我终于发现了真正的原因。我的32位盒子使用gcc 4.4.1,我的64位盒子使用gcc 4.4.3,最后一个似乎在优化这个特定的代码方面做得更好(其他建议的差别很小)。
更新:
正如上面公布的数字所示,这种效果在gcc的后续版本中仍然得到了增强,Rank Order的速度始终是其他任何替代版本的两倍。
用重新排序的交换对网络进行排序
Rex Kerr提议的gcc 4.4.3惊人的效率让我好奇:一个内存使用量是无分支排序网络3倍的程序怎么可能比它更快呢?我的假设是,它具有更少的写后读依赖性,允许更好地使用x86的超标量指令调度程序。这给了我一个想法:重新排序交换以最小化读写依赖。更简单地说:当你执行SWAP(1, 2); SWAP(0, 2);
时,你必须在执行第二次交换之前等待第一次交换完成,因为两者都访问一个公共的内存单元。当你执行__abc1时,处理器可以并行执行。我试了一下,结果和预期的一样,排序网络的运行速度快了10%。
使用简单交换对网络进行排序
在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个周期。我称之为惊人的快。还有其他可能的改进吗?