x86-16机器码(BubbleSort int8_t),20 19字节
x86-64 / 32机器码(JumpDownSort)21 19字节
变更日志:
感谢@ ped7g的lodsb
/ cmp [si],al
想法,并将其与我一直在看的指针增量/重置放在一起。不需要al
/ ah
让我们对较大的整数使用几乎相同的代码。
新的(但相关的)算法,实现方式有很多变化:Bubbly SelectionSort允许使用较小的x86-64实现字节或双字;在x86-16上达到收支平衡(字节或字)。还避免了我的BubbleSort出现的size = 1错误。见下文。
事实证明,每次找到新的最小值时,带有交换的“气泡选择排序”已经是一种已知的算法JumpDown排序。在《泡泡排序:考古学算法分析》中提到了这一点(即,泡泡排序如何在吮吸时变得很流行)。
对8位有符号整数进行原位排序。(无符号是相同的代码大小,只需将更jge
改为即可jae
)。重复不是问题。我们使用16位旋转8进行交换(带有存储目标)。
Bubble Sort会降低性能,但是我已经读到它是在机器代码中最小的实现之一。当存在用于交换相邻元素的特殊技巧时,这似乎尤其正确。这几乎是它的唯一优势,但是有时(在现实生活中的嵌入式系统中)这足以将其用于非常短的列表。
我忽略了没有交换的提前终止。我使用了Wikipedia的“优化” BubbleSort循环,该循环避免在第-次n − 1
运行时查看最后一个项目n
,因此外循环计数器是内循环的上限。
NASM列表(nasm -l /dev/stdout
),或纯来源
2 address 16-bit bubblesort16_v2:
3 machine ;; inputs: pointer in ds:si, size in in cx
4 code ;; requires: DF=0 (cld)
5 bytes ;; clobbers: al, cx=0
6
7 00000000 49 dec cx ; cx = max valid index. (Inner loop stops 1 before cx, because it loads i and i+1).
8 .outer: ; do{
9 00000001 51 push cx ; cx = inner loop counter = i=max_unsorted_idx
10 .inner: ; do{
11 00000002 AC lodsb ; al = *p++
12 00000003 3804 cmp [si],al ; compare with *p (new one)
13 00000005 7D04 jge .noswap
14 00000007 C144FF08 rol word [si-1], 8 ; swap
15 .noswap:
16 0000000B E2F5 loop .inner ; } while(i < size);
17 0000000D 59 pop cx ; cx = outer loop counter
18 0000000E 29CE sub si,cx ; reset pointer to start of array
19 00000010 E2EF loop .outer ; } while(--size);
20 00000012 C3 ret
22 00000013 size = 0x13 = 19 bytes.
cx
围绕内部循环的push / pop 表示它以cx
= external_cx降至0运行。
请注意,这 rol r/m16, imm8
不是8086指令,它是在以后添加的(186或286),但这并不是要成为8086代码,而只是16位x86。如果SSE4.1 phminposuw
有帮助,我会使用它。
此的32位版本(仍可在8位整数上运行,但具有32位指针/计数器)为20字节(上的操作数大小前缀rol word [esi-1], 8
)
错误:将size = 1视为size = 65536,因为没有什么会阻止我们在cx = 0时输入外部do /。(您通常会使用jcxz
它。)但是幸运的是19字节的JumpDown排序是19字节,没有这个问题。
原始的x86-16 20字节版本(没有Ped7g的想法)。省略以节省空间,请参阅编辑历史记录以及说明。
性能
部分重叠的存储/重载(在内存目标旋转中)导致现代x86 CPU(有序Atom除外)上的存储转发停顿。当一个高的值向上冒泡时,这种额外的延迟是循环携带的依赖链的一部分。首先,存储/重载很烂(例如Haswell上的5个周期的存储转发延迟),但是转发停顿将其增加到13个周期以上。乱序执行将很难隐藏它。
另请参阅:堆栈溢出:冒泡排序,用于以类似的实现对此版本的字符串进行排序,但是不需要交换时可以提前淘汰。它使用xchg al, ah
/ mov [si], ax
进行交换,它长1个字节,并在某些CPU上导致部分寄存器停顿。(但是它可能仍然比memory-dst旋转更好,后者需要再次加载该值)。我的评论有一些建议...
x86-64 / x86-32 JumpDown排序,19个字节(对int32_t的排序)
可以使用x86-64 System V调用约定从C调用
int bubblyselectionsort_int32(int dummy, int *array, int dummy, unsigned long size);
(返回值= max(array []))。
这是https://en.wikipedia.org/wiki/Selection_sort,但与其记住min元素的位置,不如将当前候选位置交换到array中。找到min(unsorted_region)后,将其存储到已排序区域的末尾,就像普通的Selection Sort一样。这样可使排序区域增加一个。(在代码中,rsi
指向已排序区域末尾的一个;lodsd
将其前进并将mov [rsi-4], eax
最小值存储回去。)
在泡沫排序中使用名称Jump Down Sort :考古学算法分析。我猜我的排序实际上是一个Jump Up排序,因为高元素向上跳,而底部排序而不是末尾排序。
这种交换设计导致数组的未排序部分最终以反向排序的顺序结束,此后导致大量交换。(因为您从一个较大的候选对象开始,并且不断看到越来越少的候选对象,所以您一直在交换。)我将其称为“气泡状”,即使它使元素向另一个方向移动。它移动元素的方式也有点像向后插入排序。要观看它的运行情况,请使用GDB的display (int[12])buf
,在内部loop
指令上设置一个断点,然后使用c
(继续)。按回车重复。(“ display”命令使GDB每次遇到断点时都打印整个阵列状态)。
xchg
使用mem有一个隐式lock
前缀,这会使这特别慢。可能比有效的加载/存储交换慢大约一个数量级;xchg m,r
在Skylake上,每23c吞吐量是一个,但是使用tmp reg进行加载/存储/移动以进行有效的swap(reg,mem)可以使每个时钟移动一个元素。在loop
指令速度快并且不会对内部循环造成太大瓶颈的AMD CPU上,这可能是一个更糟糕的比率,但是分支未命中仍然是一个大瓶颈,因为交换很常见(随着未排序区域变小,这种情况会变得更加普遍。 )。
2 Address ;; hybrib Bubble Selection sort
3 machine bubblyselectionsort_int32: ;; working, 19 bytes. Same size for int32 or int8
4 code ;; input: pointer in rsi, count in rcx
5 bytes ;; returns: eax = max
6
7 ;dec ecx ; we avoid this by doing edi=esi *before* lodsb, so we do redundant compares
8 ; This lets us (re)enter the inner loop even for 1 element remaining.
9 .outer:
10 ; rsi pointing at the element that will receive min([rsi]..[rsi+rcx])
11 00000000 56 push rsi
12 00000001 5F pop rdi
13 ;mov edi, esi ; rdi = min-search pointer
14 00000002 AD lodsd
16 00000003 51 push rcx ; rcx = inner counter
17 .inner: ; do {
18 ; rdi points at next element to check
19 ; eax = candidate min
20 00000004 AF scasd ; cmp eax, [rdi++]
21 00000005 7E03 jle .notmin
22 00000007 8747FC xchg [rdi-4], eax ; exchange with new min.
23 .notmin:
24 0000000A E2F8 loop .inner ; } while(--inner);
26 ; swap min-position with sorted position
27 ; eax = min. If it's not [rsi-4], then [rsi-4] was exchanged into the array somewhere
28 0000000C 8946FC mov [rsi-4], eax
29 0000000F 59 pop rcx ; rcx = outer loop counter = unsorted elements left
30 00000010 E2EE loop .outer ; } while(--unsorted);
32 00000012 C3 ret
34 00000013 13 .size: db $ - bubblyselectionsort_int32
0x13 = 19 bytes long
相同的代码大小int8_t
:使用lodsb
/ scasb
,AL
和变化[rsi/rdi-4]
来-1
。相同的机器代码在32位模式下可用于8/32位元素。需要重新构建8/16位元素的16位模式,并更改偏移量(并且16位寻址模式使用不同的编码)。但仍然是19个字节。
dec ecx
通过与继续前进之前刚加载的元素进行比较,它避免了初始值。在外循环的最后一次迭代中,它加载最后一个元素,检查它是否小于自身,然后完成。这使它可以与size = 1一起工作,而我的BubbleSort失败(将其处理为size = 65536)。
我使用以下调用者测试了此版本(在GDB中): 在线试用!。您可以在TIO上运行它,但是当然没有调试器或打印程序。不过,_start
调用它的退出状态是exit-status =最大元素= 99,因此您可以看到它的工作原理。
[7 2 4 1] -> [4 2 3 1]
。另外,CSV列表可以放在方括号内吗?此外,特定的输入格式非常适合某些语言,而对其他语言则不利。这使得输入解析对于某些提交来说很重要,而对于其他提交则不需要。