0xDEADBEEF

RSS odkazy english edition
««« »»»

Propustnost dvoukanálových pamětí

30. 1. 2022 #paměť #benchmark #CPU

Někdo mě nedávno požádal, jestli bych poradil s upgradem pamětí. Proč ne, žejo? Rád pomůžu. A tak jsem se začal dívat, jak jsou na tom různé RAMky s kompatibilitou. Jako obvykle jsem ale zašel příliš daleko a strávil netriviální množství času čtením o interním životě DIMM DRAM.

V manuálech Intelu se u Dual-Channel Symmetric Mode píše:

Addresses are ping-ponged between the channels after each cache line (64-byte boundary).

Tedy, v případě, že mám víc než jeden RAM modul4 a celek běží v dvou-kanálovém módu, liché cache line žijí na jednom kanálu a sudé na druhém. To dává smysl. Při sekvenčním čtení se využijí oba kanály, procesor s RAM komunikuje na úrovni 64 B cache line a tohle rozdělení je tak efektivnější než hrubší granularita. A i když se data čtou náhodně, je velká pravděpodobnost, že oba kanály najdou uplatnění. Rozhodně lepší, než kdyby dolní polovina fyzické paměti žila na jednom kanále a horní na druhém.

Jestli tomu tak skutečně je, toto chování by se dalo snadno otestovat programem, který čte data se skokem 64, 128, 192 atd. bajtů a měří datovou propustnost. Něco jako tohle:

import core.sys.posix.stdlib;
import core.sys.linux.sys.mman;
import std.datetime.stopwatch;
import std.stdio;

void main() {
  const step = 64;
  const size = 1UL << 30;

  void* buf;
  assert(posix_memalign(&buf, 1 << 21, size) == 0);
  madvise(buf, size, MADV_HUGEPAGE);

  auto arr = (cast(ubyte*) buf)[0 .. size];
  arr[] = 0;

  auto timer = StopWatch(AutoStart.yes);

  auto sink = 0;

  foreach (j; 0 .. step) {
    for (auto i = j % 64; i < arr.length; i += step) {
      sink += arr[i];
    }
  }

  auto bytes = arr.length * 64; // 64 byte cache line

  auto sec = timer.peek.total!"msecs" / 1000.0;
  writeln("step = ", step, "; ", sec, "s; ", bytes / sec / 1e9, " GB/s");
  writeln(sink);
}

Kouzla s posix_memalign a madvise (podle tohoto článku) zajistí, aby Linux alokoval velké stránky paměti a test nebyl tolik zasažený výpadky dTLB. perf dosvědčuje, že skutečně dojde k redukci dTLB miss, ale dopad na výsledné časy to (v tomto případě) má jen omezený1 .

Naměřené rychlosti znázorňuje následující graf. Na ose x je vynesen skok mezi přečtenými daty (64 = přečte každou cache line, 128 = přeskakuje jednu) a na ose y rychlost čtení v GB/s.

Jeden DDR3-1333 modul má propustnost 10.6 GB/s. Takové je i maximum v situaci, kdy mám zapojený jen jeden modul a ten (pochopitelně) jede jednokanálově. Ve dvoukanálovém módu, je to 16 GB/s - méně než dvojnásobek, přesto docela fajn zrychlení.2

Když načítám každou druhou cache line (128), propustnost spadne na jednokanálové rychlosti, přesně jak jsem čekal.

Pokud přeskakuju 2 cache line (192) a mělo by docházet k vytížení obou kanálů, rychlost spadne ještě níž.

Jestli ale skáču přes 5 a víc cache line, rychlost najednou o něco vyskočí.

Vysvětlení jako obvykle poskytne perf. Test provádí miliardu iterací, dotkne se miliardy cache line s minimální až žádnou lokalitou a perf to potvrzuje, když hlásí mírně přes miliardu událostí LLC-loads a offcore_requests.demand_data_rd. LLC-loads udává, kolikrát program požadoval data z L3 a offcore_requests.demand_data_rd pak kolik instrukcí explicitně chtělo data, pro které muselo opustit procesorové jádro.

Zajímavé jsou události LLC-load-misses a offcore_requests.all_data_rd (čísla v miliardách):

stepcyclesLLC-load-missesoffcore_requests.all_data_rd
6420.7270.3811.078
12824.3420.5711.305
19228.3630.9221.389
25629.0941.0171.432
32029.1981.0671.402
38426.2671.0741.074
44826.5081.0741.075

LLC-load-misses měří kolik požadavků neobsloužila L3. Vypadá to, že při malých krocích prefetcher stíhá data sypat z RAM do L3 s částečným předstihem a procesor tam občas najde, co hledá. Jak se krok zvětšuje, prefetcher stíhá méně a méně, až nakonec každý dotaz do L3 skončí neúspěchem a musí cestovat až do paměti. To by vysvětlilo postupné zpomalování.

offcore_requests.all_data_rd udává kolik požadavků čtení (jak vyžádaných, tak spekulativních prefetchů) procesor poslal ven do uncore, v našem případě do RAM.3 Tohle číslo je někdy větší než jedna miliarda.

Podle mě jde o práci prefetcheru. Intelí procesory jich mají hned několik (optimizační manuál, sekce 2.3.5.4):

Třetí jmenovaný mám způsobuje problémy.

V běžném provozu na dvoukanálových pamětech dává perfektní smysl. Když program požaduje jednu cache line, ta žije na jednom kanále a s vekou pravděpodobností druhý kanál v tu chvíli bude ležet ladem. Takhle procesor využije dostupnou kapacitu a může to vést ke zrychlení.

Pokud pravidelně přeskakuju aspoň jednu cache line, prefetcher se snaží načíst cache line do páru, která nebude nikdy použitá. Když je skok větší než určitá hranice, spatial prefetcher přestane pracovat, přestane plýtvat zdroji a to zrychlí smyčku.

Můžu si to ověřit přes perf stat -M DRAM_BW_Use ./a.out:

stepnaměřenoDRAM_BW_Use
6416.354 GB/s16.44
12810.604 GB/s18.82
1929.039 GB/s19.31
2568.807 GB/s19.47
3208.775 GB/s19.06
3849.768 GB/s9.93
4489.815 GB/s10.01
5129.826 GB/s9.97

Reálně je DRAM vytížená na maximum, prefetch dělá, co může, jen v tomhle případě jeho aktivita směřuje špatným směrem.

Takže nakonec jsem prvotní domněnku nepotvrdil. K RAM nemá smysl přistupovat jinak než k magické krabici, která poskytne data s velkou propustností, ale velkou latencí, ale jakákoli vnitřní struktura je zcela maskovaná cache pamětmi, prefetcherem, TLB a dalšími detaily, které žijí na straně procesoru. Pro maximální výkon by se měly brát v potaz tyhle věci, ne CAS, RAS latence, délka řádku DIMM, počet ranků a podobné5 .

Na druhou stranu jsem se dozvěděl několik jiných zajímavostí. To je taky pozitivum. Hlavní z nich je to, že je užitečné vědět kolik cache line za vteřinu může člověk ideálně pohnout na daném stroji. U mě to je to jedna cache line každých 20 taktů. To se hodí pro odhad, jak rychle může běžet smyčka limitovaná jen pamětí.


  1. Ale samotná alokace 1 GB fyzické paměti je rychlejší s velkými stránkami. Na mém stroji téměř 3x (133 ms vs. 358 ms).
  2. Absolutní čísla nejsou nic moc, ale odpovídají době, kdy jsem stroj stavěl. Nové DDR4-3200 zvládají 25.6 GB/s na kanál, DDR5-6400 dvakrát tolik a za ne úplně gigantické ceny se dají koupit procesory podporující 4 nebo 8 kanálů.
  3. Intel do uncore může počítat i L3, teď si nejsem úplně jistý.
  4. V manuálech se taky píše o Flex Memory Technology Mode, oportunistickém módu, kdy každý kanál může být osazený různým množstvím paměti a v dvoukanálovém množství běží tolik gigabajtů, kolik mají oba kanály společné. Zbytek, o nějž jeden kanál přečuhuje, běží jednokanálově. Takhle můžu mít 8 + 8 + 4 GB a většina z toho běží 2x rychlostí.
  5. Informace o nainstalovaných DIMM modulech na linuxu poskytnou příkazy dmidecode --type 17 nebo decode-dimms (info)
píše k47 (@kaja47, k47)