Hatékony CPU kód. írásához hasznos architekturális háttér. Valasek Gábor

Hasonló dokumentumok
Mentegetőzések. Hatékony CPU kód. írásához hasznos architekturális háttér, hogy miért is lassú a kódom. Valasek Gábor

Számítógépek felépítése

Programozás alapjai. 10. előadás

Mutatók és mutató-aritmetika C-ben március 19.

Készítette: Trosztel Mátyás Konzulens: Hajós Gergely

Memóriagazdálkodás. Kódgenerálás. Kódoptimalizálás

Mintavételes szabályozás mikrovezérlő segítségével

Pénzügyi algoritmusok

Assembly. Iványi Péter

Digitális rendszerek. Utasításarchitektúra szintje

találhatók. A memória-szervezési modell mondja meg azt, hogy miként

Szoftvertechnológia alapjai Java előadások

SZÁMÍTÓGÉP ARCHITEKTÚRÁK

Programozási nyelvek Java

Hatékony memóriakezelési technikák. Smidla József Operációkutatási Laboratórium január 16.

1. Template (sablon) 1.1. Függvénysablon Függvénysablon példányosítás Osztálysablon

A verem (stack) A verem egy olyan struktúra, aminek a tetejéről kivehetünk egy (vagy sorban több) elemet. A verem felhasználása

Pénzügyi algoritmusok

Operandus típusok Bevezetés: Az utasítás-feldolgozás menete

A 32 bites x86-os architektúra regiszterei

Magas szintű optimalizálás

Java II. I A Java programozási nyelv alapelemei

GPU Lab. 4. fejezet. Fordítók felépítése. Grafikus Processzorok Tudományos Célú Programozása. Berényi Dániel Nagy-Egri Máté Ferenc

Adatelérés és memóriakezelés

Programozás I. 3. gyakorlat. Szegedi Tudományegyetem Természettudományi és Informatikai Kar

OOP #14 (referencia-elv)

PE/COFF fájl formátum

Programozas 1. Strukturak, mutatok

Programozás I gyakorlat

5-6. ea Created by mrjrm & Pogácsa, frissítette: Félix

SzA19. Az elágazások vizsgálata

A Számítógépek felépítése, mőködési módjai

Bevezetés az informatikába

Programozási nyelvek JAVA EA+GY 1. gyakolat

Számítógép felépítése

Párhuzamos és Grid rendszerek

Architektúra, cache. Mirıl lesz szó? Mi a probléma? Teljesítmény. Cache elve. Megoldás. Egy rövid idıintervallum alatt a memóriahivatkozások a teljes

Java II. I A Java programozási nyelv alapelemei

Dr. Schuster György október 14.

5. Gyakorlat. struct diak {

GPU Lab. 3. fejezet. Az X86 Utasításkészlet. Grafikus Processzorok Tudományos Célú Programozása. Berényi Dániel Nagy-Egri Máté Ferenc

Számítógépek felépítése, alapfogalmak

Informatika terméktervezőknek

1. Az utasítás beolvasása a processzorba

Operációs rendszerek III.

Adatok ábrázolása, adattípusok

A fordítóprogramok szerkezete. Kódoptimalizálás. A kódoptimalizálás célja. A szintézis menete valójában. Kódoptimalizálási lépések osztályozása

A PROGAMOZÁS ALAPJAI 1. Függvény mint függvény paramétere. Függvény mint függvény paramétere. Függvény mint függvény paramétere

8. Fejezet Processzor (CPU) és memória: tervezés, implementáció, modern megoldások

8. Fejezet Processzor (CPU) és memória: tervezés, implementáció, modern megoldások

1. Alapok. Programozás II

SZÁMÍTÓGÉP ARCHITEKTÚRÁK

Függvények. Programozás I. Hatwágner F. Miklós november 16. Széchenyi István Egyetem, Gy r

8. gyakorlat Pointerek, dinamikus memóriakezelés

Stack Vezérlés szerkezet Adat 2.

Architektúra, megszakítási rendszerek

Bevezetés a programozásba Előadás: Objektumszintű és osztályszintű elemek, hibakezelés

LabView Academy. 4. óra párhuzamos programozás

Mi az assembly? Gyakorlatias assembly bevezető. Sokféle assembly van... Mit fogunk mi használni? A NASM fordítóprogramja. Assembly programok fordítása

B I T M A N B I v: T M A N

A számítógép alapfelépítése

A processzor hajtja végre a műveleteket. összeadás, szorzás, logikai műveletek (és, vagy, nem)

Az interrupt Benesóczky Zoltán 2004

Fordító részei. Fordító részei. Kód visszafejtés. Izsó Tamás szeptember 29. Izsó Tamás Fordító részei / 1

Adatszerkezetek Tömb, sor, verem. Dr. Iványi Péter

Programozás C++ -ban 2007/7

ARM Cortex magú mikrovezérlők

egy szisztolikus példa

elektronikus adattárolást memóriacím

B I T M A N B I v: T M A N

Máté: Számítógép architektúrák

Programozás C nyelven FELÜLNÉZETBŐL elhullatott MORZSÁK. Sapientia EMTE

Ismerkedjünk tovább a számítógéppel. Alaplap és a processzeor

Programozás alapjai gyakorlat. 2. gyakorlat C alapok

Mechatronika és mikroszámítógépek 2017/2018 I. félév. Bevezetés a C nyelvbe

Szoftvergyártás: gyártásvezérlés kód-figyeléssel

Számítógépek felépítése, alapfogalmak

C++ programok fordítása

Programozási nyelvek Java

3. Osztályok II. Programozás II

Assembly programozás levelező tagozat

Java és web programozás

Programozási nyelvek a közoktatásban alapfogalmak II. előadás

Apple Swift kurzus 3. gyakorlat

Máté: Számítógép architektúrák

OpenCL - The open standard for parallel programming of heterogeneous systems

Memóriák - tárak. Memória. Kapacitás Ár. Sebesség. Háttértár. (felejtő) (nem felejtő)

Számítógép Architektúrák

Programozás II. 4. Dr. Iványi Péter

A C# programozási nyelv alapjai

Járműfedélzeti rendszerek I. 3. előadás Dr. Bécsi Tamás

Occam 1. Készítette: Szabó Éva

A C programozási nyelv V. Struktúra Dinamikus memóriakezelés

Labor gyakorlat Mikrovezérlők

2017/12/16 21:33 1/7 Hardver alapok

C memóriakezelés. Mutató típusú változót egy típus és a változó neve elé írt csillag karakterrel hozhatjuk létre.

Programozás. (GKxB_INTM021) Dr. Hatwágner F. Miklós április 4. Széchenyi István Egyetem, Gy r

Mikrorendszerek tervezése

SZÁMÍTÓGÉP ARCHITEKTÚRÁK

A számok kiírása is alapvetően karakterek kiírásán alapul, azonban figyelembe kell venni, hogy a számjegyeket, mint karaktereket kell kiírni.

Átírás:

Hatékony CPU kód írásához hasznos architekturális háttér Valasek Gábor

Mentegetőzések Hatékony CPU kód írásához hasznos architekturális háttér, hogy miért is lassú a kódom Valasek Gábor

Műveletvégzés 4 + 9 13

Műveletvégzés 4 + 9 13

Műveletvégzés 4 + 9 13

Műveletvégzés Intel szintaxis: mnemonic src, dst 4 + 9 13

Regiszterek http://www.cs.virginia.edu/~evans/cs216/guides/x86.html

Utasítások Három fő kategória: adatmozgatás: mov, push, pop stb. aritmetikai/logikai műveletek: add, sub, imul, mul, div, and, xor stb. control flow: jmp, je, jne, call, ret stb.

Műveletvégzés 4 + 9 13

Műveletvégzés 4 + 9 13

Műveletvégzés 4 + 9 13

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Pipeline architektúrák

Pipeline architektúrák Két fontos statisztikájuk van: Latency: egy utasítás végrehajtásához szükséges teljes idő. Bandwidth vagy throughput: egységnyi idő alatt hány utasítást tud feldolgozni a rendszer. Ez a leglassabb komponenstől függ.

Szuperskalár architektúrák

Szuperskalár architektúrák

Szuperskalár architektúrák A pipeline egyes fázisait megvalósító egységeknek több, redundáns példánya is van Ezáltal egyszerre több utasítás is végrehajtható párhuzamosan Például az Intel processzorok pipeline-os és szuperskalár architektúrák Érdeklődőknek további anyagok: http://www.lighterra.com/papers/modernmicroprocessors/

Very Long Instruction Word

Hatékony kód régen és most Amíg az órajelek relatíve alacsonyak voltak, megérte minimalizálni a számítások számát Ez ma már megfordult: a mantra az, hogy inkább végezz több munkát a ALU-kkal és csak akkor nyúlj a memóriához, ha feltétlenül szükséges Ennek oka az elérési idők módosulása (forrás: https://www.theregister.co.uk/2016/04/21/storage_approaches_memory_spee d_with_xpoint_and_storageclass_memory/ )

Elérési idők

Utasítások költsége

Irodalom Egy nagyon hasznos bejegyzés erről a témáról: http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/

Kitérő Miért olyan drága az egésszel számokkal való osztás? Mindig ugyanolyan drága? Mi a helyzet a lebegőpontos számokkal? Milyen algoritmusokat használnak a szokásos művetek megvalósítására CPU-ban? Miket használnak a kevésbé szokásos műveletekre (sin, cos stb.)? Sok válasz megtalálható az ilyen jellegű kérdésekre Jean Michel Muller könyveiben: https://www.amazon.com/handbook-floating-point-arithmetic-jean-michel-muller/dp/0817647 04X https://www.amazon.com/elementary-functions-implementation-jean-michel-muller/dp/14899 79816/ref=sr_1_2?s=books&ie=UTF8&qid=1537995137&sr=1-2

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Program a memóriában A végrehajtható fájl lehet egy.exe (Windows) vagy.elf (executable and linking format - Unix) A lefordított és összelinkelt végrehajtható fájl tartalmazza a program executable image-ét Ez egy részleges képe annak, ahogyan a program memóriában ki fog nézni a dinamikus memóriafoglalásokat értelemszerűen nem tartalmazza Az image 4 szegmensből áll

Execution image Code/text segment Data segment BSS segment Read-only data segment

Execution image Végrehajtható gépikód A kódban inicializált globális illetve statikus változók Inicializálatlan globális és statikus változók Csak olvasható adatok Az értékük a specifikáció szerint adott (=0), de csak a program belépési pontjának meghívása előtt nullázódik ki. Egyes esetekben (ún. manifest constant-oknál) a kódba fordul bele a konstans értéke. Ilyen pl. az int konstans.

Execution image A BSS megnevezés történelmi hagyaték: a block started by symbol rövidítése A manifest konstansok trükkösek: mivel a compiler az értéküket belehelyettesíti a kódba, ezért az ő tárolásuk valójában a kódszegmensben történik

Endian Az egy bájtnál nagyobb foglalású változók esetén jön képbe Kétféle verziója van: Little endian: a kisebb helyiértéket reprezentáló bájtok a memória elejéhez vannak közelebb Big endian: a legnagyobb helyiérték bájtja van a kisebb memóriacímen Fontos, hogy min fejlesztünk és mire fejlesztünk: Intel processzorok: little endian-ok Wii, Xbox 360, PlayStation 3 (vagyis PowerPC hajtotta konzolok): big endian-ok

Program végrehajtása A belépési pont elindításával kezdődik (pl. main() ) A futtatás megkezdésekor az OS lefoglal egy memóriaterületet az alkalmazásnak, amit program stack-nek hívnak Minden egyes függvényhíváskor erre a stack-re push-olnak egy összefüggő (=folytonos) memóriafoglalást, amit stack frame-nek hívnak

Stack frame Háromféle adatot tárolunk itt: A függvényünket hívó függvény memóriacímét, hogy a visszatérésünk után folytatódhasson a program futtatása A CPU regisztereinek értékeit a hívás pillanatában. Visszatéréskor ezeket az értékeket visszaírjuk a regiszterekbe. A függvény visszatérési értéke viszont általában egy speciális regiszterbe kerül, amit értelemszerűen nem állítunk vissza A függvény lokális változói is itt kerülnek foglalásra. (Meg néha regiszterekben)

Példa void c() { U32 localc1;... } F32 b() { F32 localb1; I32 localb2;... c(); return localb1; } void a() { U32 alocalsa1[5];... F32 locala2 = b();... }

Változók helye a memóriában A globális és a statikus változók a futtatható fájlban vannak A lokális változók a stack-re kerülnek A dinamikus változók azonban a heap-re A probléma ezzel az, hogy az OS-től függ a foglalás Ezért váratlanul sokat állhat a programunk, amíg a new vissza nem tér (...legalábbis 4 new a 6-ból (<C++17)/8-ból(>=C++17)..)

Objektumok elhelyezése a memóriában struct Foo { U32 munsignedvalue; F32 mfloatvalue; I32 msignedvalue; };

Objektumok elhelyezése a memóriában struct InefficientPacking { U32 mu1; // 32 bits F32 mf2; // 32 bits U8 mb3; // 8 bits I32 mi4; // 32 bits bool mb5; // 8 bits char mp6; // 32 bits };

Objektumok elhelyezése a memóriában struct InefficientPacking { U32 mu1; // 32 bits F32 mf2; // 32 bits U8 mb3; // 8 bits I32 mi4; // 32 bits bool mb5; // 8 bits char mp6; // 32 bits };

Objektumok elhelyezése a memóriában Alignment: az objektum címe a memóriában az alignment méretének egy egész számú többszöröse kell, hogy legyen.

Objektumok elhelyezése a memóriában Alignment: az objektum címe a memóriában az alignment méretének egy egész számú többszöröse kell, hogy legyen.

Alignment Az alignment mérete adattípustól függ például 32 bites típusok általában 4 bájtos rácsra illesztendők feltehetjük rule of thumb-ként, hogy a típus mérete határozza meg, hogy milyen rácsra kell illeszteni az objektum kezdetét A objektumok/struktúrák adattagjaira ezek külön-külön is érvényesek +array context padding: azaz ha a struktúra példányaiból van egy tömbünk, akkor az i-edik elem utolsó adattagja után a memóriában annyival jön az i+1-edik elem első adattagja, hogy a méretének megfelelő alignment-en legyen

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Cache A CPU által írható és olvasható memóriadarab, aminek kialakításánál a cél a minél kisebb késleltetés Ezt kétféleképpen érik el: A lehető leggyorsabb memóriatechnológiát használják hozzájuk A cache-ek fizikailag is közelebb vannak a CPU-hoz A cache lényegében a globális memóriában lévő változóknak egy (CPU szempontjából) lokális másolata Méghozzá azoké, amiket gyakran lekérdez a program Így ha épp cache-ben van, akkor nem kell elzarándokolni a RAM-ig (ez a cache hit) Ha nem volt a cache-ben (cache miss), elmegyünk a RAM-ig, visszahozzuk az adatot de egyúttal beírjuk a cache-be is (hátha legközelebb is kell)

Cache line Amikor egy új memóriádarabot érünk el cache miss miatt, nem csak az általunk kért adatok jönnek vissza, hanem egy cache line-nyi darab i7-es archiektúrákon a L1, L2, L3 cache line-ok 64 byte-osak Így ha a programunk következő utasítása az előzőleg lekért memória utáni memóriát olvasná, akkor garantált () cache hit-ünk van A cache-ek asszociatív memóriák: tudják, hogy melyik RAM-beli valódi memóriaterület van bennük (ehhez használják a translation lookaside buffer-t, azaz a TLB-t)

Cache

Cache - írási policy Amikor a programunk egy változó értékét módosítja, akkor azt vissza kell juttatni a memóriába is Ez a CPU architektúra write policy-jétől függ, hogy miképp történik Az írások bekerülnek a cache-be, aztán A write-through cache-ek az írásokat rögtön továbbítják a memóriába is A write-back policy-k pedig csak bizonyos esetekben (pl. cache miss) írnak vissza

Többszintű cache Probléma, hogy kisméretű cache A nagyméretű viszont Nagyon gyors De sok cache miss-t generál Fizikailag nem tud olyan közel kerülni a CPU-hoz De cserébe több cache hit lesz Ezért csinálnak többszintű cache-eket, egyre nagyobb méretben

Cache-ek többmagos környezetben

Cache-ek többmagos környezetben Többmagos környezetben előjön a cache konzisztencia problémája Vagyis hogy minden mag lokális cache-e a fizikai memóriában található valódi értéket tükrözze Két elterjedt protokoll erre a MESI (modified, exclusive, shared, invalid) és a MOESI (modified, owned, exclusive, shared, invalid): https://en.wikipedia.org/wiki/moesi_protocol

A kétféle cache Instruction cache: a programunk kódját cache-eli. Elágazásokkal ezt tudjuk tönkrevágni, ezért van szükség branch prediction-re stb. Data cache: a programunk adatait cache-eli A fenti kettő független egymástól

D-cache-re optimalizálás Az adatainkat a memóriában folytonosan tároljuk, a lehető legkisebb méretben és szekvenciálisan dolgozzuk fel őket

I-cache-re optimalizálás A teljesítmény-kritikus ciklusok a lehető legrövidebbek legyenek kódméret szempontjából Ne hívjunk belőlük függvényeket Ha mégis kell és kicsi a függvény, akkor inline-oljuk Ha nem kicsi és csak igazi függvényhívással érhető el, ekkor érjük el, hogy a memóriában a függvény gépi kódja a ciklushoz a lehető legközelebb legyen

I-cache-re optimalizálás Az utóbbi nagyon nehéz, mert a linker és a compiler dönti el, hogy fizikailag hová kerül az executable image-ben a függvény törzse Viszont van néhány rule-of-thumb amit általában be szoktak tartani: Egy függvény kódja a memóriában szinte mindig folytonos, azaz a linker nem szúr bele más kódot a függvénybe (kivéve ha az a más kód egy inline-olt függvény hívása) A függvények a fordítási egységük forráskódjában (.cpp) található sorrendjüknek megfelelően kerülnek a memóriába Vagyis egyetlen fordítási egység függvényei a memóriában folytonosan helyezkednek el (általában)

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Stalling Előfordulhat, hogy egy utasítás végrehajtása csak akkor kezdődhet meg, hogy ha egy előtte lévő utasítás teljesen végigment a pipeline-on Ekkor a függő utasítás beakasztja a pipeline-t - ez a stalling

Stalling - adatfüggőség miatt

Stalling A fenti példában az add utasításnak be kellett várni a mul befejeződését Az ilyenek elkerülése érdekében a compilerek megpróbálják automatikusan átrendezni az utasításaink sorrendjét, hogy a függés miatt várakozó utasítás helyett független utasítások futhassanak

Branch prediction Amikor a végrehajtás elágazáshoz ér, akkor a pipeline-nak döntenie kell, hogy a then vagy az else ág kódját kezdi végrehajtani Ha rosszul tippelt, akkor a pipeline-t flush-olni kell (az eddigi műveleteket érvényteleníteni) és újratölteni a helyes elágazási irány kódjával A legegyszerűbb (statikus) stratégia a CPU részéről hogy a backward branch-et tekinti valószínűbbnek (azaz az elágazás azon ágát, aminek a gépi kód memóriacíme kisebb, mint az aktuális cím - ilyen például az az eset, amikor a ciklusunk még folytatja a futását) Következmény: if-nél, többfelé ágazó if-nél, switch-case-nél először a leggyakoribb eseteket rakjátok

Stalling + branching megoldások + O(e) => meltdown

Load-Hit-Store A probléma lényege, hogy egy változóba történő írás után közvetlenül próbáljuk újra olvasni a változó értékét A klasszikus példa: az oris-en stallol: stfs fr3,0(r3) ;Store the float lwz r9,0(r3) ;Read it back into an integer register oris r9,r9,0x8000 ;Force to negative

Load-Hit-Store int CauseLHS( int ptra ) { Nem lehetett ptra értékét regiszterben/cache-ben tartani, mert nem tudja a compiler, hogy nem módosította-e a ptra területét egy másik pointer a függvényben, ami ugyanoda mutat! int a,b; } int ptrb = ptra; // B and A point to the same direction ptra = 5; // Write data to address prta b = ptrb; // Read that data back again (won't be available for 40/80 cycles) a = b + 10;// Stall! The data b isn't available yet

Load-Hit-Store int slow( int a, int b) { a = 5; b = 7; return a + b;// Stall! The compiler doesn't know whether // a==b, so it has to reload both // before the add }

Irodalom és javasolt néznivalók Jason Gregory: Game Engine Architecture (az ábrák forrása is ez) Agner: Optimization manuals (ingyenes) Kitekintésnek C++ HPC workshop: https://www.youtube.com/watch?v=7xwvlfzrksk&list=pl1tk5lgm7zvqh6rk uropdmdomhb6lzcwl

Polinomok kiértékelése Forrás: http://lolengine.net/blog/2011/9/17/playing-with-the-cpu-pipeline A probléma: hogyan lehet kiértékelni egy polinomot hatékonyan? Most: a sin(x) hatványsoros közelítését, elvágva az x^15-edikenes tagnál

Polinomok kiértékelése static static static static static static static static double double double double double double double double a0 a1 a2 a3 a4 a5 a6 a7 = = = = = = = = +1.0; -1.666666666666580809419428987894207e-1; +8.333333333262716094425037738346873e-3; -1.984126982005911439283646346964929e-4; +2.755731607338689220657382272783309e-6; -2.505185130214293595900283001271652e-8; +1.604729591825977403374012010065495e-10; -7.364589573262279913270651228486670e-13; double sin1(double x) { return a0 x + a1 x x + a2 x x + a3 x x + a4 x x + a5 x x + a6 x x + a7 x x } 64 szorzás + 7 összeadás x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x;

Polinomok kiértékelése double sin2(double { double ret, y = x, x2 = x ret = a0 y; ret += a1 y; ret += a2 y; ret += a3 y; ret += a4 y; ret += a5 y; ret += a6 y; ret += a7 y; return ret; } x) y y y y y y y x; = = = = = = = x2; x2; x2; x2; x2; x2; x2; 16 szorzás + 7 összeadás

Polinomok kiértékelése double sin3(double x) // Horner { double x2 = x x; return x (a0 + x2 (a1 + x2 (a2 + x2 (a3 + x2 (a4 + x2 (a5 + x2 (a6 + x2 a7))))))); } 9 szorzás + 7 összeadás

Mérések Intel Core i7-2620m CPU at 2.70GHz. The functions were compiled using -O3 -ffast-math: function nanoseconds per call sin 22.518 sin1 16.406 sin2 16.658 sin3 25.276

Mérések Intel Core i7-2620m CPU at 2.70GHz. The functions were compiled using -O3 -ffast-math: function nanoseconds per call sin 22.518 sin1 16.406 sin2 16.658 sin3 25.276

Polinomok kiértékelése double sin3(double x) // Horner { double x2 = x x; return x (a0 + x2 (a1 + x2 (a2 + x2 (a3 + x2 (a4 + x2 (a5 + x2 (a6 + x2 a7))))))); }

Polinomok kiértékelése double sin2(double { double ret, y = x, x2 = x ret = a0 y; ret += a1 y; ret += a2 y; ret += a3 y; ret += a4 y; ret += a5 y; ret += a6 y; ret += a7 y; return ret; } x) y y y y y y y x; = = = = = = = x2; x2; x2; x2; x2; x2; x2;

Polinomok kiértékelése Kézzel optimalizálni: function sin sin1 sin2 sin3 sin4 sin5 sin6 sin7 nanoseconds per call 22.518 16.406 16.658 25.276 18.666 18.582 16.366 17.470 Persze nem árt a compiler flag-ekkel is kicsit foglalkozni (csak -O3): function nanoseconds per call sin 22.497 sin1 30.250 sin2 19.865 sin3 25.279 sin4 18.587 De akár használhatsz irodalmat is: Estrin séma sin5 18.958 sin6 16.362 sin7 15.891