x86-16-Maschinencode (BubbleSort int8_t), 20 bis 19 Byte
x86-64 / 32-Maschinencode (JumpDownSort) 21 19 Bytes
Änderungsprotokoll:
Vielen Dank an @ ped7g für die lodsb
/ cmp [si],al
-Idee und das Zusammenfügen mit einem Zeigerinkrement / -Reset, das ich mir angesehen habe. Wir brauchen nicht al
/ ah
lassen uns fast den gleichen Code für größere ganze Zahlen verwenden.
Neuer (aber verwandter) Algorithmus, viele Implementierungsänderungen: Bubbly SelectionSort ermöglicht eine kleinere x86-64-Implementierung für Bytes oder Dwords; Break-Even auf x86-16 (Bytes oder Wörter). Vermeidet auch den Fehler bei Größe = 1, den mein BubbleSort hat. Siehe unten.
Es stellt sich heraus, dass meine Bubbly Selection Sort mit Swaps jedes Mal, wenn Sie eine neue Min finden, bereits ein bekannter Algorithmus ist, JumpDown Sort. Es wird in Bubble Sort: Eine archäologische algorithmische Analyse erwähnt (dh wie Bubble Sort trotz Saugen populär wurde).
Sortiert 8-Bit-Ganzzahlen mit Vorzeichen direkt . (Unsigned hat die gleiche Codegröße, ändern Sie einfach die jge
in a jae
). Duplikate sind kein Problem. Wir tauschen mit einer 16-Bit-Drehung um 8 (mit einem Speicherziel).
Bubble Sort ist der Hammer für die Leistung , aber ich habe gelesen, dass es eines der kleinsten ist, das in Maschinencode implementiert werden kann. Dies scheint insbesondere dann der Fall zu sein, wenn es spezielle Tricks zum Austauschen benachbarter Elemente gibt. Dies ist so ziemlich der einzige Vorteil, aber manchmal (in realen eingebetteten Systemen) reicht dieser Vorteil aus, um ihn für sehr kurze Listen zu verwenden.
Ich habe die vorzeitige Kündigung für keine Swaps ausgelassen . Ich habe die "optimierte" BubbleSort-Schleife von Wikipedia verwendet, die es vermeidet, die letzten n − 1
Elemente zu betrachten, wenn sie zum n
-ten Mal ausgeführt werden, sodass der Zähler der äußeren Schleife die Obergrenze für die innere Schleife ist.
NASM-Auflistung ( nasm -l /dev/stdout
) oder einfache Quelle
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.
push / pop von cx
um die innere Schleife herum bedeutet, dass sie mit cx
= outer_cx bis auf 0 läuft .
Beachten Sie, dass rol r/m16, imm8
es sich nicht um einen 8086-Befehl handelt, der später hinzugefügt wurde (186 oder 286), aber es wird nicht versucht, 8086-Code zu sein, sondern nur 16-Bit x86. Wenn SSE4.1phminposuw
helfen würde, würde ich es verwenden.
Eine 32-Bit-Version davon (die immer noch mit 8-Bit-Ganzzahlen, aber mit 32-Bit-Zeigern / Zählern arbeitet) hat eine Größe von 20 Byte (Präfix für Operandengröße aktiviert rol word [esi-1], 8
).
Bug: size = 1 wird als size = 65536 behandelt, weil nichts uns davon abhält, das äußere do / while mit cx = 0 zu betreten. (Sie würden normalerweise dafür verwenden jcxz
.) Aber zum Glück ist die 19-Byte-JumpDown-Sortierung 19 Byte und hat dieses Problem nicht.
Original x86-16 20-Byte-Version (ohne die Idee von Ped7g). Aus Platzgründen ausgelassen, finden Sie im Bearbeitungsverlauf eine Beschreibung.
Performance
Teilweise überlappendes Speichern / Neuladen (beim Rotieren des Speicherziels) führt bei modernen x86-CPUs (außer Atom in der richtigen Reihenfolge) zu einem Stillstand der Speicherweiterleitung. Wenn ein hoher Wert nach oben sprudelt, ist diese zusätzliche Latenzzeit Teil einer durch eine Schleife übertragenen Abhängigkeitskette. Das Speichern / Neuladen ist an erster Stelle scheiße (wie die Speicherweiterleitungsverzögerung von 5 Zyklen bei Haswell), aber ein Weiterleitungsstopp bringt es auf mehr als 13 Zyklen. Bei der Ausführung außerhalb der Reihenfolge kann dies nur schwer ausgeblendet werden.
Siehe auch: Stapelüberlauf: Blasensortierung zum Sortieren von Zeichenfolgen für eine Version mit einer ähnlichen Implementierung, jedoch mit einem frühen Aus, wenn keine Auslagerungen erforderlich sind. Es verwendet xchg al, ah
/ mov [si], ax
zum Austauschen, was 1 Byte länger ist und bei einigen CPUs einen Teilregister-Stillstand verursacht. (Aber es kann immer noch besser sein, als den Speicher zu drehen, der den Wert erneut laden muss). Mein Kommentar dort hat einige Vorschläge ...
x86-64 / x86-32 JumpDown-Sortierung, 19 Byte (sortiert nach int32_t)
Aufrufbar von C unter Verwendung der x86-64-System-V-Aufrufkonvention als
int bubblyselectionsort_int32(int dummy, int *array, int dummy, unsigned long size);
(Rückgabewert = max (array [])).
Dies ist https://en.wikipedia.org/wiki/Selection_sort , aber anstatt sich an die Position des Elements min zu erinnern, tauschen Sie den aktuellen Kandidaten in das Array . Wenn Sie die minimale (unsortierte_Region) gefunden haben, speichern Sie sie wie bei der normalen Auswahlsortierung am Ende der sortierten Region. Dies vergrößert den sortierten Bereich um eins. (Zeigen Sie im Code rsi
auf eins nach dem Ende des sortierten Bereichs, lodsd
schieben Sie ihn vor und mov [rsi-4], eax
speichern Sie die Min zurück.)
Der Name Jump Down Sort wird in Bubble Sort: An Archaeological Algorithmic Analysis verwendet . Ich denke, meine Sortierung ist wirklich eine Jump Up-Sortierung, da hohe Elemente nach oben springen und den unteren Teil sortiert lassen, nicht das Ende.
Dieses Austauschdesign führt dazu, dass der unsortierte Teil des Arrays größtenteils in umgekehrter Reihenfolge abläuft, was später zu vielen Auslagerungen führt. (Da Sie mit einem großen Kandidaten beginnen und immer niedrigere Kandidaten sehen, tauschen Sie weiter.) Ich habe es "sprudelnd" genannt, obwohl es Elemente in die andere Richtung bewegt. Die Art und Weise, wie Elemente verschoben werden, ist auch ein bisschen wie beim Einfügen in Rückwärtsrichtung. Um es in Aktion zu sehen, verwenden Sie GDBs display (int[12])buf
, setzen Sie einen Haltepunkt für die innere loop
Anweisung und verwenden Sie c
(continue). Drücken Sie die Eingabetaste, um den Vorgang zu wiederholen. (Der Befehl "display" veranlasst GDB, jedes Mal, wenn wir den Haltepunkt erreichen, den gesamten Array-Status zu drucken.)
xchg
with mem hat ein implizites lock
Präfix, das dies besonders langsam macht. Vermutlich um eine Größenordnung langsamer als ein effizienter Load / Store-Swap; xchg m,r
beträgt eins pro 23c Durchsatz bei Skylake, aber Laden / Speichern / Verschieben mit einem tmp-Register für einen effizienten Tausch (reg, mem) kann ein Element pro Takt verschieben. Auf einer AMD-CPU, bei der die loop
Anweisung schnell ist und die innere Schleife nicht so stark beeinträchtigt, ist dies möglicherweise ein schlechteres Verhältnis. Verzweigungsausfälle sind jedoch immer noch ein großer Engpass, da Auslagerungen häufig auftreten (und häufiger auftreten, wenn die unsortierte Region kleiner wird) ).
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
Gleiche Code - Größe für int8_t
: Verwendung lodsb
/ scasb
, AL
und ändern Sie die [rsi/rdi-4]
zu-1
. Derselbe Maschinencode funktioniert im 32-Bit-Modus für 8/32-Bit-Elemente. Der 16-Bit-Modus für 8/16-Bit-Elemente muss mit geänderten Offsets neu erstellt werden (und die 16-Bit-Adressierungsmodi verwenden eine andere Codierung). Aber immer noch 19 Bytes für alle.
Es vermeidet die Initiale, dec ecx
indem es mit dem Element vergleicht, das es gerade geladen hat, bevor es weitergeht. Bei der letzten Iteration der äußeren Schleife lädt sie das letzte Element, prüft, ob es kleiner ist als sie selbst und ist dann fertig. Dies ermöglicht es, mit Größe = 1 zu arbeiten, wo mein BubbleSort fehlschlägt (behandelt es als Größe = 65536).
Ich habe diese Version (in GDB) mit dem folgenden Aufrufer getestet: Online ausprobieren ! . Sie können dies auf TIO ausführen, aber natürlich keinen Debugger oder Drucker. Das, das _start
es aufruft, wird mit exit-status = größtes Element = 99 beendet, sodass Sie sehen können, dass es funktioniert.
[7 2 4 1] -> [4 2 3 1]
. Kann sich die CSV-Liste auch in eckigen Klammern befinden? Das spezielle Eingabeformat eignet sich auch sehr gut für einige Sprachen und schlecht für andere. Dadurch wird das Parsen von Eingaben für einige Übermittlungen zu einem großen Teil und für andere zu einem unnötigen Teil.