code machine x86-16 (BubbleSort int8_t), 20 19 octets
code machine x86-64 / 32 (JumpDownSort) 21 19 octets
Journal des modifications:
Merci à @ ped7g pour l' idée lodsb
/ cmp [si],al
, et à mettre cela ensemble avec une augmentation / réinitialisation du pointeur que je regardais. Ne pas avoir besoin al
/ ah
permet d'utiliser presque le même code pour les entiers plus grands.
Nouvel algorithme (mais lié), de nombreux changements d'implémentation: Bubbly SelectionSort permet une implémentation x86-64 plus petite pour les octets ou les mots-clés; seuil de rentabilité sur x86-16 (octets ou mots). Évite également le bug sur size = 1 que mon BubbleSort a. Voir ci-dessous.
Il s'avère que mon Bubbly Selection Sort avec des swaps chaque fois que vous trouvez un nouveau min est déjà un algorithme connu, JumpDown Sort. Il est mentionné dans Bubble Sort: une analyse algorithmique archéologique (c'est-à-dire comment Bubble Sort est devenu populaire malgré la succion).
Trie les entiers signés 8 bits sur place . (Non signé a la même taille de code, changez simplement le jge
en a jae
). Les doublons ne sont pas un problème. Nous échangeons en utilisant une rotation de 16 bits par 8 (avec une destination mémoire).
Bubble Sort craint pour les performances , mais j'ai lu que c'est l'un des plus petits à implémenter dans le code machine. Cela semble particulièrement vrai lorsqu'il existe des astuces spéciales pour échanger des éléments adjacents. C'est à peu près son seul avantage, mais parfois (dans les systèmes embarqués réels), c'est assez d'avantage pour l'utiliser pour des listes très courtes.
J'ai omis la résiliation anticipée sur aucun échange . J'ai utilisé la boucle BubbleSort «optimisée» de Wikipédia qui évite de regarder les derniers n − 1
éléments lors de l'exécution pour la n
cinquième fois, donc le compteur de la boucle externe est la limite supérieure de la boucle interne.
Liste NASM ( nasm -l /dev/stdout
) ou source simple
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 cx
autour de la boucle interne signifie qu'il s'exécute avec cx
= externe_cx jusqu'à 0.
Notez qu'il rol r/m16, imm8
ne s'agit pas d'une instruction 8086, elle a été ajoutée plus tard (186 ou 286), mais cela n'essaie pas d'être du code 8086, juste du x86 16 bits. Si SSE4.1 phminposuw
pouvait aider, je l'utiliserais.
Une version 32 bits de ceci (fonctionnant toujours sur des entiers 8 bits mais avec des pointeurs / compteurs 32 bits) est de 20 octets (préfixe de taille d'opérande activé rol word [esi-1], 8
)
Bug: size = 1 est traité comme size = 65536, car rien ne nous empêche d'entrer dans le do / while externe avec cx = 0. (Vous l'utiliseriez normalement jcxz
pour cela.) Mais heureusement, le tri JumpDown de 19 octets fait 19 octets et n'a pas ce problème.
Version originale x86-16 de 20 octets (sans idée de Ped7g). Omis pour économiser de l'espace, consultez l' historique des modifications avec une description.
Performance
Le stockage / rechargement partiellement chevauchant (dans la rotation de destination de la mémoire) provoque un blocage de transfert de magasin sur les processeurs x86 modernes (sauf Atom dans l'ordre). Lorsqu'une valeur élevée se propage vers le haut, cette latence supplémentaire fait partie d'une chaîne de dépendance portée par la boucle. Le stockage / rechargement aspire en premier lieu (comme la latence de transfert de magasin à 5 cycles sur Haswell), mais un blocage de transfert le ramène à plus ou moins 13 cycles. Une exécution dans le désordre aura du mal à le masquer.
Voir aussi: Stack Overflow: bubble sort pour trier la chaîne pour une version de ceci avec une implémentation similaire, mais avec un early-out quand aucun swap n'est nécessaire. Il utilise xchg al, ah
/ mov [si], ax
pour l'échange, qui est de 1 octet de plus et provoque un blocage de registre partiel sur certains processeurs. (Mais cela peut être encore mieux que la rotation de mémoire-dst, qui doit charger à nouveau la valeur). Mon commentaire a quelques suggestions ...
Tri JumpDown x86-64 / x86-32, 19 octets (tri int32_t)
Appelable depuis C en utilisant la convention d'appel System V x86-64 as
int bubblyselectionsort_int32(int dummy, int *array, int dummy, unsigned long size);
(valeur de retour = max (tableau [])).
Il s'agit de https://en.wikipedia.org/wiki/Selection_sort , mais au lieu de se souvenir de la position de l'élément min, permutez le candidat actuel dans le tableau . Une fois que vous avez trouvé le min (unsorted_region), stockez-le à la fin de la région triée, comme le tri de sélection normal. Cela agrandit la région triée d'une unité. (Dans le code, rsi
pointe vers un après la fin de la région triée; l' lodsd
avance et y mov [rsi-4], eax
stocke le min.)
Le nom Jump Down Sort est utilisé dans Bubble Sort: An Archaeological Algorithmic Analysis . Je suppose que mon tri est vraiment un tri par saut, car les éléments hauts sautent vers le haut, laissant le bas trié, pas la fin.
Cette conception d'échange conduit à la partie non triée du tableau se retrouvant principalement dans un ordre trié inversé, ce qui entraîne de nombreux échanges plus tard. (Parce que vous commencez avec un grand candidat, et continuez à voir des candidats de plus en plus bas, alors vous continuez à permuter.) Je l'ai appelé "pétillant" même s'il déplace les éléments dans l'autre sens. La façon dont il déplace les éléments est également un peu comme un tri par insertion vers l'arrière. Pour le regarder en action, utilisez les GDB display (int[12])buf
, définissez un point d'arrêt sur l' loop
instruction interne et utilisez c
(continuer). Appuyez sur retour pour répéter. (La commande "display" permet à GDB d'imprimer tout l'état du tableau à chaque fois que nous atteignons le point d'arrêt).
xchg
avec mem a un lock
préfixe implicite qui rend cela très lent. Probablement environ un ordre de grandeur plus lent qu'un échange de charge / stockage efficace; xchg m,r
est un par débit 23c sur Skylake, mais charger / stocker / déplacer avec un reg tmp pour un swap efficace (reg, mem) peut décaler un élément par horloge. Cela pourrait être un pire rapport sur un processeur AMD où l' loop
instruction est rapide et ne gênerait pas autant la boucle interne, mais les échecs de branchement seront toujours un gros goulot d'étranglement car les échanges sont courants (et deviennent plus courants à mesure que la région non triée devient plus petite). ).
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
Taille du même code pour int8_t
: utilisation lodsb
/ scasb
, AL
et changer le [rsi/rdi-4]
à -1
. Le même code machine fonctionne en mode 32 bits pour les éléments 8/32 bits. Le mode 16 bits pour les éléments 8/16 bits doit être reconstruit avec les décalages modifiés (et les modes d'adressage 16 bits utilisent un codage différent). Mais toujours 19 octets pour tous.
Il évite l'initiale dec ecx
en comparant avec l'élément qu'il vient de charger avant de continuer. Lors de la dernière itération de la boucle externe, il charge le dernier élément, vérifie s'il est inférieur à lui-même, puis c'est fait. Cela lui permet de fonctionner avec size = 1, où mon BubbleSort échoue (le traite comme size = 65536).
J'ai testé cette version (en GDB) en utilisant cet appelant: Essayez-le en ligne! . Vous pouvez l'exécuter sur TIO, mais bien sûr sans débogueur ni impression. Pourtant, celui _start
qui l'appelle quitte avec exit-status = le plus grand élément = 99, donc vous pouvez voir que cela fonctionne.
[7 2 4 1] -> [4 2 3 1]
. De plus, la liste CSV peut-elle être entre crochets? De plus, le format d'entrée spécifique est très approprié pour certaines langues et mauvais pour d'autres. Cela rend l'analyse des entrées importante pour certaines soumissions et inutile pour d'autres.