C/C++ : tisztán hatékonyabb!

Címkék

Pfeiffer Szilárd (Balasys) előadása a HWSW free! meetup-sorozat 2022. november 15-i C++ fejlesztői tematikájú állomásán hangzott el. A meetupjainkon megszokottal szemben ez egy nagyobb lélegzetvételű, 40 perces előadás. A Clean Code elvek kapcsán gyakran felmerülő kérdés, hogy az átlátható, ember által olvasható kód írása nem okoz-e hatékonyságcsökkenést futtatáskor. Az előadás keretében néhány gyakorlati példán keresztül világítok rá arra, hogy a Clean Code nem csak a fejlesztőt, de a fordító programokat is segíti a hatékonyabb kódok előállításában.

Hozzászólások

Vagy a "Színházi vegyes"-ből válogatva:

"Ebben nincs igazad, egy előadásnak minden részében tökéletesnek kell lennie, különben az egész nem ér semmit. Hadd mondjak egy példát. Meghívnak téged valahová, és a házigazdák igazán kitesznek magukért: öt asztalon terítenek, feltálalnak, velőscsontot, ötféle húst, halat, rákot, süteményt, bort, sört, francia konyakot, skót whiskyt, de – emelte fel a mutatóujját Alfonzó – az egyik asztal sarkán egy egészen kis tányérban van egy icipici darab szar. Van neked kedved az egész ebédhez?"

A Clean Code elvek kapcsán gyakran felmerülő kérdés, hogy az átlátható, ember által olvasható kód írása nem okoz-e hatékonyságcsökkenést futtatáskor.

Igen, a kódformázás hatékonyságcsökkenést okoz gépi kód szinten, mert az a sok szar tab/space/linefeed mind-mind belefordul a gépi kódba NOP-ként... :]

de nem erted... a valtozonevek elnevezese szamit (biztos, mert az elso 15 perc errol szolt), mert az hogy pl. pid meg i meg j az nem eleg beszedes, es igy a fordito sem fogja tudni, hogy mire hasznalod, bezzeg ha azt irod, hogy ezAciklusValtozom akkor mindjart megvilagosodik! :)

meg hogy a szamokat ne inline ird hanem const-kent! attol is biztos lassabb lesz a gepi kod...

Ó, nahát, hogy ez nekem nem jutott eszembe, pedig tök logikus!

Hát ha a fordító elég hülye ahhoz, hogy a const-ként deklarált értékeket ne immediate címzéssel töltse be a regiszterekbe, hanem behányja egy címke mögé a data szegmensbe és onnan olvassa be indirekt címzéssel, akkor ebben van valami. :D

hanem behányja egy címke mögé a data szegmensbe és onnan olvassa be indirekt címzéssel, 

Pedig az ARM az igy csinalja: ldr r0, [pc, #124]. Position independent modon olvassa be a .text-bol, es minden fuggveny utan ki vannak gyujtve a konstansok. Kiveve azok amik [7:0]<<... modon eloallithatoak, mert azt ket inline utasitasbol megcsinalja. 

Az ARM azt csinálja, amit mondanak neki. Lehet így is, de 32-bitig felesleges: 8 vagy 16-bites értékeket közvetlenül is betölthetsz a MOV és MOVT utasításokkal, utóbbival a felső 16 bitbe. Ergo egy 32-bites konstanst mindenféle PC-relatív címzős bűvészkedés nélkül, közvetlenül is be tudsz tölteni egy MOV-MOVT párossal (vagy az őket eredményező MOV32 wrapper-utasítással). A legtöbb konstans pedig belefér 32 bitbe.

És egyébként én csak vicceltem, hogy a fordító szándékosan csinálja ezt, amikor nem kéne; nem olyan use case-ekről volt szó, amikor muszáj. :P

Igen, Thumb-2-n lehetnek ilyen opcioid is ;) de kerdes hogymivel jar jobban az ember (latency, wait cycle, pipelining, kodsuruseg, stb). Ha egy szem Thumb(-1) utasitassal beolvastatod valahonnan tavolrol, vagosszerakod ket Thumb-2 utasitasbol de nem szakad meg a fetch pipeline egy extra wait cycle miatt. Vsz ez erosen architekturafuggo, mert pl ARMv7-en leginkabb ldr rx, [pc, #...]-t latok meg mindig, pedig ott mar van mov+movt is Thumb-2 alatt es nemcsak ldr es/vagy movs+lsls. De megsem csinal a fordito movt-ket... 

A Thumb-2 már vagy 20 éve - az ARMv6-os ARM11-ek óta (a T2-es verzióktól felfelé) - alapfelszereltség... :P (De egyébként 8-bites immediate a Thumb-1-ben is volt.)
De isoraz, ez egy poén volt; ha muszáj, muszáj, de itt a vicc az volt, hogy a konstansokat feleslegesen rakja ki valami táblába, immmediate load helyett, mert az úgy biztos gyorsabb; olvasd el, min poénkodtunk arpi_esp-pel.

int main(int argc, char *argv[])
{
    return 0x12345678;
}

C x86-64:

main:                                   # @main
        mov     eax, 305419896
        ret

C ARMv8:

main:                                   // @main
        mov     w0, #22136
        movk    w0, #4660, lsl #16
        ret

C++ x86-64:

main:                                   # @main
        mov     eax, 305419896
        ret

C++ ARMv8:

main:                                   // @main
        mov     w0, #22136
        movk    w0, #4660, lsl #16
        ret
const int x = 0x12345678;

int main(int argc, char *argv[])
{
    return x;
}

C x86-64:

main:                                   # @main
        mov     eax, 305419896
        ret
x:
        .long   305419896                       # 0x12345678

C ARMv8:

main:                                   // @main
        mov     w0, #22136
        movk    w0, #4660, lsl #16
        ret
x:
        .word   305419896                       // 0x12345678

C++ x86-64:

main:                                   # @main
        mov     eax, 305419896
        ret

C++ ARMv8:

main:                                   // @main
        mov     w0, #22136
        movk    w0, #4660, lsl #16
        ret

C esetén - ha deklarálva volt - kirakta egy táblába a konstansot, de nem használta, ugyanúgy immediate értékként töltötte be; értelme mi...? A kód az összes esetben identikus volt, persze CPU-nként.
x86-64-en:

        mov     eax, 305419896

ARMv8-on:

        mov     w0, #22136
        movk    w0, #4660, lsl #16

A fordító minden esetben CLang 16 volt.

Mindenki megnyugodott? Elengedhetjük végre ezt a viccet?

C esetén - ha deklarálva volt - kirakta egy táblába a konstansot

static const int ...

Oka: ha nem static, akkor "extern" kulcsszó mentén másik fájlból is tudtál rá hivatkozni, amit a linker megkeresett és felhasznált.
Ne feledd, a C fordító fájlonként (+include) fordít, felette áll még egy .o -kat összeboronáló linker.

Durvább dolgok vannak a mostani C fordítóban, mintsem hogy regiszterbe tegye amit lehet. Ezeket mind ki kell cseleznem, ha valami sebességtesztet írok.
Nézz rá erre: https://c.godbolt.org/z/vnYre8han

Amit tud, ki fog optimalizálni. A másik példa arról szól, hogy az iskolában tanult osztás (de binárisan) még hardverre lerakva is lassú. Ahol tudja, nem sdiv-hez nyúl.

Durvább dolgok vannak a mostani C fordítóban, mintsem hogy regiszterbe tegye amit lehet. Ezeket mind ki kell cseleznem, ha valami sebességtesztet írok.
Nézz rá erre: https://c.godbolt.org/z/vnYre8han

Én ránéztem, de én azt látom, hogy minden literált regiszterbe pakolt... (Az más kérdés, hogy előre kiszámolta, hogy mi is lesz a végső literál.)

Amit tud, ki fog optimalizálni. A másik példa arról szól, hogy az iskolában tanult osztás (de binárisan) még hardverre lerakva is lassú. Ahol tudja, nem sdiv-hez nyúl.

Ezt ha megszakadok se értem, hogy jött ide... Itt el lett sütve egy poén, hogy ha a fordító elég hülye, akkor ha nem literálban adod meg a számokat, hanem konstansként, akkor nem immediate load-ot használ, hanem bevágja egy címke mögé a data szegmensben és onnan olvasgatja be. Osztásról szó sem volt...

Úgy hoztam be példának, hogy egy normális C vagy C++ fordító, de akár a Rust fordítót is említhetem, ma már nem azt csinálja szolgai módon, amit gondolsz a forráskód alapján, hanem pontosan ugyanazt az eredményt a proci szempontjából sokkal gyorsabban produkálja. Amit lehet azt előre kiszámol és az eredményt használja, amit lehet regiszterbe tesz. A "register" C kulcsszó mára már gyakorlatilag hatástalan, annyira jó lett a C fordító. És amelyik általad leírt C utasítás esetén van gyorsabb művelettr lehetőség (például osztás helyett szorzás és shift), ott azzal írja körül.

Sokkal de sokkal okosabbak lettek a normális fordítók (gcc, clang). Te csak magára az algoritmusra figyelj, illetve arra hogy a C és C++ fordító csak fájlon belül tud optimalizálni (+include), másik fájlra nem lát át.

Hát, azért ez nem feltétlen így van. Egyszer újraírtam a Dune Legacy térképgenerátorát; az algoritmus ekvivalens volt (jóhogy, hiszen egyébként nem működne), viszont az optimalizációimmal 1.7x-es sebességnövekedést értem el. Annyira azért nem okosak a fordítók.

Ez a 4.9-es gcc előtt volt vagy után?
Nekem ez a verzió jelentett ugyanis komoly vízválasztót jelfeldolgozásnál. Addig sokkal butább volt céljaimra és addig mindent kézzel kellett kidolgoznom, ha tempót akartam.
A mostani GCC-nél és CLANG-nál már sokkal kevesebb dologra kell figyelnem, ha gyors jelfeldolgozásra kell kódot írnom.

Nem emlékszen, hogy valaha írtam volna bármi jót a HWSW előadásairól, de most "kénytelen leszek".

Szerintem ez egy kimondottan jó előadás és jó előadó volt. Látszott rajta, hogy felkészült, tényleg ért ahhoz, amiről beszél és az előadást se azért tartja, mert ezzel akar nagyzolni, önmagát reklámozni, hanem nyújtani akar valami hasznosat. És ez sikerült is.

Volt eleje-közepe-vége, volt íve, és betöltötte azt a funkcióját, ami egy ilyen 40 perces előadástól elvárható: megtudhatja belőle az érdeklődő, hogy mit nem tud és ha érdekli a téma, hol keresse a folytatást.

Én végignéztem és a felettem olvasható véleményekkel szemben nyugodt lélekkel tudom ajánlani.

> Látszott rajta, hogy felkészült, tényleg ért ahhoz, amiről beszél

ezzel teljesen egyetertek. csak nekem az eloadasmodja nem tetszett, tulsagosan elmerult a lenyegtelen reszletekben, sztorizgatasban stb es 15 perc alatt nem jutott sehova, unalomba fulladt az egesz. kb mint egyakciofilm ahol az elso 1 oraban nem tortenik semmi csak beszelgetnek...

mai rohano vilagunkban, ahol az 1 perces tiktok a "standard", kicsit porgosebben kene eloadni :)

En hianyoltam belole az optimalizaciok neveit. (pl tail call optimization, return value optimization, stb). Meg a felteteleikrol is lehetett volna beszelni, mert azt tudni csak, hogy egyszerubb esetekben mukodik, szerintem nem eleg konkret. Meg 15-20 percbe is belefert volna.

Szerintem jó előadás volt, magam is ezen elvek alapján dolgozok.