Welke toewijzing is sneller?
============================
2022-02-13
Ik check bijna dagelijks de site [Ada Planet [1]. Dit is
een verzamelplek voor allerhande posts over Ada. Ada Planet
zuigt ook alle Ada-gerelateerde vragen van Stack Overflow op
en een paar weken geleden kwam er een interessante vraag [2]
voorbij. Deze betrof de overhead bij het declareren en
toewijzen (assignment) van variabelen.
Als je een subprogram hebt - een functie of procedure - dan
kun je er enerzijds voor kiezen om een variabele in het
declaratiedeel van de subprogram zelf te initialiseren:
procedure Foo (Bar : Integer) is
Another_Bar : Integer := Bar;
begin
...
end Foo;
Anderzijds is het ook mogelijk om dit te doen in de lijst
met statements die na de begin komt:
procedure Foo2 (Bar : Integer) is
Another_Bar : Integer;
begin
Another_Bar := Bar;
...
end Foo;
De vraagsteller wou weten of beide voorbeelden dezelfde
assembly-instructies opleveren en dus gelijkwaardig zijn qua
performance. Chris, de antwoorder, geeft de juiste respons:
op basis van de specificatie van de taal Ada is er geen
reden waarom beide voorbeelden zouden verschillen in
performance. Dat voelt weliswaar intuïtief correct, maar is
het niet veel eenvoudiger om gewoon naar de assembly te
kijken die GNAT produceert bij het compileren?
Dit is gelukkig erg eenvoudig. Je kunt enerzijds aan GCC
vragen of hij zo vriendelijk wil zijn om ook een bestand met
assembly voor je te genereren (met de -S flag), maar nog
makkelijker kun je gebruikmaken van de geweldige Compiler
Explorer op godbolt.org [3]. Deze site stelt je in staat om
broncode in een keur aan talen in te voeren, een compiler en
doelplatform te selecteren en dan laat hij je zien welke
assemblycode dit oplevert.
Het eerste voorbeeld, met de toewijziging in het
declaratieve deel van de procedure levert de volgende code
op:
_ada_foo:
push rbp #
mov rbp, rsp #,
mov DWORD PTR [rbp-20], edi # bar, bar
mov eax, DWORD PTR [rbp-20] # tmp82, bar
mov DWORD PTR [rbp-4], eax # another_bar, tmp82
nop
nop
pop rbp #
ret
Hierbij heb ik de "..." moeten vervangen door de statement
null;, omdat het in Ada niet is toegestaan om de lijst met
statements in de body van een subprogram leeg te laten.
Het tweede voorbeeld levert in Godbolt deze assembly op:
_ada_foo:
push rbp #
mov rbp, rsp #,
mov DWORD PTR [rbp-20], edi # bar, bar
mov eax, DWORD PTR [rbp-20] # tmp82, bar
mov DWORD PTR [rbp-4], eax # another_bar, tmp82
nop
pop rbp #
ret
Deze zijn dus vrijwel hetzelfde. Het enige verschil is dat
de eerste een extra nop-instructie heeft. Deze nop staat
voor "No Operation" die de processor verteld dat hij even
niets moet doen. Deze extra instructie komt voort uit het
feit dat we null; moeten gebruiken na de begin. Een nop
instructie kost in de meeste processoren 1 klokcyclus, maar
dat kan wel verschillen per familie.
De reactie van Chris op Stack Overflow is een aardig
voorbeeld van waarom ik niet zo'n fan ben van die site. Er
wordt in dit geval weliswaar een goed antwoord gegeven, en
het is mooi om te zien hoe programmeurs zo behulpzaam zijn,
maar om mijn favoriete programmeur Casey Muratori te citeren
is het beter om terug te gaan naar de "first principles".
Wat voor machine-instructies worden er precies door de
compiler gebakken? Gaat het hier om een taalconstruct voor
het gemak van de programmeur of dient het een onderliggend
doel dat zich afspeelt in de krochten van het
transistorlabyrint?
In 2020 had ik na het begin van de eerste coronagolf de tijd
om mijn kennis van assembly weer wat op te poetsen. De
laatste keer dat ik serieus handmatig ASM had geschreven was
nog in de 16-bit MS-DOS-dagen. Op de universiteit kwam het
af en toe voorbij, maar als informatici hadden we de luxe om
over het algemeen hoog boven zulke aardse zaken te zweven.
Nu ik weer zelf assembly heb geschreven en kennis heb
opgedaan over wat de 64-bits instructieset allemaal voor
leuks heeft toegevoegd ben ik deze low level taal opnieuw
gaan waarderen.
Het is gemakkelijk om de waanzinnige kracht van een
processor uit het oog te verliezen als je werkt in een
hogere programmeertaal. Wat dat betreft vind ik Ada ook wel
heel aardig, want als je wil is het nog steeds mogelijk om
inline assembly te gebruiken. Daarnaast laat dit voorbeeld
wel zien dat het vaak vrij goed mogelijk is om van je
Ada-code terug te redeneren naar de machine-instructies die
dit oplevert. Als je echter serieus bezig gaat met
performance en ook debuggers ten volle wil gebruiken dan is
het soms nuttig om een duik te nemen in het bad met
assembly-instructies en Godbolt is daarbij een fantastische
duikbril.
Hyperlinks:
[1]:
https://www.laeran.pl/adaplanet/i/
[2]:
https://stackoverflow.com/questions/70802970/ada-declaration-assignment-overhead
[3]:
https://godbolt.org/
-----------------------------------------------------------
Tags: ada