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