Powrót do strony głównej

Spekulatywne wykonywanie przeciw branchless kodowi

Artykuł analizuje wpływ branchless kodu na wydajność z uwzględnieniem spekulatywnego wykonania i hierarchii cache. Przynoszone są przykłady pętli, wyszukiwania i wyszukiwania binarnego z kodem. Rekomendacje wyboru podejścia w zależności od lokalizacji danych.

Branchless kod vs spekulacja: kto szybszy na L3 i DRAM?
Advertisement 728x90

Wykonanie spekulacyjne i kod bez rozgałęzień: ukryte kompromisy wydajności

Współczesne procesory wykonują miliardy operacji na sekundę, ale prędkość dostępu do pamięci rośnie wolniej. Ta luka sprawia, że oczekiwanie na dane staje się głównym źródłem strat wydajności. Procesory ewoluowały od pasywnych wykonawców do aktywnych menedżerów przepływu danych: wykonanie poza kolejnością, przeorganizowanie zależności i wykonanie spekulacyjne.

Przewidywanie rozgałęzień pozwala uruchamiać ładowania z wyprzedzeniem, ukrywając opóźnienie pamięci. Błąd przewidywania kosztuje 10–20 taktów, ale oczekiwanie z L3 lub DRAM — setki. Kod bez rozgałęzień eliminuje błędne przewidywania, ale pozbawia procesor możliwości spekulacji.

Ewolucja od sekwencji do spekulacji

Wczesne systemy wykonywały instrukcje ściśle sekwencyjnie. IBM System/360 Model 91 wprowadził algorytm Tomasulo do wykonania poza kolejnością: instrukcje startują, gdy operandy są gotowe.

Google AdInline article slot

Wykonanie spekulacyjne pojawiło się później w superskalarnych procesorach takich jak Intel Pentium Pro (1995). Potok na 40 mikrooperacji utrzymuje dziesiątki instrukcji w locie, przewidując rozgałęzienia na podstawie historii.

for (size_t i = 0; i < n; i++) {
    if (arr[i] >= 0) {
          ... code_true ...;
    } else {
        ... code_false ...;
    }
}

Tutaj załadowanie arr[i] może spowodować przestój na 10–100+ taktów, jeśli danych nie ma w L1. Procesor przewiduje gałąź, zaczyna code_true, inicjuje prefetch. Przy błędzie — flush i restart.

Branchless vs branchful: zależność od pamięci podręcznej

Uproszczenie z rozgałęzieniem:

Google AdInline article slot
if (arr[i] > 0) {
    result++;
}

Staje się:

result += (arr[i] > 0);
  • Dane w L1: branchless wygrywa — brak błędnych przewidywań, minimalne opóźnienie.
  • L2: neutralnie, koszt błędu przewidywania jest mały.
  • L3/DRAM: branchful szybszy — spekulacja uruchamia prefetch z wyprzedzeniem, ukrywając opóźnienie.

Branchless przenosi opóźnienie na ścieżkę krytyczną: procesor czeka na dane przed porównaniem, bez nakładania obliczeń.

Wyszukiwanie wartości: koszt bezwarunkowych ładowań

Oryginał:

Google AdInline article slot
Value result = default_value;
for (size_t i = 0; i < n; ++i) {
    if (keys[i] == target) {
        result = values[i];
    }
}
return result;

Branchless:

Value result = default_value;
for (size_t i = 0; i < n; ++i) {
    result = (keys[i] == target) ? values[i] : result;
}
return result;

W branchless values[i] jest ładowane w każdej iteracji, zużywając przepustowość na całą tablicę. Oryginał czyta tylko do dopasowania.

Wyszukiwanie binarne: łańcuchy zależności

Branchful:

while (low < high) {
    size_t mid = low + (high - low) / 2;
    if (keys[mid] < target)
        low = mid + 1;
    else
        high = mid;
}

Procesor spekulacyjnie oblicza następny mid i prefetch keys[mid].

Branchless:

while (low < high) {
    size_t mid = low + (high - low) / 2;
    size_t step = (high - low) / 2;
    low  = (keys[mid] < target) ? mid + 1 : low;
    high = (keys[mid] < target) ? high    : mid;
}

Zależności od wyniku porównania blokują spekulację: następny mid czeka na zapis low/high.

  • Połowa przewidywań jest błędna, ale wczesny prefetch nakłada się na flush.
  • Branchless: więcej instrukcji, późne ładowania, jawne opóźnienia pamięci.

Co jest ważne

  • Spekulacja ukrywa opóźnienie pamięci, uruchamiając prefetch według przewidywanych gałęzi.
  • Branchless eliminuje błędne przewidywania, ale pogarsza wydajność przy dostępie do L3/DRAM.
  • Profiluj na docelowym sprzęcie: L1 — branchless, duże tablice — branchful.
  • Porty konsolowe gier często cierpią z powodu ignorowania tych efektów.
  • Zależności w łańcuchach branchless blokują optymalizacje OoO.

Przy losowych danych w L1 branchless dominuje. Przy wzorcach lub dużych wolumenach branchful wygrywa dzięki spekulacyjnym ładowaniom.

— Editorial Team

Advertisement 728x90

Czytaj dalej