Test: paměti mají čtyřistakrát delší latenci než procesory

16. 2. 2009
Doba čtení: 16 minut

Sdílet

V článku se zabývám nepoměrem mezi časem na provedení instrukce a přečtení dat z paměti. V extrémním případě dnešní procesory stihnou sečíst až 1200 integer čísel za čas, který trvá přečtení jednoho čísla z RAM. Vycházím ze skutečného měření a přikládám měřicí program, který jsem pro tyto účely sestavil.

Z výsledků je dále patrné, že procesory Intel Xeon mají lepší správu paměti cache než AMD Opteron. Ta se projeví v případě, že přes CPU „teče“ hodně dat, která budou potřeba jen jednou (například při kódování videa). Nakonec si neodpustím několik úplně zbytečných programátorských rad, které však používám už řadu let a věřte, že se mi plně osvědčují.

Úvod

Kdysi dávno se výkon procesoru hodnotil podle rychlosti jeho hodin. Když se přišlo na to, že je to hloupost, začaly se pomocí drystone&whetstone testu měřit MIPSy a MFLOPSy. Pak ale vyšlo najevo, že v praktických programech MIPSově pomalejší počítač mnohdy předhoní MIPSově rychlejší. Od té doby se používají „aplikační testy“, tedy například kolik dosáhne FPS při animování hry Quake (toto, po přepočtu na frekvenci Intelu P4, používala firma AMD pro číselné značení Athlonu), jak dlouho trvá zazipování a rozbalení nějakého souboru a další podobné testy. Tato metoda měření má nesporné výhody pro běžné uživatele. Na konkrétních příkladech jim ukazuje, jak dlouho co trvá. Také do jistě míry potlačuje nesprávnou odpověď „4GB“ na otázku „Jak rychlej máš procesor?“.

Když ale programuji, tak mě zajímá, jak dlouho různé operace trvají, abych mohl program udělat co nejrychlejší. Částečně se to dá zjistit z datasheetu procesoru, ale na opravdovou výkonnost počítače má vliv ještě chipset a paměti, a proto jsem napsal program, který tuto rychlost zkusí přibližně změřit. Nicméně měření jednotlivých instrukcí je třeba brát pouze jako orientační. Při optimalizaci je nakonec nejlepší měřit přímo program, který píšeme. Na druhou stranu, jeho celková architektura má vliv na rychlost mnohdy větší než všechny pozdější optimalizace dohromady. A pro její volbu se už vyplatí znát špičkové výkony ve sčítání, násobení a přesouvání dat z paměti, které si dále ukážeme.

Jak měřit dobu provádění instrukce

Dnešní procesory jsou jednak pipelinované a za druhé superskalární. To první znamená, že instrukce prochází „výrobní linkou“ s několika „odděleními“. První oddělení načítá instrukci, další ji dekóduje, další provádí a poslední zapisuje výsledky do paměti (například; v PC procesorech je to mnohem složitější).

V ideálním případě, kdy další instrukce nemusí čekat na výsledek předchozí, jsou takto rozpracovány čtyři instrukce a každý takt je jedna dokončena. Superskalárnost znamená, že je takových výrobních linek několik. Například dnes běžně máme v procesoru tři sčítačky. Tak je možné, aby procesor za jeden takt dokončil tři instrukce. Schopnosti procesoru pro různé instrukce pak udávají následující dvě čísla:

Latency je čas, za jaký bude k dispozici výsledek, od doby, co instrukce načetla operandy, měřený v počtu taktů. V jednoduchých případech odpovídá délce pipeline zkrácené o načítací a dekódovací část.

Repeat Rate, někdy též zvaný Throughput. Říká, kolik taktů průměrně uplyne, než mohu spustit tutéž instrukci znovu (s jinými daty). Když například pracují všechny tři sčítačky zároveň, dostávám repeat_rate 1/3.

Když kompilátor optimalizuje nějaký program, měl by ideálně vědět víc než jen tato dvě čísla. Ve skutečném procesoru jsou některé jednotky sdílené (dekodér instrukcí) a může se stát, že i když třeba máme ještě volnou sčítačku, dekodér, který u většiny CPU dokáže dodávat maximálně tři instrukce za takt, je již zahlcen. Rozhodnout, jak rychle se pak co bude provádět, je bez přesného popisu CPU velmi těžké (jelikož kromě superskalárnosti je zde ještě out-of-order execution, což znamená, že nezávislé instrukce můžou být přeházeny ještě dřív, než se dostanou do pipeline).

Pro naše účely však postačí, když určíme špičkovou výkonnost procesoru pro některé často používané operace. Proto ho necháme provádět dlouhé bloky stejných instrukcí a budeme měřit čas pomocí instrukce RDTSC (která přečte z čítače uvnitř procesoru počet taktů hodin od zapnutí počítače). Latence se měří pomocí řady závislých instrukcí, například:

    add $1,%eax
    add $1,%eax
    add $1,%eax
    add $1,%eax
        ....

Takových instrukcí je sto, pak je skok na začátek, opakujeme pětsetkrát (důvod pro opakování je případné vyprůměrování doby, kdy se instrukce načítaly do instrukční cache). Repeat-rate se měří pomoci řady nezávislých instrukcí, například:

    add $1,%eax
    add $1,%ebx
    add $1,%ecx
    add $1,%edx
    add $1,%esi

    add $1,%eax
    add $1,%ebx
    add $1,%ecx
    add $1,%edx
    add $1,%esi
        ....

Pro jednoduchost se čas jumpu, který tvoří cyklus na konci bloku, a čas inicializace citace započítává do celkového testu. Jelikož je ale blok velký (stovky instrukcí), je chyba poměrně zanedbatelná (v řádech procent). V případě násobení je u ia32 architektury problém v předem daných registrech (%eax*něco → %edx:%eax). Je potřeba udělat explicitní MOV, abych se zbavil závislosti. Teoreticky by se měl provést zároveň s násobením.

Měření rychlosti pamětí

Kromě měření operací které něco počítají, budeme také měřit, jak dlouho trvá, než dostaneme data z paměti. Nejprve jak dlouho trvá MOV dat z L1-cache. Alokujeme si nevelké pole. Toto pole vynulujeme a pak ho dvaatřicetkrát přečteme. Tím procesor pochopí, že ho má držet v L1-cache. Pak následuje test:

        movl RPTS, %%edx
        dec  %%edx
        movl PITCH, %%ecx
        movl ADDRESS, %%edi
        movl %%ecx, %%esi
        addl %%esi, %%esi
loop:                       (*)
        movl (%%edi), %%eax
        movl (%%edi,%%ecx), %%ebx
        addl %%esi, %%edi

        movl (%%edi), %%eax
        movl (%%edi,%%ecx), %%ebx
        addl %%esi, %%edi
        .
    .
        .
        subl $1, %%edx
        jns loop

Je zde snaha číst data tak, aby byla teoretická šance provést všechny tři instrukce v bloku zároveň. Nakonec se změří, kolik taktů bylo v průměru potřeba na přenos jednoho 32bitového čísla. PITCH bylo v tomto případě rovno čtyřem, takže jsme četli souvislý úsek.

Pokud budeme chtít změřit, jak asi rychle funguje načítání z RAM, můžeme použít stejný kód, ale než ho spustíme, je třeba pole (které nyní bude mnohem větší) vystrnadit z cache. Toho se docílí alokováním 150 MB pomocného pole, které nejdřív vynulujeme, pak přečteme a pak se zaměříme na jeho kousky o velikosti 0,5 MB, které čteme a zapisujeme a postupně se posouváme podél velkého 150 MB pole. To celé se několikrát opakuje s různé velikými kousky (abychom různé veliké L1-cache nalákali na tato data a přiměli je zapomenout původní pole).

Nakonec spustíme čtení (*), tentokrát s větším PITCH. Pro hodnotu 74 bytů (což označuji za typickou cache-miss) uvidíme celkem přívětivé výsledky, jelikož počítač do L2-cache načte celou cache-line, takže na RAM budeme skutečně čekat jen občas. Zároveň ale 74 bytů je delší než typická L1-cache-line (která je 64 bytů), takže tím procesor nutíme číst z L2-cache.

Pro hodnotu PITCH=1048572 bytu však již uvidíme poměrně dlouhé časy. Velikost okolo 1 MB zaručí, že každý přístup bude do jiné řádky DRAM paměti, takže řadič paměti bude muset pokaždé provést plný cyklus přístupu (close/prechar­ge/řádek/slou­pec). Ještě tu máme jeden test s PITCH=1048572 a s poznámkou „1-pass cache flush“, což znamená, že 150 MB pole se pouze vynuluje a jednou přečte a pak se hned začne testovat pomocí (*). Jak uvidíme, procesorům AMD to stačí, aby zapomněly na původní data, nové procesory Intel si většinu z nich udrží přinejmenším v L2 cache.

Měříme v Linuxu

Čítač z RDTSC je globální pro celý operační systém, ne pro jednotlivé procesy. Měří tudíž reálný čas a kdybychom měli spuštěn s testem ještě jiný program, tak by měření ovlivnil. I kdyby však byl čítač lokální, výsledky by byly jiné, vzhledem k jinému chování cache po task switchi.

Proto je potřeba spouštět test jen na nezatíženém systému. I tak však neustále běží přerušení od časovače a ostatních periférii. Proto se každý test opakuje dvacetkrát a vybere se minimální naměřená hodnota (zbrzdilo nás minimálně, ideálně žádné, přerušení). Toto opatření je trochu v rozporu s měřením vybavovací doby paměti, protože tam se zajímáme spíše o maximální čas. Přijde-li například několikrát refresh (chodí každých 7 až 15 µs, podle typu paměti) a jednou ne, dostaneme na výstupu ten lepší výsledek, když nepřišel a nemuselo se na jeho dokončení čekat.

Přesnějšího měření by bylo možné dosáhnout při zakázaném přerušení, ale nevím, co by se pak stalo při případném page-faultu (jestli by si Linux přerušení zase povolil, kdyby třeba potřeboval mmapovat program z disku nebo swapovat, čemuž by se částečně dalo předejít zamknutím stránek v RAM). Pak by mělo smysl výsledky z jednotlivých opakování průměrovat. Jelikož jsem ale nechtěl riskovat zamrznutí systému a na většině měřených počítačů stejně nejsem root, dále jsem tento problém neřešil.

O programu

Program (zdrojový kód i binárka) jsou v příloze tohoto článku. Vzhledem k tomu, že používám spoustu assembleru, je to napsáno v gcc-2.95.3, které ještě podle staré normy C++ povolovalo stringy přes několik řádků. Měřil jsem jen 32bitovou integerovou výkonnost a ve floatech jen standardní FPU (bez SSE). To vše z lenosti. Program lze spustit i na 64bitových strojích, pokud je přeložen 32bitově (staré gcc stejně neumělo překládat jinak).

Tabulky

Výsledky nejsou úplně vědecké, správné bych měl měřit ještě směrodatnou odchylku (a dobře by bylo měřit maximum i minimum), ale to by pak vznikla spousta nepřehledných čísel (kdo to má rád, snadno si program upraví). V případě počítače SOL5 jsem měření několikrát opakoval a dvě instance jsem napsal pod sebe, aby si čtenář udělal představu o rozptylu.

V sloupečku DEPENDENT je odhad na latency, ve sloupečku INDEPENDENT je odhad na repeat-rate. Počet jader počítače je počet CPU, který uvádí /proc/cpuinfo. Testovalo se vždy jen jedno jádro a ostatní zahálela (resp. běžel top, sshd a nějaké systémové procesy).

Počítač FIREBALL1 (4jádro):

model name: Intel® Xeon® CPU 5160 @ 3.00GHz
cpu MHz: 2992.494
cache size: 4096 KB
cache_alignment: 64

Počítač FIREBALL1
AVG CPU CLOCKS FOR DEPENDENT INDEPENDENT
rdtsc 65.011 64.011
32bit add 1.004 0.343
32*32->64bit imul 4.759 1.528
0*0 imul 4.767
64/32->32.32 div 41.050
64bit fadd 2.252 1.031
64bit NaN+x fadd 195.273 214.584
64bit fmul 5.000 2.030
64bit fdiv 37.471 37.002
32bit load from L1-cache 1.001 pitch=4B
typical L1-miss load 64.689 pitch=74B
load from RAM 142.65ns 426.877 pitch=1048572B
load from RAM 9.65ns 28.889 pitch=1048572B (1-pass cache flush)

Ukázka variability výsledků. Na lichých řádcích je jedno měření, na sudých je druhé. Měření byla provedena na nezatíženém stroji okamžitě po sobě a byla vybrána dvě z nich, která se hodně liší v přístupové době k paměti. Na ostatních instrukcích je vidět, že chyba měření je spíše systematická (zanedbávání instrukcí cyklu apod.) než náhodná (přerušení).

Počítač SOL5 (4jádro):

model name: Dual Core AMD Opteron™ Processor 275
cpu MHz: 2190.981
cache size: 1024 KB
cache_alignment: 64

Počítač SOL5
AVG CPU CLOCKS FOR DEPENDENT INDEPENDENT
rdtsc 6.501 6.501
rdtsc 6.501 6.501
32bit add 1.000 0.336
32bit add 1.000 0.336
32*32->64bit imul 3.002 1.044
32*32->64bit imul 3.002 1.044
0*0 imul 3.002
0*0 imul 3.002
64/32->32.32 div 39.049
64/32->32.32 div 39.049
64bit fadd 2.959 1.006
64bit fadd 2.959 1.006
64bit NaN+x fadd 4.041 1.005
64bit NaN+x fadd 4.041 1.006
64bit fmul 4.000 1.006
64bit fmul 4.000 1.003
64bit fdiv 22.513 21.002
64bit fdiv 22.513 21.002
32bit load from L1-cache 0.563 pitch=4B
32bit load from L1-cache 0.563 pitch=4B
typical L1-miss load 54.030 pitch=74B
typical L1-miss load 54.026 pitch=74B
load from RAM 154.00ns 337.403 pitch=1048572B
load from RAM 154.42ns 338.324 pitch=1048572B
load from RAM 123.57ns 270.732 pitch=1048572B (1-pass cache flush)
load from RAM 133.53ns 292.557 pitch=1048572B (1-pass cache flush)

Počítač GAIA (2jádro):

model name: Intel® Pentium® 4 CPU 3.00GHz
cpu MHz: 2992.602
cache size: 1024 KB

Počítač GAIA
AVG CPU CLOCKS FOR DEPENDENT INDEPENDENT
rdtsc 100.001 99.999
32bit add 0.999 0.383
32*32->64bit imul 11.035 2.045
0*0 imul 11.034
64/32->32.32 div 76.320
64bit fadd 5.259 1.355
64bit NaN+x fadd 1100.139 1077.490
64bit fmul 7.998 2.151
64bit fdiv 45.025 45.075
32bit load from L1-cache 1.061 pitch=4B
typical L1-miss load 84.591 pitch=74B
load from RAM 124.01ns 371.102 pitch=1048572B
load from RAM 61.85ns 185.087 pitch=1048572B (1-pass cache flush)

Počítač VICTORIA (1jádro):

model name: Intel® Pentium® 4 CPU 2.66GHz
cpu MHz: 2660.310
cache size: 512 KB

Počítač VICTORIA
AVG CPU CLOCKS FOR DEPENDENT INDEPENDENT
rdtsc 80.081 80.001
32bit add 0.501 0.336
32*32->64bit imul 18.051 10.046
0*0 imul 18.052
64/32->32.32 div 57.157
64bit fadd 4.590 1.131
64bit NaN+x fadd 965.619 944.172
64bit fmul 7.000 2.135
64bit fdiv 43.011 43.066
32bit load from L1-cache 1.427 pitch=4B
typical L1-miss load 52.012 pitch=74B
load from RAM 118.07ns 314.090 pitch=1048572B
load from RAM 129.22ns 343.757 pitch=1048572B (1-pass cache flush)

Počítač OLDATHLON (1jádro):

model name : AMD Athlon™ XP 1800+
cpu MHz : 1530.999
cache size : 256 KB

Počítač OLDATHLON
AVG CPU CLOCKS FOR DEPENDENT INDEPENDENT
rdtsc 11.001 11.001
32bit add 1.000 0.416
32*32->64bit imul 6.067 3.019
0*0 imul 6.051
64/32->32.32 div 39.427
64bit fadd 3.052 1.003
64bit NaN+x fadd 4.040 1.004
64bit fmul 4.000 1.003
64bit fdiv 22.626 21.002
32bit load from L1-cache 0.655 pitch=4B
typical L1-miss load 53.064 pitch=74B
load from RAM 247.98ns 379.665 pitch=1048572B
load from RAM 231.62ns 354.610 pitch=1048572B (1-pass cache flush)

Interpretace výsledků a postřehy

Na první pohled je vidět převaha AMD v množství vykonané práce na takt procesoru (zvláště u násobení a dělení). Zajímavou výjimkou je počítač VICTORIA (Intel P4), který dovede provést dvě závislá sčítání během jednoho taktu. Je to tím, že jeho sčítačka umí pracovat jak v náběžné, tak i v sestupné hraně hodin. V případě, že jsou data nezávislá, už lepší není, protože dovede spustit stejně jen tři instrukce zároveň, což dává stejný výkon, jako má většina ostatních procesorů.

Řádek 0*0 imul testuje rychlost násobení „nula krát nula“. Na některých starších procesorech (i386) nebo třeba na ARM-7TDMI závisí doba násobení na číslech, která násobíme, přičemž 0*0 je nejrychlejší. Je vidět, že u nových procesorů tomu tak není.

Je pozoruhodné, že RDTSC trvá u některých Intelů až sto taktů. Přitom nevidím důvod, proč by musela být delší než jeden takt, když je to vlastně jen přečtení registru TSC čítače do %edx:%eax.

Jak je z tabulek vidět, latence pamětí je pořád poměrně dlouhá a od dob 8bitových počítačů se na rozdíl od přenosové rychlosti příliš nezlepšila (ATARI 800 XE mělo latenci paměti 559 ns z pohledu programátora, z pohledu HW spíš něco jako 260 ns).

Je vidět, že i FPU jednotka je u AMD lepší, jelikož je celkově rychlejší pro závislé instrukce a pro nezávislé má dokonce repeat-rate pro násobení rovno jedné. I nejlepší Xeon, který jsem testoval, měl repeat-rate dvě (nicméně Intely můžou být lepší v SSE, což jsem neměřil).

Řádek NaN+x fadd udává dobu instrukce fadd, která dostala jako operand NaN (Not a Number – výsledek operace 0/0). Jak je vidět, u AMD je pak latence (ale nikoliv repeat-rate) o jeden takt delší. Naproti tomu u Intelu provedení jedné takové instrukce může trvat až 1100 taktů (počítač GAIA). Důvod je mi neznámý, jen vím, že FPU je možné nastavit tak, že buď počítá s NaNy a Infy, nebo vyhodí výjimku. Vypadá to, jako by ji Linux chytil a do správného FPU registru strčil NaN jako výsledek a zase se vrátil, čímž by emuloval IEEE aritmetiku. Proč to není implementováno přímo, to netuším. Je to poměrně zrádné chování, představte si real-time program, který má zpracovávat nějaká data, ve kterých se občas objeví blok Infu nebo NaNu – rázem to běží tisíckrát pomaleji! Takovou chybu může být poměrně těžké objevit dřív, než způsobí nějakou škodu.

Pokud někdo víte, čím to přesně je, napište to do diskuze. Úplně nejlepší by bylo, kdyby někdo znal univerzální „zaříkadlo“, které napíšu na začátek svého programu a které mi nastaví FPU tak, aby se výjimky negenerovaly a fungovalo to jako u AMD.

Dále je vidět, že novější Intely (FIREBALL1) mají chytřejší cache, která se nedá vyprázdnit pomoci „1pass cache flush“, kdežto AMD se takto nechá zmást. Sice má SOL5 již určitý náznak zlepšení, ale k FIREBALL1 má ještě daleko.

Zajímavý je repeat-rate 0,56 pro čtení z L1-cache u počítače SOL5 (a 0,65 pro OLDATHLON). Znamená to, že se většinou podařilo v (*) provádět všechny tři instrukce zároveň. Intely toto neumějí.

Ještě zajímavější ale je, že starší verze testovacího programu, která pro přesun dvou čísel v (*) místo tří instrukci používala čtyři tímto způsobem (před vstupem do bloku bylo %esi=%edi+%ecx):

        movl (%edi), %eax
        addl %ecx, %edi
        movl (%esi), %ebx
        addl %ecx, %esi

dávala následující výsledky:

Počítač SOL5

Počítač SOL5 podruhé
AVG CPU CLOCKS FOR DEPENDENT INDEPENDENT
32bit load from L1-cache 0.838 pitch=4B
32bit load from L1-cache 0.838 pitch=4B
typical L1-miss load 28.592 pitch=74B
typical L1-miss load 17.222 pitch=74B
load from RAM 75.53ns 165.475 pitch=1048572B
load from RAM 53.00ns 116.128 pitch=1048572B
load from RAM 63.39ns 138.893 pitch=1048572B (1-pass cache flush)
load from RAM 44.59ns 97.691 pitch=1048572B (1-pass cache flush)

Počítač FIREBALL1

Počítač FIREBALL1 podruhé
AVG CPU CLOCKS FOR DEPENDENT INDEPENDENT
32bit load from L1-cache 1.002 pitch=4B
typical L1-miss load 37.818 pitch=74B
load from RAM 74.27ns 222.243 pitch=1048572B
load from RAM 4.96ns 14.855 pitch=1048572B (1-pass cache flush)

Počítač GAIA

Počítač GAIA podruhé
AVG CPU CLOCKS FOR DEPENDENT INDEPENDENT
32bit load from L1-cache 1.042 pitch=4B
typical L1-miss load 43.628 pitch=74B
load from RAM 61.45ns 183.892 pitch=1048572B
load from RAM 106.77ns 319.524 pitch=1048572B (1-pass cache flush)

Počítač OLDATHLON

Počítač OLDATHLON podruhé
AVG CPU CLOCKS FOR DEPENDENT INDEPENDENT
32bit load from L1-cache 1.004 pitch=4B
typical L1-miss load 28.082 pitch=74B
load from RAM 110.94ns 169.892 pitch=1048572B
load from RAM 110.07ns 168.561 pitch=1048572B (1-pass cache flush)

Je vidět že zde už se AMDčku tolik nedařilo spouštět čtení najednou, na druhou stranu ale máme téměř dvojnásobné zrychlení čtení z paměti, a to u všech procesorů. Důvod pro toto podivné chování neznám, mám hypotézu, že procesor nebo řadič paměti možná dělá spekulativní načítání dat. Když mám čtecí instrukce rozmístěné tak, že se provádí jedna za druhou s rostoucími adresami, tak z toho pozná, jakou asi další část budu z paměti chtít, a snaží se ji připravit do cache. Když je ale provádím najednou, je možné, že požadavky přicházejí víceméně náhodně (jak to procesoru zrovna vyjde při skládání a přehazování instrukce). To pak nejspíš zmate prediktor v určení vhodné adresy pro spekulativní načtení. Pokud máte někdo lepší vysvětlení, napište ho do diskuse. Původní chování se zapne definováním makra OLDVERSION a je přeloženo do binárky oldcpubench.

V každém případě je nová verze lepší pro otestování latence RAM. Dokonce se variabilita času snížila (jak je vidět z výsledků SOL5). Také zmizelo abnormální chování počítače GAIA, kdy načítání po „1pass cache flush“ trvalo déle než po důkladném vyprázdnění cache. To vše naznačuje, že jsme blíž k měření latence než ve staré verzi. Pouze se vtírá otázka, jestli skutečná latence není náhodou ještě třeba dvakrát větší a pouze se to teď šťastnou náhodou neprojevilo.

Poučení pro programátory (ano, to je úplně zbytečné…)

V „hot spotech“ programu je dobře řídit se těmito pravidly:

(1) Pokud násobíte integery, rozdělte kód tak, aby zároveň s násobením (které má latenci 3, 5, 6, 12 nebo 18 taktů podle typu procesoru) měl kompilátor šanci rozvrhnout nějaké nezávislé instrukce a výsledek násobení byl potřeba ideálně až za 18 taktů. Násobení a*(2^n) se to netýká, protože ho kompilátor přepíše na a<<n.

(2) Vyhýbejte se celočíselnému dělení, pokud to jde. Je pomalejší než floatové.

(3) Při práci s floaty místo (a+b)/2 radši pište 0.5*(a+b), nebo zkontrolujte v assembleru, že kompilátor první zápis sám převede na druhý.

(4) Nedopusťte, aby se vám na vstup dostaly NaNy nebo Infy (většinou to stejně znamená chybu), těch pár případů, kdy se to velmi hodí, je asi lepší oželet než riskovat sto- až tisícinásobné zpomalení programu.

(5) Při hashovaní typu „open addressing“ bývá linear-probing často rychlejší než double-hashing. Je to proto, že porovnání klíčů je řádově rychlejší než cache-miss. Takže zatímco double-hashing má při téměř každém přístupu tendenci tahat novou cache-line z paměti (nebo alespoň z cache další úrovně), linear-probing to udělá jen jednou a pak už může celkem rychle porovnávat. Řetízek je sice o něco delší, ale načtení dat do registru je řádově rychlejší.

(6) Pokud máte algoritmus, který prochází nějakou velkou datovou strukturu (například nějaký velký graf) víceméně náhodně, snažte se ho nahradit něčím jiným (třeba ho počítejte on-the-fly), nebo alespoň v paměti strukturu uspořádejte tak, jak k ní bude pravděpodobně přistupováno. Zrychlení může být klidně desetinásobné (což se například při paralelizaci některých algoritmů na nekonečně mnoho procesorů považuje za úspěch).

(7) Problémům s cache lze částečně ulehčit rozumným používáním instrukce PREFETCH, která načte data do cache ještě dřív, než budou opravdu potřeba.

(8) Pokud dosáhnete 50% rychlosti oproti teoretickému špičkovému výkonu z tohoto testu, nemá cenu dál optimalizovat – lepší už to nebude (co na jednom procesoru získáte, na jiném pokazíte). Mimo hot spot nemá cenu špinit si ruce jakýmkoliv optimalizováním, které za vás neudělá kompilátor.

bitcoin_skoleni

Závěr

Rozdíl 50 % ve výkonu CPU (a troufám si říct, že i v aplikačním testu – pokud jedinou vaší aplikaci nebude ZIP a Quake) vůbec nic neznamená. Čili, nenechte se obalamutit „nezávislými testy CPU“. Velice záleží na tom, jak přesně je program přeložen, kde fyzicky v RAM sídlí a na mnoha dalších subtilních vlivech.

Pokud jste někdo šťastným majitelem procesoru AMD Phenom nebo Intel Quadcore, neváhejte, změřte a vložte výsledky do diskuze pod článkem. Doufám, že nemusím připomínat, že za chování měřicího programu neručím. Je možné ho dál šířit ve smyslu licence GNU GPL.

Autor článku