最快的固定长度 6 int 数组
Posted
技术标签:
【中文标题】最快的固定长度 6 int 数组【英文标题】:Fastest sort of fixed length 6 int array 【发布时间】:2011-02-16 17:33:13 【问题描述】:回答另一个 Stack Overflow 问题 (this one) 我偶然发现了一个有趣的子问题。对 6 个整数的数组进行排序的最快方法是什么?
由于问题级别很低:
我们不能假设库是可用的(并且调用本身有它的成本),只有普通的 C 为避免清空指令流水线(成本非常高),我们可能应该尽量减少分支、跳转和所有其他类型的控制流中断(例如隐藏在&&
中的序列点后面的那些)或||
)。
空间有限,尽量减少寄存器和内存使用是一个问题,理想情况下,就地排序可能是最好的。
真的,这个问题是一种高尔夫,其目标不是最小化源长度,而是最小化执行时间。我将其称为“Zening”代码,正如Michael Abrash 及其sequels 的书名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);
原始结果
随着变体的数量越来越多,我将它们全部收集在一个测试套件中,可以在here 找到。感谢 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.30Linux 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)
优于插入排序。我想知道主要效果是否不是来自避免外部循环。我尝试通过展开插入排序进行检查,确实我们得到了大致相同的数字(代码是here)。
排序网络 (Paul R)
迄今为止最好的。我用来测试的实际代码是here。还不知道为什么它的速度几乎是其他排序网络实现的两倍。参数传递?快速最大值?
使用快速交换对网络进行排序 12 SWAP
按照 Daniel Stutzbach 的建议,我将他的 12 交换排序网络与无分支快速交换(代码为 here)结合起来。它确实更快,是迄今为止最好的,利润率很小(大约 5%),正如使用少掉 1 次交换所预期的那样。
有趣的是,无分支交换似乎比在 PPC 架构上使用 if 的简单交换效率低很多(4 倍)。
调用库 qsort
为了提供另一个参考点,我还按照建议尝试调用库 qsort(代码为 here)。正如预期的那样,它要慢得多:慢了 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.3,最后一个在优化这个特定代码方面似乎要好得多(其他建议几乎没有区别)。
更新:
正如上面公布的数据所示,更高版本的 gcc 仍然增强了这种效果,并且排名顺序始终是任何其他替代方案的两倍。
使用重新排序的交换对网络 12 进行排序
Rex Kerr 提案与 gcc 4.4.3 的惊人效率让我想知道:内存使用量是 3 倍的程序怎么可能比无分支排序网络更快?我的假设是,它具有较少的读写依赖,从而可以更好地使用 x86 的超标量指令调度程序。这给了我一个想法:重新排序交换以最大程度地减少写入后读取的依赖关系。更简单地说:当您执行SWAP(1, 2); SWAP(0, 2);
时,您必须等待第一次交换完成才能执行第二次交换,因为两者都访问公共存储单元。当您执行SWAP(1, 2); SWAP(4, 5);
时,处理器可以并行执行两者。我试过了,它按预期工作,排序网络的运行速度快了大约 10%。
使用简单交换对网络进行排序 12
在最初的帖子 Steinar H. Gunderson 建议一年后,我们不应该试图智取编译器并保持交换代码简单。这确实是一个好主意,因为生成的代码快了大约 40%!他还提出了使用 x86 内联汇编代码手动优化的交换,仍然可以节省更多的周期。最令人惊讶的是(它说明了程序员的心理),一年前没有人尝试过那个版本的交换。我用来测试的代码是here。其他人提出了编写 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 个周期。我称之为惊人的快。还有其他可能的改进吗?
【问题讨论】:
你对整数有什么限制吗?例如,我们是否可以假设对于任何 2 x,yx-y
和 x+y
都不会导致下溢或上溢?
您应该尝试将我的 12-swap 排序网络与 Paul 的无分支交换功能结合起来。他的解决方案将所有参数作为堆栈上的单独元素而不是指向数组的单个指针传递。这也可能会有所作为。
请注意,rdtsc 在 64 位上的正确实现是 __asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
,因为 rdtsc 将答案放在 EDX:EAX 中,而 GCC 期望它在单个 64 位寄存器中。您可以通过在 -O3 处编译来查看该错误。另请参阅下面我对 Paul R 关于更快 SWAP 的评论。
@Tyler:你如何在没有分支的情况下在装配级别实现它?
@Loren: CMP EAX, EBX; SBB EAX, EAX
将根据EAX
分别大于或小于EBX
将0 或0xFFFFFFFF 放入EAX
。 SBB
是“借位减法”,与ADC
对应(“进位加法”);您引用的状态位 是 进位位。再说一次,我记得 ADC
和 SBB
在 Pentium 4 上的延迟和吞吐量与 ADD
和 SUB
相比非常糟糕,并且在 Core CPU 上仍然慢两倍。由于80386还有SETcc
conditional-store和CMOVcc
conditional-move指令,但是也很慢。
【参考方案1】:
对于任何优化,最好是测试、测试、测试。我会尝试至少排序网络和插入排序。如果我下注,我会根据过去的经验将钱投入到插入排序中。
你对输入数据有什么了解吗?对于某些类型的数据,一些算法会表现得更好。例如,插入排序在已排序或几乎已排序的数据上表现更好,因此如果几乎已排序数据的机会高于平均水平,则它会是更好的选择。
您发布的算法类似于插入排序,但看起来您以更多比较为代价最小化了交换次数。但是,比较比交换要昂贵得多,因为分支会导致指令流水线停止。
这是一个插入排序的实现:
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;
以下是我构建排序网络的方法。首先,使用this site 为适当长度的网络生成一组最小的 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
【讨论】:
+1:很好,你用 12 个交换而不是上面我的手工编码和经验派生网络中的 13 个交换。如果可以的话,我会再给你一个 +1 链接到为你生成网络的网站 - 现在已加入书签。 如果您希望大多数请求是小型数组,这对于通用排序功能来说是一个绝妙的主意。使用此过程,对要优化的情况使用 switch 语句;让默认情况使用库排序函数。 @Mark good 库排序函数已经为小数组提供了快速路径。许多现代库将使用递归 QuickSort 或 MergeSort,在递归到n < SMALL_CONSTANT
后切换到 InsertionSort。
@Mark 好吧,C 库排序函数要求您通过函数移植器指定比较操作。每次比较调用函数的开销是巨大的。通常,这仍然是最干净的方法,因为这很少是程序中的关键路径。但是,如果它是关键路径,那么如果我们知道我们正在对整数进行排序并且正好是其中的 6 个整数,那么我们确实可以更快地排序。 :)
@tgwh:异或交换几乎总是一个坏主意。【参考方案2】:
这是一个使用sorting networks的实现:
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);
为此,您确实需要非常高效的无分支 min
和 max
实现,因为这实际上是这段代码归结为的 - 一系列 min
和 max
操作(总共 13 个) .我把这个作为练习留给读者。
请注意,此实现很容易用于矢量化(例如 SIMD - 大多数 SIMD ISA 具有矢量最小/最大指令)以及 GPU 实现(例如 CUDA - 无分支,因此没有扭曲发散等问题)。
另见:Fast algorithm implementation to sort very small list
【讨论】:
对于最小值/最大值的一些小技巧:graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax @Paul:在真正的 CUDA 使用环境中,这当然是最好的答案。我会检查它是否也在 Golf x64 上下文中(以及多少)并发布结果。Sort3
会更快(在大多数架构上,无论如何)如果您注意到 (a+b+c)-(min+max)
是中心数字。
@Rex:我明白了——看起来不错。对于像 AltiVec 和 SSE 这样的 SIMD 架构,它的指令周期数将相同(最大值和最小值是单周期指令,如加/减),但对于普通的标量 CPU,您的方法看起来更好。
如果我让 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;
。在这里我没有使用 ?: for d[y] 因为它的性能稍差一些,但几乎没有噪音。【参考方案3】:
既然这些是整数并且比较速度很快,为什么不直接计算每个的排名顺序:
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];
【讨论】:
@Rex:使用 gcc -O1 它低于 1000 个周期,相当快但比排序网络慢。有什么想法可以改进代码吗?也许如果我们可以避免数组复制... @kriss:对我来说,使用 -O2 比排序网络快。是否有某些原因导致 -O2 不正常,或者 -O2 对您来说也更慢?也许这是机器架构的不同? @Rex:抱歉,我一见钟情就错过了 > vs >= 模式。它适用于任何情况。 @kriss:啊哈。这并不完全令人惊讶——有很多变量四处飘荡,它们必须仔细排序并缓存在寄存器中等等。 @SSpoke0+1+2+3+4+5=15
由于缺少其中一个,因此 15 减去其余的总和得出缺少一个【参考方案4】:
看起来我迟到了一年参加聚会,但我们开始吧......
查看 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;
我更改了modified the test suite 以报告每个排序的时钟并运行更多测试(更新 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
【讨论】:
您对寄存器变量的想法应该应用于 Rex Kerr 的“排名顺序”解决方案。那应该是最快的,也许到那时-O3
优化不会适得其反。
@cdunn2001 我刚刚测试了它,我没有看到改进(除了 -O0 和 -Os 的几个周期)。查看 asm 似乎 gcc 已经设法弄清楚使用寄存器并消除对 memcpy 的调用。
您是否介意将简单交换版本添加到您的测试套件中,我想将其与手动优化的组装快速交换进行比较可能会很有趣。
你的代码仍然使用 Gunderson 的交换,我的应该是 #define SWAP(x,y) int oldx = x; x = x < y ? x : y; y ^= oldx ^ x;
。
@Paolo Bonzini:是的,我打算和你一起添加一个测试用例,只是还没来得及。但我会避免内联汇编。【参考方案5】:
测试代码很糟糕;它溢出了初始数组(这里的人不阅读编译器警告吗?),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)。
【讨论】:
好的,测试代码很差。随意改进它。是的,您可以使用汇编代码。为什么不一直使用 x86 汇编程序对其进行完全编码?它可能不太便携,但为什么要麻烦? 感谢您注意到数组溢出,我已更正。其他人可能没有注意到,因为点击了复制/粘贴代码的链接,没有溢出。 实际上,你甚至不需要汇编程序;如果您放弃所有巧妙的技巧,GCC 将识别序列并为您插入条件移动:#define min(a, b) ((a ...最后,如果您的数字是浮点数,并且您不必担心 NaN 等问题,GCC 可以将其转换为 minss/maxss SSE 指令,速度快 25%。士气:放弃聪明的小技巧,让编译器完成它的工作。 :-)【参考方案6】:几天前我偶然发现了 Google 提出的这个问题,因为我还需要快速排序一个由 6 个整数组成的固定长度数组。然而,就我而言,我的整数只有 8 位(而不是 32 位),而且我没有严格要求只使用 C。我想无论如何我都会分享我的发现,以防它们可能对某人有帮助......
我在汇编中实现了一种网络排序的变体,它尽可能使用 SSE 对比较和交换操作进行矢量化。对数组进行完全排序需要六次“通过”。我使用一种新颖的机制将 PCMPGTB(矢量化比较)的结果直接转换为 PSHUFB(矢量化交换)的随机参数,仅使用 PADDB(矢量化加法),在某些情况下还使用 PAND(按位与)指令。
这种方法还具有产生真正无分支函数的副作用。没有任何跳转指令。
与当前在问题中被标记为最快选项的实现(“使用简单交换对网络 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
您可以将其编译为可执行对象并将其链接到您的 C 项目中。有关如何在 Visual Studio 中执行此操作的说明,您可以阅读 this article。您可以使用以下 C 原型从您的 C 代码中调用该函数:
void simd_sort_6(char *values);
【讨论】:
将您的提案与其他装配级提案进行比较会很有趣。执行的比较性能不包括它们。无论如何,使用 SSE 听起来不错。 未来研究的另一个领域是将新的英特尔 AVX 指令应用于此问题。较大的 256 位向量足以容纳 8 个 DWORD。 用pxor / pinsrd xmm4, mem, 0
代替movd
!【参考方案7】:
虽然我真的很喜欢提供的交换宏:
#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;
我看到了改进(一个好的编译器可能会做出):
#define SWAP(x,y) int tmp = ((x ^ y) & -(y < x)); y ^= tmp; x ^= tmp;
我们注意到 min 和 max 是如何工作的,并明确地提取公共子表达式。这完全消除了 min 和 max 宏。
【讨论】:
这会让它们倒退,注意 d[y] 得到最大值,即 x^(common subexpression)。 我注意到了同样的事情;我认为要使您的实现正确,您需要d[x]
而不是x
(y
相同),d[y] < d[x]
用于此处的不等式(是的,与最小/最大代码不同)。
我尝试了你的交换,但本地优化在更大的层面上会产生负面影响(我猜它会引入依赖关系)。并且结果比其他交换慢。但正如您所看到的,提出的新解决方案确实有很多性能可以获得优化交换。【参考方案8】:
在没有进行基准测试和查看实际编译器生成的程序集的情况下,切勿优化最小值/最大值。如果我让 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 或多或少是一样的,几乎迷失在噪音中,但上面的速度有点快。这种 SWAP 使用 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 在排序网络上更快,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 条指令)。例如,这里是 ARM 汇编中的 t = d[2]; ITER(1); ITER(0);
行:
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)。
【讨论】:
或多或少相关:我向 GCC 提交了a report,以便它可以直接在编译器中实现优化。不确定它是否会完成,但至少你可以跟随它的发展。【参考方案9】:我将测试套件移植到我无法识别的 PPC 架构机器上(不必接触代码,只需增加测试的迭代次数,使用 8 个测试用例以避免使用 mod 污染结果并替换 x86 特定的 rdtsc ):
直接调用qsort库函数:101
朴素实现(插入排序):299
插入排序(Daniel Stutzbach):108
插入排序展开:51
排序网络 (Daniel Stutzbach):26
排序网络(Paul R):85
使用快速交换对网络 12 进行排序:117
Sorting Networks 12 reordered Swap:116
排名顺序:56
【讨论】:
真的很有趣。看起来无分支交换在 PPC 上是个坏主意。它也可能是编译器相关的效果。用的是哪一个? 它是 gcc 编译器的一个分支——最小、最大逻辑可能不是无分支的——我会检查反汇编并让你知道,但除非编译器足够聪明,包括像 x 这里是无符号输入的 PPC 的无分支最小值/最大值:subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3
。 r3/r4 是输入,r5/r6 是暂存器,输出 r3 获取最小值,r4 获取最大值。它应该是可以手动安排的。我用 GNU 超级优化器找到了它,从 4 条指令最小和最大序列开始,然后手动寻找两个可以组合的。对于有符号输入,当然可以在所有元素的开头加上 0x80000000,最后再减去,然后就好像它们没有符号一样工作。【参考方案10】:
XOR 交换可能对您的交换函数很有用。
void xorSwap (int *x, int *y)
if (*x != *y)
*x ^= *y;
*y ^= *x;
*x ^= *y;
if 可能会导致您的代码有太多分歧,但如果您保证所有的 int 都是唯一的,这可能会很方便。
【讨论】:
xor swap 也适用于相等的值... x^=y 将 x 设置为 0,y^=x 将 y 保留为 y (==x),x^=y 将 x 设置为 y 当x
和y
指向同一个位置时,它不起作用。
无论如何,当与排序网络一起使用时,我们永远不会调用 x 和 y 都指向同一个位置。仍然需要找到一种方法来避免测试,从而获得与无分支交换相同的效果。我有一个实现这一目标的想法。【参考方案11】:
期待亲自尝试并从这些示例中学习,但首先是我的 1.5 GHz PPC Powerbook G4 w/1 GB DDR RAM 的一些时序。 (我从http://www.mcs.anl.gov/~kazutomo/rdtsc.html 借了一个类似 rdtsc 的 PPC 计时器用于计时。)我运行了几次程序,绝对结果各不相同,但始终最快的测试是“插入排序(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
【讨论】:
【参考方案12】:这是我对该线程的贡献:针对包含唯一值的 6 成员 int 向量 (valp) 优化的 1、4 间隙 shellsort。
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 位环境。
【讨论】:
不错。我会将它添加到更长的测试套件中【参考方案13】:如果插入排序在这里具有相当的竞争力,我建议尝试使用 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 个元素的问题,谁知道...
根据 Wikipedia,这甚至可以与排序网络结合使用: 普拉特,五世 (1979)。 Shellsort 和排序网络(计算机科学中的杰出论文)。花环。国际标准书号 0-824-04406-1
【讨论】:
随意提出一些实现:-) 提案已添加。享受错误。【参考方案14】:我知道我迟到了,但我有兴趣尝试一些不同的解决方案。首先,我清理了该粘贴,使其编译,并将其放入存储库。我保留了一些不受欢迎的解决方案作为死胡同,这样其他人就不会尝试了。其中包括我的第一个解决方案,它试图确保 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 计算索引,然后我必须制作一个索引表。这是因为计算是基于目标的,而不是基于源的。见Converting from Source-based Indices to Destination-based Indices
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/
【讨论】:
您可以在整数向量上使用vmovmskps
(使用强制转换来保持内在函数快乐),避免需要右移位扫描 (ffs
) 结果。
您可以根据cmpgt
结果通过减去它有条件地加1,而不是用set1(1)
屏蔽它。例如index = _mm256_sub_epi32(index, gt)
index -= -1 or 0;
eq = _mm256_insert_epi32(eq, 0, I)
如果按写入方式编译,则不是将元素归零的有效方法(尤其是对于低 4 以外的元素,因为 vpinsrd
仅适用于 XMM 目标;索引高于 3必须模仿)。相反,_mm256_blend_epi32
(vpblendd
) 的向量为零。 vpblendd
是在任何端口上运行的单指令指令,而在 Intel CPU 上需要端口 5 的 shuffle。 (agner.org/optimize)。
另外,您可以考虑从同一来源生成具有不同 shuffle 的 rot
向量,或者至少并行运行 2 个交替使用的 dep 链,而不是通过一条通道的单个 dep 链- 交叉洗牌(3 个周期延迟)。这将在一次排序中增加 ILP。 2 个 dep 链将向量常量的数量限制在一个合理的范围内,只有 2:1 用于 1 次旋转,1 用于 2 次旋转步骤相结合。【参考方案15】:
这个问题已经很老了,但这些天我实际上不得不解决同样的问题:快速算法来对小数组进行排序。我认为分享我的知识是个好主意。虽然我一开始使用排序网络,但我最终设法找到了其他算法,它们对 6 个值的每个排列进行排序所执行的比较总数小于排序网络,并且小于插入排序。我没有计算掉交换的次数;我希望它大致相等(有时可能会更高一些)。
算法sort6
使用算法sort4
,后者使用算法sort3
。这是一些轻量级 C++ 形式的实现(原始模板是大量模板,因此它可以与任何随机访问迭代器和任何合适的比较函数一起使用)。
Sorting 3 values
以下算法是展开插入排序。当必须执行两次交换(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次:例如, 如果第一个元素已经插入到第 3 个位置,那么最后一个元素不能插入到低于第 4 个位置。
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。
我希望这会有所帮助,即使这个问题可能不再代表实际问题:)
编辑: 将其放入提供的基准测试后,它显然比大多数有趣的替代方案要慢。它的性能往往比展开的插入排序好一点,但仅此而已。基本上,它不是整数的最佳排序,但对于具有昂贵的比较操作的类型可能会很有趣。
【讨论】:
这些很好。由于解决的问题已有数十年的历史,可能与 C 编程一样古老,现在这个问题已经有近 5 年的历史了,看起来并没有那么重要。 你应该看看其他答案的计时方式。关键是,对于如此小的数据集计数比较甚至比较和交换并不能真正说明算法有多快(基本上排序 6 个整数总是 O(1),因为 O(6*6) 是 O(1))。目前最快的先前提出的解决方案是使用大比较(RexKerr)立即找到每个值的位置。 @kriss 现在是最快的吗?根据我对结果的阅读,排序网络方法是最快的,我的错。确实,我的解决方案来自我的通用库,而且我并不总是比较整数,也不总是使用operator<
进行比较。除了比较和交换的客观计数之外,我还正确地计时了我的算法;这个解决方案是最快的通用解决方案,但我确实错过了@RexKerr 的解决方案。试试看:)
RexKerr (Order Rank) 的解决方案成为自 gcc 编译器 4.2.3 以来 X86 架构上最快的解决方案(从 gcc 4.9 开始,速度几乎是第二好的两倍)。但它在很大程度上依赖于编译器优化,在其他架构上可能并非如此。
@kriss 这很有趣。我确实可以再次与-O3
产生更多差异。我想我会为我的排序库采用另一种策略:提供三种算法,以减少比较次数、减少交换次数或获得最佳性能。至少,发生的事情对读者来说是透明的。感谢您的见解:)【参考方案16】:
我发现,至少在我的系统上,下面定义的函数 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
的迭代器。
我怀疑(来自像 this 和其他地方的 cmets)使用迭代器可以让 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
关键字,gcc 对模板函数的优化通常比 vanilla C 函数更积极(这与 gcc 需要处理 vanilla C 函数的函数指针有关,但与模板无关函数)。
-
在为各种排序函数计时时,我注意到调用排序函数的上下文(即周围代码)对性能有很大影响,这可能是由于函数被内联然后优化。例如,如果程序足够简单,那么传递排序函数指针和传递迭代器之间的性能通常不会有太大差异。否则,使用迭代器通常会带来明显更好的性能,并且永远不会(至少以我目前的经验)任何明显更差的性能。我怀疑这可能是因为 g++ 可以全局优化足够简单的代码。
【讨论】:
【参考方案17】:我相信你的问题有两个部分。
首先是确定最优算法。这是通过循环遍历每个可能的排序(没有那么多)来完成的,这允许您计算比较和交换的精确最小值、最大值、平均值和标准偏差。也有一个或两个亚军。 二是优化算法。可以做很多事情来将教科书代码示例转换为平均和精益的现实生活算法。如果您意识到算法无法优化到所需的程度,请尝试获得亚军。我不会太担心清空管道(假设当前是 x86):分支预测已经走了很长一段路。我要担心的是确保代码和数据分别适合一个缓存行(可能两个用于代码)。一旦获取延迟非常低,这将弥补任何停顿。这也意味着您的内部循环可能是十条左右的指令,这是正确的(我的排序算法中有两个不同的内部循环,它们分别是 10 条指令/22 字节和 9/22 长)。假设代码不包含任何 div,您可以确定它会非常快。
【讨论】:
我不确定如何理解您的回答。首先,我完全不明白您提出的是什么算法?如果您必须遍历 720 个可能的排序(现有答案需要的周期远少于 720 个),它如何才能达到最佳效果。如果您有随机输入,我无法想象(即使在理论上)分支预测如何表现得比 50-50 更好,除非它根本不关心输入数据。此外,已经提出的大多数好的解决方案可能已经完全在缓存中处理数据和代码。但也许我完全误解了你的答案。介意显示一些代码吗? 我的意思是,6 个整数只有 720(6!)种不同的组合,通过候选算法运行所有这些组合,您可以确定很多事情,正如我提到的 - 这是理论上的部分。实际部分是微调该算法以在尽可能少的时钟周期内运行。我对 6 个整数进行排序的起点是 1、4 间隙 shellsort。 4 间隙为在 1 间隙中进行良好的分支预测铺平了道路。 6 的 1、4 间隙 shellsort!独特的组合(以 012345 开始,以 543210 结束)将具有 7 次比较和 0 次交换的最佳情况,以及 14 次比较和 10 次交换的最差情况。平均情况是大约 11.14 次比较和 6 次交换。 我没有得到“常规随机分布”——我正在做的是测试每个可能的组合并确定最小/平均/最大统计数据。 Shellsort 是一系列递减递增的插入排序,因此最终递增 - 1 - 比在纯插入排序中单独执行时所做的工作要少得多。至于时钟计数,我的算法平均需要 406 个时钟周期,这包括收集统计数据和对实际排序例程进行两次调用——每个间隔调用一次。这是在 Athlon M300 移动设备上,编译器 OpenWatcom。 “正则随机分布”是指被排序的实际数据的每个组合可能不具有相等的概率。如果每个组合的概率不相等,那么您的统计数据就会被破坏,因为平均值需要考虑给定分布可能发生的次数。对于时钟计数,如果您尝试任何其他此类实现(上面提供的链接)并在您的测试系统上运行它,我们将有一个比较基础,看看您选择的执行情况如何。【参考方案18】:我知道这是一个老问题。
但我只是写了一种不同的解决方案,我想分享。 只使用嵌套的 MIN MAX,
它并不快,因为它每个使用 114 个, 可以很简单地把它减少到 75 -> pastebin
但它不再是纯粹的最小最大。
使用 AVX 一次对多个整数做 min/max 可能有效
PMINSW reference
#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;
【讨论】:
总是很高兴看到新的解决方案。看起来可以进行一些简单的优化。最后,它可能与排序网络没有太大区别。 是的,MIN 和 MAX 的数量可能会减少,例如 MIN(AB, CD) 重复几次,但我认为很难减少它们。我添加了你的测试用例。 pmin/maxsw 对压缩的 16 位有符号整数 (int16_t
) 进行操作。但是您的 C 函数声称它对 int
的数组进行排序(在所有支持 asm
语法的 C 实现中都是 32 位的)。您是否仅使用高半部分只有 0 的小正整数对其进行了测试?这将起作用......对于int
,您需要SSE4.1 pmin/maxsd
(d = dword)。 felixcloutier.com/x86/pminsd:pminsq 或 pminusd
对应 uint32_t
。【参考方案19】:
我想我会尝试一个展开的Ford-Johnson merge-insertion sort,它实现了尽可能少的比较次数 (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");
【讨论】:
进行最少比较次数并使用它来选择正确的变量排序的想法也是排名顺序的基础。看起来如果避免交换很好,拥有 10 个分支和 720 个代码路径并不便宜。 @kriss 它看起来有些相似,但是,我认为基于排名顺序的解决方案不会进行最少数量的比较,是吗?看起来其中一个进行了 25 次比较,另一个进行了 15 次比较。排名顺序末尾的分配也通过间接方式进行。当然,排名顺序无论如何都会获胜,但我想知道我的方法是否会在未来拥有更多指令缓存或其他资源的机器上获胜。 分支在实现为跳转时可能是成本最高的 CPU 功能,因为它会清空所有缓存和预期的执行管道。我看不到任何可以使它变得便宜的演变,尤其是具有 720 个独特的代码路径。单个测试可能很便宜,因为它可以作为条件赋值实现无分支。 rank order 的核心思想是执行测试,但实际上没有分支。这里的麻烦可能是条件分支对每个最小测试的跟进。但我不知道如何避免这种情况并尽量减少比较。 @kriss 我想到的“未来机器”场景就是这样:en.wikipedia.org/wiki/Speculative_execution#Eager_execution。 “资源无限,急切执行……理论上可以提供与完美分支预测相同的性能”。 我明白,但我不相信它至少在硬件层面上的实际可行性。当预测失败时,即使是分支预测今天也没有效率。当然,我们可以想象在相同的代码上运行 720 个处理器,并且只有一个处理器保持结果,但是要花费如此多的资源,我们必须想象一个用例,其中任何微小的速度改进都比使用的任何资源更重要。而且选择正确的结果的成本非常低。【参考方案20】:尝试“合并排序列表”排序。 :) 使用两个数组。对于小型和大型阵列来说最快。
如果你连接,你只检查插入位置。您不需要比较的其他较大值 (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代码
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;
【讨论】:
【参考方案21】:也许我迟到了,但至少我的贡献是一种新方法。
代码真的应该内联 就算内联了,分支也太多了 分析部分基本上是 O(N(N-1)),对于 N=6 来说似乎没问题 如果swap
的成本更高 代码会更有效(而不是compare
的成本)
我相信静态函数是内联的。
该方法与rank-sort有关
使用相对等级(偏移量)代替等级。
任何排列组中每个循环的秩和为零。
不是SWAP()
ing 两个元素,而是循环,只需要一个临时和一个(寄存器->寄存器)交换(新
更新:修改了一点代码,有些人使用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);
【讨论】:
看起来像冒泡排序。可能是最慢实现的一个很好的竞争者,但是知道在代码上工作是否会产生很大的不同仍然是有一些兴趣的。请将您的代码与其他代码格式相同,以便我们在其上运行基准测试。 @kriss en.wikipedia.org/wiki/Permutation_group 当然不是冒泡排序:代码检测给定排列中的循环,并遍历这些循环,将每个元素放在其最终位置。最终的wsort6()
函数具有正确的接口。
@joop:我的错,确实没有冒泡排序。话虽如此,我仍然希望代码比任何其他当前实现都要糟糕得多。顺便说一下,排名顺序解决方案在交换次数方面是最佳的,因为它直接找到每个项目的最终位置。当我们删除所有排序数字都不同的假设时,也不清楚walksort是否有效。为了对代码进行基准测试,我们应该跟踪代码。另外,由于我通常在 C++ 编译器上进行编译,因此代码将无法工作,因为 OP 将变量称为“new”(这会破坏语法突出显示)。
该方法非常接近排名顺序,只有最后的分配完成 in place 。除了排名o1..o5
,不需要第二个临时e[6]
数组。并且:在 C++ 编译器上编译 C 代码,并指责代码?
您的代码缩进肯定是别的东西(例如,尝试获取 indent(1) 来生成它):您从哪里得到的?【参考方案22】:
//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
【讨论】:
无论速度如何,您确定它有效吗?在蛮力排序中,您的循环是可疑的。在我看来,如果我们的排序值为零,它们将不起作用。 t[6] 数组初始化为 0x0。因此,无论在何处以及是否写入 0x0 值的密钥都无关紧要。【参考方案23】:好吧,如果它只有 6 个元素,并且您可以利用并行性、希望最小化条件分支等。为什么不生成所有组合并测试顺序?我敢冒险,在某些架构中,它可能非常快(只要您预先分配了内存)
【讨论】:
有 720 个订单,快速版本远低于 100 个周期。即使可以利用大规模并行性,在如此小的时间范围内,创建和同步线程的成本也可能超过仅在一个核心上对数组进行排序的成本。【参考方案24】:使用 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;
【讨论】:
用例与问题的初始上下文略有不同。对于固定长度的排序,细节很重要,计算交换的 cmp 是不够的。如果它根本不是会消耗时间的实际排序,而是在初始化中调用 typeof() 的完全不同的光,我什至不会感到惊讶。我不知道如何使用 javascript 执行实际的时钟时间测量。也许与节点?以上是关于最快的固定长度 6 int 数组的主要内容,如果未能解决你的问题,请参考以下文章