Enhanced REP MOVSB for memcpy

Chciałbym użyć enhanced REP MOVSB (ERMSB), aby uzyskać wysoką przepustowość dla niestandardowego memcpy.

ERMSB został wprowadzony wraz z mikroarchitekturą Ivy Bridge. Jeśli nie wiesz, czym jest ERMSB, zapoznaj się z sekcją "Enhanced REP movsb and STOSB operation (ERMSB)" w Intel optimization manual.

Jedyny sposób, jaki znam, aby zrobić to bezpośrednio, to wbudowany montaż. Otrzymałem następującą funkcję od https://groups.google.com/forum/#! topic / gnu.GCC. help/ - Bmlm_EG_fE

static inline void *__movsb(void *d, const void *s, size_t n) {
  asm volatile ("rep movsb"
                : "=D" (d),
                  "=S" (s),
                  "=c" (n)
                : "0" (d),
                  "1" (s),
                  "2" (n)
                : "memory");
  return d;
}

Kiedy jednak tego używam, przepustowość jest znacznie mniejsza niż w przypadku memcpy. __movsb dostaje 15 GB / s i memcpy dostać 26 GB / s z moim systemem i7-6700HQ (Skylake), Ubuntu 16.10, DDR4@2400 MHz dual channel 32 GB, GCC 6.2.

Dlaczego przepustowość jest o wiele niższa z REP MOVSB? Co mogę zrobić, aby go ulepszyć?

Oto kod, którego użyłem do przetestowania tego.

//gcc -O3 -march=native -fopenmp foo.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stddef.h>
#include <omp.h>
#include <x86intrin.h>

static inline void *__movsb(void *d, const void *s, size_t n) {
  asm volatile ("rep movsb"
                : "=D" (d),
                  "=S" (s),
                  "=c" (n)
                : "0" (d),
                  "1" (s),
                  "2" (n)
                : "memory");
  return d;
}

int main(void) {
  int n = 1<<30;

  //char *a = malloc(n), *b = malloc(n);

  char *a = _mm_malloc(n,4096), *b = _mm_malloc(n,4096);
  memset(a,2,n), memset(b,1,n);

  __movsb(b,a,n);
  printf("%d\n", memcmp(b,a,n));

  double dtime;

  dtime = -omp_get_wtime();
  for(int i=0; i<10; i++) __movsb(b,a,n);
  dtime += omp_get_wtime();
  printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);

  dtime = -omp_get_wtime();
  for(int i=0; i<10; i++) memcpy(b,a,n);
  dtime += omp_get_wtime();
  printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);  
}

Powód I jestem zainteresowany rep movsb opiera się na tych komentarzy

Zauważ, że na Ivybridge i Haswell, z buforami do dużych, aby zmieścić się w MLC, możesz pokonać movntdqa używając rep movsb; movntdqa wprowadza RFO do LLC, rep movsb nie... rep movsb jest znacznie szybszy niż movntdqa podczas przesyłania strumieniowego do pamięci na Ivybridge i Haswell (ale pamiętaj, że pre-Ivybridge jest powolny!)

Czego brakuje/poniżej optymalnego w tej memcpy wdrożenie?


Oto moje wyniki na tym samym systemie z tinymembnech.

 C copy backwards                                     :   7910.6 MB/s (1.4%)
 C copy backwards (32 byte blocks)                    :   7696.6 MB/s (0.9%)
 C copy backwards (64 byte blocks)                    :   7679.5 MB/s (0.7%)
 C copy                                               :   8811.0 MB/s (1.2%)
 C copy prefetched (32 bytes step)                    :   9328.4 MB/s (0.5%)
 C copy prefetched (64 bytes step)                    :   9355.1 MB/s (0.6%)
 C 2-pass copy                                        :   6474.3 MB/s (1.3%)
 C 2-pass copy prefetched (32 bytes step)             :   7072.9 MB/s (1.2%)
 C 2-pass copy prefetched (64 bytes step)             :   7065.2 MB/s (0.8%)
 C fill                                               :  14426.0 MB/s (1.5%)
 C fill (shuffle within 16 byte blocks)               :  14198.0 MB/s (1.1%)
 C fill (shuffle within 32 byte blocks)               :  14422.0 MB/s (1.7%)
 C fill (shuffle within 64 byte blocks)               :  14178.3 MB/s (1.0%)
 ---
 standard memcpy                                      :  12784.4 MB/s (1.9%)
 standard memset                                      :  30630.3 MB/s (1.1%)
 ---
 MOVSB copy                                           :   8712.0 MB/s (2.0%)
 MOVSD copy                                           :   8712.7 MB/s (1.9%)
 SSE2 copy                                            :   8952.2 MB/s (0.7%)
 SSE2 nontemporal copy                                :  12538.2 MB/s (0.8%)
 SSE2 copy prefetched (32 bytes step)                 :   9553.6 MB/s (0.8%)
 SSE2 copy prefetched (64 bytes step)                 :   9458.5 MB/s (0.5%)
 SSE2 nontemporal copy prefetched (32 bytes step)     :  13103.2 MB/s (0.7%)
 SSE2 nontemporal copy prefetched (64 bytes step)     :  13179.1 MB/s (0.9%)
 SSE2 2-pass copy                                     :   7250.6 MB/s (0.7%)
 SSE2 2-pass copy prefetched (32 bytes step)          :   7437.8 MB/s (0.6%)
 SSE2 2-pass copy prefetched (64 bytes step)          :   7498.2 MB/s (0.9%)
 SSE2 2-pass nontemporal copy                         :   3776.6 MB/s (1.4%)
 SSE2 fill                                            :  14701.3 MB/s (1.6%)
 SSE2 nontemporal fill                                :  34188.3 MB/s (0.8%)

Zauważ, że na moim systemie SSE2 copy prefetched jest również szybszy niż MOVSB copy.


W moich oryginalnych testach nie wyłączyłem turbo. Wyłączyłem turbo i testowałem ponownie i nie wydaje się to robić dużej różnicy. Jednak zmiana zarządzania energią ma duże znaczenie.

Kiedy to zrobię

sudo cpufreq-set -r -g performance

Czasami widzę ponad 20 GB / s z rep movsb.

Z

sudo cpufreq-set -r -g powersave

Najlepsze, jakie widzę, to około 17 GB/s. Ale memcpy nie wydaje się być wrażliwy na zarządzanie energią.


Sprawdziłem częstotliwość (używając turbostat) z włączonym i bez SpeedStep, z performance i z powersave dla biegu jałowego, obciążeniem 1-rdzeniowym i obciążeniem 4-rdzeniowym. Uruchomiłem mnożenie macierzy gęstej MKL Intela, aby utworzyć obciążenie i ustawić liczbę wątków za pomocą OMP_SET_NUM_THREADS. Oto tabela wyników (liczby w GHz).

              SpeedStep     idle      1 core    4 core
powersave     OFF           0.8       2.6       2.6
performance   OFF           2.6       2.6       2.6
powersave     ON            0.8       3.5       3.1
performance   ON            3.5       3.5       3.1

To pokazuje, że z powersave nawet z SpeedStep wyłącza procesor / align = "left" / Tylko z performance BEZ SpeedStep procesor pracuje ze stałą częstotliwością.

Użyłem np sudo cpufreq-set -r performance (ponieważ cpufreq-set dawało dziwne wyniki), aby zmienić ustawienia mocy. To włącza turbo z powrotem, więc musiałem wyłączyć turbo po.

Author: Z boson, 2017-04-11

6 answers

Jest to temat bliski mojemu sercu i niedawnym badaniom, więc przyjrzę się mu pod kilkoma kątami: historia, kilka uwag technicznych (głównie akademickich), wyniki testów na moim pudełku, a na koniec próba odpowiedzi na twoje pytanie, kiedy i gdzie {2]} może mieć sens.

Częściowo jest to wywołanie , aby udostępnić wyniki - jeśli możesz uruchomić Tinymembench i udostępnić wyniki wraz ze szczegółami konfiguracji procesora i pamięci RAM, byłoby świetnie. Zwłaszcza jeśli masz konfigurację 4-kanałową, skrzynkę Ivy Bridge, skrzynkę serwera itp.

Historia i oficjalne porady

Historia wydajności szybkich instrukcji kopiowania ciągów była trochę sprawą schodową - tj. okresy stagnacji wydajności na przemian z dużymi ulepszeniami, które wprowadziły je do linii lub nawet szybciej niż konkurencyjne podejścia. Na przykład, nastąpił skok wydajności w Nehalem (głównie celowanie kosztów ogólnych) i ponownie w Ivy Bridge (większość celowania ogółem przepustowość dla dużych kopii). W tym wątku można znaleźć dziesięcioletni wgląd w trudności związane z implementacją instrukcji rep movsod inżyniera Intela.

Na przykład w przewodnikach poprzedzających wprowadzenie Ivy Bridge, typową radą jest unikanie ich lub używanie ich bardzo ostrożnie1. [85]} obecny (dobrze, czerwiec 2016) przewodnik ma wiele mylących i nieco niespójnych porad, takich jak2:

Konkretny wariant realizacji jest wybierany w czasie realizacji na podstawie układu danych, wyrównania i wartości licznika (ECX). Na przykład, MOVSB/STOSB z prefiksem REP powinien być używany z licznikiem wartość mniejsza lub równa 3 dla najlepszej wydajności.

Więc dla kopii 3 lub mniej bajtów? Nie potrzebujesz do tego prefiksu rep, ponieważ przy deklarowanym opóźnieniu uruchamiania wynoszącym ~9 cykli jesteś prawie na pewno lepiej z prostym DWORDEM lub QWORD mov z odrobiną przekręcania bitów, aby zamaskować nieużywane bajty(lub może z 2 jawnymi bajtami, word movs, jeśli wiesz, że rozmiar jest dokładnie trzy).

Mówią dalej:

String MOVE/STORE instrukcje mają wiele ziarnistości danych. Na efektywny przepływ danych, większe ziarnistości danych są preferowane. Oznacza to, że lepszą wydajność można osiągnąć poprzez rozkładanie dowolna wartość licznika na liczbę podwójnych słów plus jeden bajt porusza się z wartość zliczania mniejsza lub równa 3.

To z pewnością wydaje się złe na obecnym sprzęcie z ERMSB, gdzie rep movsb jest co najmniej tak szybki, lub szybszy, niż warianty movd lub movq dla dużych kopii.

Ogólnie rzecz biorąc, ta sekcja (3.7.5) obecnego przewodnika zawiera zestaw rozsądnych i bardzo przestarzałych porad. Jest to powszechna przepustowość podręczników Intela, ponieważ są one aktualizowane w sposób Przyrostowy dla każdej architektury (i rzekomo obejmują prawie dwie dekady wartości stare sekcje często nie są aktualizowane w celu zastąpienia lub wprowadzenia warunkowych porad, które nie mają zastosowania do bieżącej architektury.

Następnie przejdą do ermsb wyraźnie w sekcji 3.7.6.

Nie będę wyczerpująco omawiać pozostałych porad, ale podsumuję dobre części w "Dlaczego z nich korzystać" poniżej.

Inne ważne twierdzenia z przewodnika to to, że na Haswell, rep movsb został ulepszony do korzystania z operacji 256-bitowych wewnętrznie.

Względy Techniczne

Jest to tylko krótkie podsumowanie podstawowych zalet i wad, które mają rep instrukcje z punktu widzenia implementacji .

Zalety dla rep movs

  1. Po wydaniu rep instrukcji movs, procesorwie , że cały blok o znanym rozmiarze ma zostać przesłany. Może to pomóc zoptymalizować działanie w sposób, który nie może z dyskretnym instrukcje, na przykład:

    • unikanie żądania RFO, gdy wie, że cała linia bufora zostanie nadpisana.
    • wysyłanie żądań prefetch natychmiast i dokładnie. Wstępne ustawianie sprzętowe dobrze sprawdza się w wykrywaniu wzorców podobnych do memcpy, ale nadal wymaga kilku odczytów, aby zacząć i "przepełni" wiele linii pamięci podręcznej poza końcem kopiowanego regionu. rep movsb zna dokładnie rozmiar regionu i może prefetch dokładnie.
  2. Najwyraźniej nie ma gwarancji zamawiania wśród sklepów w3 pojedynczy rep movs, który może pomóc uprościć ruch spójny i po prostu inne aspekty ruchu bloków, w porównaniu z prostymi mov instrukcjami, które muszą być zgodne z dość ścisłym porządkiem pamięci4.

  3. W zasadzie Instrukcja rep movs może wykorzystywać różne sztuczki architektoniczne, które nie są eksponowane w ISA. Na przykład, architektury mogą mieć szersze wewnętrzne ścieżki danych, które Isa ujawnia5 i rep movs przydałoby się to wewnętrznie.

Wady

  1. rep movsb musi zaimplementować konkretny semantyczny, który może być silniejszy niż podstawowe wymagania programowe. W szczególności memcpy zabrania nakładania się regionów, więc może ignorować tę możliwość, ale rep movsb dopuszcza je i musi przynieść oczekiwany rezultat. Na obecnych implementacjach wpływa głównie na uruchomienie napowietrznych, ale prawdopodobnie nie do dużych przepustowości bloków. Podobnie, {[2] } musi obsługiwać kopie bajtowo-ziarniste, nawet jeśli używasz go do kopiowania dużych bloków, które są wielokrotnością pewnej dużej mocy 2.

  2. Oprogramowanie może posiadać informacje o wyrównaniu, rozmiarze kopii i możliwym aliasingu, których nie można przekazać sprzętowi, jeśli używa się rep movsb. Kompilatory mogą często określać wyrównanie bloków pamięci6 i tak można uniknąć wielu prac startowych, które rep movs musi wykonywać na każde wywołanie.

Wyniki Testów

Oto wyniki testów dla wielu różnych metod kopiowania z tinymembench na moim i7-6700HQ przy 2.6 GHz (szkoda, że mam identyczny Procesor więc nie dostajemy nowego punktu danych...):

 C copy backwards                                     :   8284.8 MB/s (0.3%)
 C copy backwards (32 byte blocks)                    :   8273.9 MB/s (0.4%)
 C copy backwards (64 byte blocks)                    :   8321.9 MB/s (0.8%)
 C copy                                               :   8863.1 MB/s (0.3%)
 C copy prefetched (32 bytes step)                    :   8900.8 MB/s (0.3%)
 C copy prefetched (64 bytes step)                    :   8817.5 MB/s (0.5%)
 C 2-pass copy                                        :   6492.3 MB/s (0.3%)
 C 2-pass copy prefetched (32 bytes step)             :   6516.0 MB/s (2.4%)
 C 2-pass copy prefetched (64 bytes step)             :   6520.5 MB/s (1.2%)
 ---
 standard memcpy                                      :  12169.8 MB/s (3.4%)
 standard memset                                      :  23479.9 MB/s (4.2%)
 ---
 MOVSB copy                                           :  10197.7 MB/s (1.6%)
 MOVSD copy                                           :  10177.6 MB/s (1.6%)
 SSE2 copy                                            :   8973.3 MB/s (2.5%)
 SSE2 nontemporal copy                                :  12924.0 MB/s (1.7%)
 SSE2 copy prefetched (32 bytes step)                 :   9014.2 MB/s (2.7%)
 SSE2 copy prefetched (64 bytes step)                 :   8964.5 MB/s (2.3%)
 SSE2 nontemporal copy prefetched (32 bytes step)     :  11777.2 MB/s (5.6%)
 SSE2 nontemporal copy prefetched (64 bytes step)     :  11826.8 MB/s (3.2%)
 SSE2 2-pass copy                                     :   7529.5 MB/s (1.8%)
 SSE2 2-pass copy prefetched (32 bytes step)          :   7122.5 MB/s (1.0%)
 SSE2 2-pass copy prefetched (64 bytes step)          :   7214.9 MB/s (1.4%)
 SSE2 2-pass nontemporal copy                         :   4987.0 MB/s

Kilka kluczowych rzeczy:

  • metody rep movs są szybsze niż wszystkie inne metody, które nie są "nie-czasowe"7, i znacznie szybciej niż zbliża się "C" które kopiują 8 bajtów na raz.
  • metody "nie-temporalne" są szybsze, nawet o około 26% od metod rep movs - ale to znacznie mniejsza delta niż ta, którą zgłosiłeś(26 Gb / s vs 15 GB/s = ~73%).
  • jeśli nie używasz magazynów nie-temporalnych, używanie 8-bajtowych kopii z C jest prawie tak samo dobre jak 128-bitowe load/stores SSE. Dzieje się tak dlatego, że dobra pętla kopiująca może wygenerować wystarczająco dużo ciśnienia pamięci, aby nasycić przepustowość (np. 2.6 GHz * 1 store/cycle * 8 bajtów = 26 GB / s dla sklepów).
  • W tinymembench nie ma jawnych 256-bitowych algorytmów (z wyjątkiem prawdopodobnie "standardowego" memcpy), ale prawdopodobnie nie ma to znaczenia ze względu na powyższą uwagę.
  • zwiększona przepustowość nie-czasowego podejścia do przechowywania w stosunku do czasowych wynosi Około 1.45 x, co jest bardzo zbliżone do 1.5 x, którego można oczekiwać, jeśli NT eliminuje 1 z 3 transferów (tj. 1 odczyt, 1 zapis dla NT vs 2 odczyty, 1 zapis). rep movs podejścia leżą pośrodku.
  • The połączenie dość niskiego opóźnienia pamięci i skromnej przepustowości 2-kanałowej oznacza, że ten konkretny układ jest w stanie nasycić przepustowość pamięci z jednego wątku, co zmienia zachowanie dramatycznie.
  • rep movsd wydaje się używać tej samej magii co rep movsb na tym chipie. To ciekawe, ponieważ ERMSB tylko jawnie celuje movsb i wcześniejsze testy na wcześniejszych łukach z ERMSB pokazują movsb działanie znacznie szybciej niż movsd. Jest to głównie akademickie, ponieważ movsb jest bardziej ogólne niż movsd w każdym razie.

Haswell

Patrząc na Haswell wyniki uprzejmie przedstawione przez iwillnotexist w komentarzach, widzimy te same ogólne trendy (najbardziej istotne wyniki wyodrębnione):

 C copy                                               :   6777.8 MB/s (0.4%)
 standard memcpy                                      :  10487.3 MB/s (0.5%)
 MOVSB copy                                           :   9393.9 MB/s (0.2%)
 MOVSD copy                                           :   9155.0 MB/s (1.6%)
 SSE2 copy                                            :   6780.5 MB/s (0.4%)
 SSE2 nontemporal copy                                :  10688.2 MB/s (0.3%)
W przeciwieństwie do Skylake 'a, Skylake nie jest w stanie wytrzymać zbyt dużej prędkości, a w przeciwieństwie do Skylake' a nie jest w stanie wytrzymać zbyt dużej prędkości. Przewaga technik NT nad ich czasowymi kuzynami wynosi teraz ~57%, nawet nieco więcej niż teoretyczna korzyść redukcji przepustowości.

Kiedy należy stosować rep movs?

Wreszcie zadajesz sobie pytanie: kiedy i dlaczego powinieneś go używać? Czerpie z powyższego i wprowadza kilka nowych pomysłów. Niestety nie ma prostej odpowiedzi: będziesz musiał wymienić różne czynniki, w tym niektóre, których prawdopodobnie nie możesz dokładnie wiedzieć, takie jak przyszłe wydarzenia.

Zwróć uwagę, że alternatywą dla rep movsb może być zoptymalizowany libc memcpy (w tym kopie kompilator), lub może to być ręcznie rozwijana wersja memcpy. Niektóre z poniższych korzyści mają zastosowanie tylko w porównaniu do jednej lub drugiej z tych alternatyw (np. "prostota" pomaga w przypadku wersji ręcznie zwijanej, ale nie w przypadku wbudowanej memcpy), ale niektóre odnoszą się do obu.

Ograniczenia dotyczące dostępnych instrukcji

W niektórych środowiskach istnieje ograniczenie dotyczące pewnych instrukcji lub korzystania z niektórych rejestrów. Na przykład w jądrze Linuksa użycie rejestrów SSE/AVX lub FP jest ogólnie zabronione. W związku z tym większość zoptymalizowanych wariantów memcpy nie może być używana, ponieważ opierają się one na rejestrach SSE lub AVX, a zwykła 64-bitowa Kopia mov jest używana na x86. W przypadku tych platform użycie rep movsb pozwala na większość wydajności zoptymalizowanego memcpy bez łamania ograniczeń kodu SIMD.

Bardziej ogólnym przykładem może być kod, który musi być adresowany do wielu generacji sprzętu i który nie używa wysyłania specyficznego dla sprzętu (np. używając cpuid). Proszę. może być zmuszony do używania tylko starszych zestawów instrukcji, co wyklucza wszelkie AVX, itp. rep movsb może być dobrym podejściem, ponieważ umożliwia "Ukryty" dostęp do szerszych ładunków i sklepów bez użycia nowych instrukcji. Jeśli celujesz w sprzęt pre-ERMSB, musisz sprawdzić, czy wydajność rep movsb jest tam akceptowalna...

Future Proofing]}

Miłym aspektem rep movsb jest to, że może, w teorii wykorzystać ulepszenia architektoniczne na przyszłych architekturach, bez zmiany źródła, że jawne ruchy nie mogą. Na przykład, gdy wprowadzono 256-bitowe ścieżki danych, rep movsb był w stanie z nich skorzystać (jak twierdzi Intel) bez żadnych zmian w oprogramowaniu. Oprogramowanie wykorzystujące 128-bitowe ruchy (co było optymalne przed Haswellem) musiałoby zostać zmodyfikowane i przekompilowane.

Jest to więc zarówno korzyść z utrzymania oprogramowania (nie ma potrzeby zmiany źródła), jak i korzyść dla istniejących binariów (nie ma potrzeby wdrażania nowych binariów, aby skorzystać z poprawa).

To, jak ważne jest to, zależy od Twojego modelu konserwacji (np. jak często nowe pliki binarne są wdrażane w praktyce) i bardzo trudnego do oceny, Jak szybkie będą te instrukcje w przyszłości. Przynajmniej Intel jest swego rodzaju przewodnikiem w tym kierunku jednak, zobowiązując się do co najmniej rozsądne wydajność w przyszłości (15.3.3.6):

REP MOVSB i REP STOSB będą nadal działać w miarę dobrze on przyszłych procesorów.

Nakładanie się na kolejne prace

Ta korzyść nie pojawi się oczywiście w prostym memcpy benchmarku, który z definicji nie ma kolejnych prac, które nakładają się na siebie, więc wielkość korzyści musiałaby być starannie mierzona w rzeczywistym scenariuszu. Maksimum korzyści może wymagać zmiany organizacji kodu wokół memcpy.

Na tę korzyść zwraca uwagę firma Intel w podręczniku optymalizacji (sekcja 11.16.3.4) i w ich słowach:

Gdy liczba jest znana jako co najmniej tysiąc bajtów lub więcej, używając enhanced REP MOVSB / STOSB może zapewnić kolejną zaletę amortyzacji koszt nie zużywającego się kodu. Heurystyka może być rozumiana użycie wartości Cnt = 4096 i memset () jako przykład:

• 256-bitowa implementacja SIMD memset() będzie musiała wydać / wykonać wycofanie 128 przypadków 32-bajtowej pracy magazynu z VMOVDQA, przed nie zużywający sekwencje instrukcji mogą przejść do emerytura.

• instancja enhanced REP STOSB o ECX= 4096 jest dekodowana jako długi przepływ mikro-op zapewniany przez sprzęt, ale rezygnuje jako jeden Instrukcja. Istnieje wiele operacji store_data, które muszą zostać zakończone zanim wynik memset() zostanie wykorzystany. Ponieważ dopełnienie operacji przechowywania danych jest odłączany od programu-zlecenie wycofania, a znaczna część nie zużywającego się strumienia kodu może przetwarzać poprzez na wydanie/wykonanie i wycofanie, zasadniczo bez kosztów, jeśli nie zużywająca Sekwencja nie konkuruje o zasoby bufora magazynu.

Więc Intel mówi, że po wszystkich UOPs kod po rep movsb został wydany, ale podczas gdy wiele sklepów jest nadal w locie, a rep movsb jako całość nie wycofała się jeszcze, UOPs z wykonywania instrukcji może zrobić większy postęp przez maszynerię out-of-order niż mogliby gdyby ten kod przyszedł po pętli kopiowania.

Uops z explicit load I store loop muszą faktycznie przejść na emeryturę oddzielnie w kolejności programów. To musi się zdarzyć, aby zrobić miejsce w ROB na śledzenie uops.

Wydaje się, że nie ma zbyt wielu szczegółowych informacji o tym, jak bardzo długo działają mikrokodowane instrukcje, takie jak rep movsb. Nie wiemy dokładnie, w jaki sposób oddziały mikro-kodu żądają innego strumienia UOPs od sekwencera mikrokodu, ani jak uops odchodzi na emeryturę. Jeśli poszczególne uops nie muszą osobno przejść na emeryturę, być może cały Instrukcja zajmuje tylko jedno miejsce w ROB?

Kiedy front-end, który zasila maszynę OoO, widzi instrukcję rep movsb w pamięci podręcznej uop, aktywuje ROM sekwencera mikrokodu (MS-ROM), aby wysłać mikrokody uops do kolejki, która zasila etap wydania/zmiany nazwy. Prawdopodobnie nie jest możliwe, aby jakikolwiek inny uops mieszał się z tym i wydawał/wykonywał8 podczas gdy rep movsb jest nadal wydawane, ale kolejne instrukcje mogą być pobierane/dekodowane i wydawane zaraz po ostatnim rep movsb uop robi, podczas gdy część kopii nie została jeszcze wykonana. Jest to przydatne tylko wtedy, gdy przynajmniej część kodu nie zależy od wyniku memcpy (co nie jest niczym niezwykłym).

Teraz rozmiar tej korzyści jest ograniczony: co najwyżej możesz wykonać N instrukcji (w rzeczywistości uops) poza powolną instrukcją rep movsb, w którym momencie przeciągniesz, gdzie N to Rozmiar ROB . Przy obecnych rozmiarach ROB ~200 (192 na Haswell, 224 na Skylake), to maksymalna korzyść ~200 cykli darmowej pracy dla kolejnego kodu z IPC równym 1. W ciągu 200 cykli możesz skopiować około 800 bajtów z prędkością 10 Gb / s, dzięki czemu w przypadku kopii tego rozmiaru możesz uzyskać bezpłatną pracę zbliżoną do kosztu kopii(dzięki czemu kopia jest bezpłatna).

Ponieważ rozmiary kopii stają się znacznie większe, względne znaczenie tego szybko maleje (np. jeśli zamiast tego kopiujesz 80 KB, Darmowa praca stanowi tylko 1% kosztu kopii). Mimo to jest to dość interesujące jak na skromne egzemplarze.

Pętle kopiowania nie należy też całkowicie blokować wykonywania kolejnych instrukcji. Intel nie wdaje się w szczegóły dotyczące wielkości korzyści, ani tego, jakiego rodzaju kopie lub otaczający kod jest najbardziej korzystny. (Hot or cold destination or source, high ILP or low ILP high-latency code after).

Rozmiar Kodu

Rozmiar wykonanego kodu (kilka bajtów) jest mikroskopijny w porównaniu do typowej, zoptymalizowanej procedury memcpy. Jeśli wydajność jest w ogóle ograniczona przez i-cache (w tym pamięć podręczną uop), Zmniejszony rozmiar kodu może być korzystny.

Ponownie, możemy powiązać wielkość tej korzyści na podstawie wielkości kopii. W rzeczywistości nie będę pracować nad tym numerycznie, ale intuicja jest taka, że zmniejszenie dynamicznego rozmiaru kodu O B bajty może zapisać co najwyżej C * B Cache-misses, dla pewnej stałej C. każde wywołanie do memcpy pociąga za sobą koszt miss cache (lub korzyść) raz, ale zaletą wyższej skali przepustowości z liczbą skopiowanych bajtów. Więc dla dużych transferów, wyższe przepływność zdominuje efekty pamięci podręcznej.

Ponownie, nie jest to coś, co pojawi się w prostym benchmarku, gdzie cała pętla bez wątpienia zmieści się w pamięci podręcznej uop. Do oceny tego efektu potrzebny będzie test w rzeczywistym świecie.

Optymalizacja Specyficzna Dla Architektury

Zgłosiłeś, że na twoim sprzęcie, rep movsb był znacznie wolniejszy niż platforma memcpy. Jednak nawet tutaj istnieją doniesienia o odwrotnym wyniku na wcześniejszym sprzęcie (jak Ivy Most).

Jest to całkowicie prawdopodobne, ponieważ wydaje się, że operacje przenoszenia strun są okresowo popularne - ale nie w każdym pokoleniu, więc może być szybsze lub przynajmniej wiązane (w tym momencie może wygrać na podstawie innych zalet) na architekturach, na których został zaktualizowany, tylko po to, aby pozostawać w tyle w kolejnych sprzętach.

Cytując Andy Glew, który powinien wiedzieć co nieco o tym po zaimplementowaniu ich na P6:

The big słabością wykonywania szybkich ciągów w mikrokodach było [...] the mikrokody wypadały z każdą generacją, coraz wolniej i wolniej, dopóki ktoś go nie naprawił. Just like a library men Kopia wypada z gry. Przypuszczam, że jest możliwe, że jeden z utraconych możliwości było wykorzystanie 128-bitowych obciążeń i sklepów, gdy stał się dostępny, i tak dalej.

W takim przypadku może to być postrzegane jako kolejna" specyficzna dla platformy " optymalizacja do zastosowania w typowych every-trick-in-the-book memcpy procedury można znaleźć w standardowych bibliotekach i kompilatorach JIT: ale tylko do użytku na architekturach, gdzie jest to lepsze. W przypadku skompilowanych elementów JIT lub AOT jest to łatwe, ale w przypadku skompilowanych statycznie binariów wymaga to wysyłki specyficznej dla platformy, ale często już istnieje (czasami zaimplementowane w czasie połączenia), lub argument mtune może być użyty do podjęcia statycznej decyzji.

Prostota

Nawet na Skylake, gdzie wydaje się, że jest w tyle bezwzględnie najszybsze techniki nie-temporalne, jest jeszcze szybsze niż większość podejść i jest bardzo proste . Oznacza to mniej czasu na walidację, mniej tajemniczych błędów, mniej czasu na strojenie i aktualizowanie implementacji monster memcpy (lub odwrotnie, mniejsze uzależnienie od kaprysów implementatorów bibliotek standardowych, jeśli na tym polegasz).

Platformy Związane Z Opóźnieniami

Algorytmy związane z przepustowością pamięci9 może faktycznie działać w dwóch głównych systemy: Wiązanie przepustowości pamięci DRAM lub współbieżność/opóźnienie.

Pierwszy tryb jest Tym, który prawdopodobnie znasz: podsystem DRAM ma pewną teoretyczną przepustowość, którą można łatwo obliczyć na podstawie liczby kanałów, szybkości/szerokości danych i częstotliwości. Na przykład mój system DDR4-2133 z 2 kanałami ma maksymalną przepustowość 2.133 * 8 * 2 = 34.1 GB / s, to samo co zgłoszone na ARK.

Nie wytrzymasz więcej niż ta szybkość z DRAM (i zwykle nieco mniej ze względu na różne nieefektywności) dodawane we wszystkich rdzeniach na gnieździe (tj. jest to globalny limit dla Systemów z pojedynczymi gniazdami).

Drugi limit jest narzucany przez liczbę jednoczesnych żądań, które rdzeń może faktycznie wydać podsystemowi pamięci. Wyobraź sobie, że rdzeń może mieć tylko 1 żądanie na raz, dla 64-bajtowej linii pamięci podręcznej - po zakończeniu żądania możesz wydać kolejne. Załóżmy również bardzo szybkie opóźnienie pamięci 50ns. Wtedy mimo dużej pamięci DRAM 34,1 GB/s przepustowość, w rzeczywistości otrzymałbyś tylko 64 bajty / 50 ns = 1,28 GB / s, czyli mniej niż 4% maksymalnej przepustowości.

W praktyce rdzenie mogą wysyłać więcej niż jedno żądanie na raz, ale nie nieograniczoną liczbę. Zwykle przyjmuje się, że pomiędzy L1 i resztą hierarchii pamięci jest tylko 10 buforów wypełnienia linii na rdzeń, a być może 16 buforów wypełnienia między L2 i DRAM. Prefetching konkuruje o te same zasoby, ale przynajmniej pomaga zmniejszyć efektywne opóźnienia. Po więcej szczegółów zajrzyj do jednego ze świetnych postów Dr. Bandwidth napisał na ten temat, głównie na forach Intela.

Mimo to, większość ostatnich procesorów jest ograniczona przez ten czynnik, a nie przepustowość pamięci RAM. Zazwyczaj osiągają 12 - 20 GB/s na rdzeń, podczas gdy przepustowość pamięci RAM może wynosić 50+ GB/s (w systemie 4-kanałowym). Tylko niektóre najnowsze rdzenie "klienta" 2-kanałowego gen, które wydają się mieć lepszy uncore, być może więcej buforów liniowych może osiągnąć limit pamięci DRAM na jeden rdzeń, a nasze chipy Skylake wydają się być jednym z nich.

Obecnie Intel projektuje systemy z przepustowością pamięci DRAM 50 GB/s, utrzymując tylko

Dlaczego ciągle o tym mówię? Ponieważ najlepsza memcpy implementacja często zależy od reżimu, w którym działasz. Po ograniczeniu pamięci DRAM BW (tak jak nasze chipy, ale większość z nich nie ma jednego rdzenia), używanie zapisów nie-czasowych staje się bardzo ważne, ponieważ oszczędza odczyt dla własności, który zwykle marnuje 1/3 przepustowości. Widzisz to dokładnie w wynikach testów powyżej: implementacje memcpy, które nie używają, tracą 1/3 swojej przepustowości.

Jeśli jednak jesteś ograniczony współbieżnością, sytuacja wyrównuje się i czasami odwraca, jednak. Masz wolne pasmo DRAM, więc sklepy NT nie pomagają i mogą nawet zaszkodzić, ponieważ mogą zwiększyć opóźnienie, ponieważ czas przekazania bufora linii może być dłuższy niż scenariusz, w którym prefetch przenosi linię RFO do LLC (lub nawet L2), a następnie sklep kończy się w LLC dla efektywnego niższego opóźnienia. W końcu, serwer uncores mają zazwyczaj dużo wolniejsze sklepy NT niż klienckie (i dużą przepustowość), co podkreśla to efekt.

Więc na innych platformach może się okazać, że sklepy NT są mniej przydatne (przynajmniej jeśli zależy ci na wydajności jednowątkowej) i być może rep movsb wygrywa gdzie (jeśli dostanie to, co najlepsze z obu światów).

[85]}naprawdę, ten ostatni element to wezwanie do większości testów. Wiem, że sklepy NT tracą widoczną przewagę nad testami jednowątkowymi na większości archów( w tym obecnych archów serwerowych), ale nie wiem, jak rep movsb wykona stosunkowo...

Referencje

Inne dobre źródła informacji nie zintegrowane w powyższym.

Komp.Arch investigation of rep movsb versus alternatives. Wiele dobrych notatek na temat przewidywania gałęzi i implementacji podejścia, które często sugerowałem dla małych bloków: używanie nakładających się pierwszych i / lub ostatnich odczytów / zapisów zamiast próbować pisać tylko dokładnie wymaganą liczbę bajtów (na przykład, implementowanie wszystkich kopii od 9 do 16 bajtów jako dwóch 8-bajtów kopie, które mogą nakładać się na siebie w maksymalnie 7 bajtach).


1 prawdopodobnie intencją jest ograniczenie go do przypadków, w których, na przykład, rozmiar kodu jest bardzo ważny.

2 Patrz sekcja 3.7.5: przedrostek REP i przenoszenie danych.

3 ważne jest, aby pamiętać, że dotyczy to tylko różnych sklepów w obrębie samej pojedynczej instrukcji: po zakończeniu, blok sklepów nadal wydaje się uporządkowany w odniesieniu do wcześniejszych i późniejszych sklepy. Tak więc kod może widzieć sklepy z rep movs out of order w stosunku do siebie , ale nie w stosunku do poprzednich lub kolejnych sklepów (i to jest ta ostatnia gwarancja, której zwykle potrzebujesz). Będzie to problem tylko wtedy, gdy użyjesz końca miejsca docelowego kopii jako flagi synchronizacji, zamiast oddzielnego magazynu.

4 zauważ, że nie-czasowe dyskretne sklepy również unikają większości wymagań porządkowych, chociaż w praktyce rep movs ma jeszcze większą swobodę od nadal istnieją pewne ograniczenia dotyczące zamawiania w sklepach WC/NT.

5 było to powszechne w drugiej części ery 32-bitowej, gdzie wiele układów miało 64-bitowe ścieżki danych (np. do obsługi FPU, który miał wsparcie dla typu 64-bit {78]}). Obecnie" wykastrowane " chipy, takie jak marki Pentium czy Celeron, mają wyłączony AVX, ale przypuszczalnie rep movs microcode może nadal korzystać z 256b obciążeń / sklepów.

6 np. ze względu na reguły wyrównania języka, atrybuty wyrównania lub operatory, Zasady aliasingu lub inne informacje określone w czasie kompilacji. W przypadku osiowania, nawet jeśli nie można określić dokładnego osiowania, mogą one przynajmniej być w stanie podnieść kontrole osiowania z pętli lub w inny sposób wyeliminować zbędne kontrole.

7 zakładam, że " standard "memcpy wybiera podejście nie-czasowe, co jest bardzo prawdopodobne dla tej wielkości bufora.

8 niekoniecznie jest to oczywiste, ponieważ może to być przypadek, że strumień uop generowany przez rep movsb po prostu monopolizuje wysyłkę i wtedy wyglądałby bardzo podobnie do przypadku jawnego mov. Wydaje się jednak, że to tak nie działa - uops z kolejnych instrukcji może mieszać się z UOPs z mikrokodowanego rep movsb.

9 na przykład, te, które mogą wysyłać dużą liczbę niezależnych żądań pamięci, a tym samym nasycać dostępną przepustowość pamięci DRAM-to-core, z czego {14]} byłoby potomkiem plakatu (i jak to się do obciążeń związanych wyłącznie z opóźnieniem, takich jak ściganie wskaźnikiem).

 50
Author: BeeOnRope,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-26 22:21:41

Enhanced REP MOVSB (Ivy Bridge i nowsze)

Mikroarchitektura Ivy Bridge (procesory wydane w 2012 i 2013 roku) wprowadziła Enhanced REP MOVSB (musimy jeszcze sprawdzić odpowiedni bit) i pozwoliła nam na szybkie kopiowanie pamięci.

Najtańsze wersje późniejszych procesorów-Kaby Lake Celeron i Pentium, wydane w 2017 roku, nie mają AVX, który mógłby być używany do szybkiej kopii pamięci, ale nadal mają ulepszony REP MOVSB.

REP MOVSB (ERMSB) jest tylko szybszy niż AVX copy lub general-use register copy, jeśli rozmiar bloku wynosi co najmniej 256 bajtów. Dla bloków poniżej 64 bajtów jest to znacznie wolniejsze, ponieważ w ERMSB jest wysoki start wewnętrzny - około 35 cykli.

Patrz podręcznik Intela dotyczący optymalizacji, sekcja 3.7.6 Enhanced REP MOVSB and STOSB operation (ERMSB) http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf

  • Koszt uruchomienia jest 35 cykli;
  • Adres źródłowy i docelowy muszą być wyrównane do 16-bajtowej granicy;
  • region źródłowy nie powinien pokrywać się z regionem docelowym;
  • Aby uzyskać wyższą wydajność, długość musi być wielokrotnością 64.]}
  • kierunek musi być do przodu (CLD).

Jak już mówiłem, REP MOVSB zaczyna przewyższać inne metody, gdy długość wynosi co najmniej 256 bajtów, ale aby zobaczyć wyraźną korzyść w stosunku do AVX kopiuję, długość musi być większa niż 2048 bajtów.

Na efekt wyrównania jeśli REP MOVSB vs. AVX copy, Instrukcja Intela podaje następujące informacje:

    Jeśli bufor źródłowy nie jest wyrównany, wpływ implementacji ERMSB na 128-bitowy AVX jest podobny;]}
  • jeśli bufor docelowy nie jest wyrównany, wpływ na implementację ERMSB może wynosić 25% degradacji, podczas gdy 128-bitowa implementacja AVX memcpy może ulec degradacji tylko o 5% w stosunku do 16-bajtowy scenariusz wyrównany.

Zrobiłem testy na Intel Core i5-6600, Pod 64-bit, i porównałem REP movsb memcpy() z prostą implementacją MOV RAX, [SRC]; MOV [DST], Rax gdy dane mieszczą się w pamięci podręcznej L1:

REP MOVSB memcpy ():

 - 1622400000 data blocks of  32 bytes took 17.9337 seconds to copy;  2760.8205 MB/s
 - 1622400000 data blocks of  64 bytes took 17.8364 seconds to copy;  5551.7463 MB/s
 - 811200000 data blocks of  128 bytes took 10.8098 seconds to copy;  9160.5659 MB/s
 - 405600000 data blocks of  256 bytes took  5.8616 seconds to copy; 16893.5527 MB/s
 - 202800000 data blocks of  512 bytes took  3.9315 seconds to copy; 25187.2976 MB/s
 - 101400000 data blocks of 1024 bytes took  2.1648 seconds to copy; 45743.4214 MB/s
 - 50700000 data blocks of  2048 bytes took  1.5301 seconds to copy; 64717.0642 MB/s
 - 25350000 data blocks of  4096 bytes took  1.3346 seconds to copy; 74198.4030 MB/s
 - 12675000 data blocks of  8192 bytes took  1.1069 seconds to copy; 89456.2119 MB/s
 - 6337500 data blocks of  16384 bytes took  1.1120 seconds to copy; 89053.2094 MB/s

MOV RAX... memcpy ():

 - 1622400000 data blocks of  32 bytes took  7.3536 seconds to copy;  6733.0256 MB/s
 - 1622400000 data blocks of  64 bytes took 10.7727 seconds to copy;  9192.1090 MB/s
 - 811200000 data blocks of  128 bytes took  8.9408 seconds to copy; 11075.4480 MB/s
 - 405600000 data blocks of  256 bytes took  8.4956 seconds to copy; 11655.8805 MB/s
 - 202800000 data blocks of  512 bytes took  9.1032 seconds to copy; 10877.8248 MB/s
 - 101400000 data blocks of 1024 bytes took  8.2539 seconds to copy; 11997.1185 MB/s
 - 50700000 data blocks of  2048 bytes took  7.7909 seconds to copy; 12710.1252 MB/s
 - 25350000 data blocks of  4096 bytes took  7.5992 seconds to copy; 13030.7062 MB/s
 - 12675000 data blocks of  8192 bytes took  7.4679 seconds to copy; 13259.9384 MB/s

Tak więc, nawet na 128-bitowych blokach, REP MOVSB jest wolniejszy niż zwykła kopia MOV RAX w pętli (nie rozwijana). Wdrożenie ERMSB zaczyna przerastać pętla MOV RAX zaczyna się tylko od 256-bajtowych bloków.

Normalny (nie wzmocniony) REP MOVS na Nehalem i później

Co zaskakujące, poprzednie architektury (Nehalem i późniejsze), które nie miały jeszcze ulepszonego REP MOVB, miały dość szybką implementację REP MOVSD/MOVSQ (ale nie REP MOVSB/MOVSW) dla dużych bloków, ale nie na tyle dużą, aby przewymiarować pamięć podręczną L1.

Intel Optimization Manual (2.5.6 REP String Enhancement) podaje następujące informacje związane z Nehalem mikroarchitektura-procesory Intel Core i5, i7 i Xeon wydane w 2009 i 2010 roku.

REP MOVSB

Opóźnienie dla MOVSB wynosi 9 cykli, jeśli ECX 9 mają Koszt uruchomienia 50 cykli.

  • tiny string( ECX
  • mały ciąg (ECX jest między 4 A 9): brak oficjalnych informacji w instrukcji Intela, prawdopodobnie więcej niż 9 cykli, ale mniej niż 50 cykli;
  • long string (ECX > 9): Koszt uruchomienia 50 cykli.

Mój wniosek: REP MOVSB jest prawie bezużyteczny na Nehalem.

MOVSW/MOVSD/MOVSQ

Cytat z Intel Optimization Manual (2.5.6 Rep String Enhancement):

  • krótki ciąg (ECX
  • Fast string( ECX > = 76: excluding REP MOVSB): implementacja procesora zapewnia optymalizację sprzętu, przenosząc tyle fragmentów danych w 16 bajtach, ile możliwe. Opóźnienie ciągu REP będzie się różnić, jeśli jeden z 16-bajtowych transferów danych przekroczy granicę linii bufora: = Split-free: opóźnienie składa się z kosztu uruchomienia około 40 cykli, a każdy 64 bajty danych dodaje 4 cykle. = Cache splits: opóźnienie składa się z kosztu uruchomienia około 35 cykli i każdy 64 bajty danych dodaje 6 cykli.
  • pośrednie długości strun: opóźnienie REP MOVSW/MOVSD/MOVSQ ma koszt uruchomienia około 15 cykli plus jeden cykl dla każdego iteracja ruchu danych w word / dword / qword.

Informacje nie wydają się być poprawne tutaj. Z powyższego cytatu rozumiemy, że w przypadku bardzo dużych bloków pamięci REP MOVSW jest tak szybki jak REP MOVSD/MOVSQ, ale testy wykazały, że tylko REP MOVSD/MOVSQ są szybkie, podczas gdy REP MOVSW jest jeszcze wolniejszy niż REP MOVSB na Nehalem i Westmere.

Zgodnie z informacjami podanymi przez Intela w podręczniku, na poprzednich mikroarchitekturach Intela (przed 2008) uruchamianie koszty są jeszcze wyższe.

Wniosek: jeśli potrzebujesz tylko skopiować dane pasujące do pamięci podręcznej L1, tylko 4 cykle do skopiowania 64 bajtów danych są doskonałe i nie musisz używać rejestrów XMM!

REP MOVSD / MOVSQ jest uniwersalnym rozwiązaniem, które działa doskonale na wszystkich procesorach Intela (nie wymaga ERMSB), jeśli dane pasują do pamięci podręcznej L1

Oto testy REP MOVS*, gdy źródło i miejsce docelowe znajdowały się w pamięci podręcznej L1, bloków na tyle dużych, aby nie były poważnie dotknięte uruchomieniem koszty, ale nie tak duże, aby przekroczyć rozmiar pamięci podręcznej L1. Źródło: http://users.atw.hu/instlatx64/

Yonah (2006-2008)

    REP MOVSB 10.91 B/c
    REP MOVSW 10.85 B/c
    REP MOVSD 11.05 B/c

Nehalem (2009-2010)

    REP MOVSB 25.32 B/c
    REP MOVSW 19.72 B/c
    REP MOVSD 27.56 B/c
    REP MOVSQ 27.54 B/c

Westmere (2010-2011)

    REP MOVSB 21.14 B/c
    REP MOVSW 19.11 B/c
    REP MOVSD 24.27 B/c

Ivy Bridge (2012-2013) - z ulepszonym REP MOVSB

    REP MOVSB 28.72 B/c
    REP MOVSW 19.40 B/c
    REP MOVSD 27.96 B/c
    REP MOVSQ 27.89 B/c
[[12]}SkyLake (2015-2016) - z ulepszonym REP MOVSB
    REP MOVSB 57.59 B/c
    REP MOVSW 58.20 B/c
    REP MOVSD 58.10 B/c
    REP MOVSQ 57.59 B/c

Kaby Lake (2016-2017) - ze wzmocnioną REP MOVSB

    REP MOVSB 58.00 B/c
    REP MOVSW 57.69 B/c
    REP MOVSD 58.00 B/c
    REP MOVSQ 57.89 B/c

Jak widzisz, implementacja REP MOVS różni się znacząco z jednej mikroarchitektury do drugiej. Na niektórych procesorach, takich jak Ivy Bridge - REP MOVSB jest najszybszy, choć tylko nieco szybszy niż REP MOVSD/MOVSQ, ale bez wątpienia na wszystkich procesorach od Nehalem, REP MOVSD/MOVSQ działa bardzo dobrze - nawet nie potrzebujesz "Enhanced REP MOVSB", ponieważ na Ivy Bridge (2013) z Enhacnced REP MOVSB, REP MOVSD pokazuje ten sam bajt na dane zegara, co na Nehalem (2010) bez enhacnced REP Movsb, podczas gdy w rzeczywistości REP movsb stał się bardzo szybko tylko od SkyLake (2015) - dwa razy szybciej niż na Ivy Bridge. Więc ten enhacnced REP movsb bit W CPUID może być mylący-pokazuje tylko, że REP MOVSB per se jest w porządku, ale nie, że jakikolwiek REP MOVS* jest szybszy.

Najbardziej myląca implementacja ERMBSB znajduje się w mikroarchitekturze Ivy Bridge. Tak, na bardzo starych procesorach, przed ERMSB, REP MOVS* dla dużych bloków używał funkcji protokołu cache, która nie jest dostępna dla zwykłego kodu (no-RFO). Ale protokół ten nie jest już używany na Ivy Bridge, który ma ERMSB. Według Andy Glew 's comments on an answer to" dlaczego skomplikowane memcpy / memset są lepsze?"z odpowiedzi Petera Cordesa , funkcja protokołu pamięci podręcznej, która nie jest dostępna dla zwykłego kodu, była kiedyś używana na starszych procesorach, ale już nie na Ivy Bridge. I pojawia się wyjaśnienie, dlaczego koszty uruchomienia są tak wysokie dla REP MOVS*: "duży nacisk na wybór i skonfigurowanie właściwej metody wynika głównie z braku przewidywania gałęzi mikrokodu". Ciekawostką jest też fakt, że Pentium Pro (P6) w 1996 roku zaimplementowało REP MOVS* z 64-bitowymi obciążeniami i magazynami mikrokodu oraz protokołem cache no - RFO-nie naruszały one kolejności pamięci, w przeciwieństwie do ERMSB w Ivy Bridge.

Disclaimer

  1. ta odpowiedź ma znaczenie tylko w przypadkach, gdy dane źródłowe i docelowe pasują do pamięci podręcznej L1. W zależności od okoliczności, specyfika dostępu do pamięci (cache, itp.). Prefetch i NTI może dać lepsze wyniki w niektórych przypadkach, szczególnie na procesorach, które nie miał jeszcze wzmocnionego REP MOVSB. Nawet na tych starszych procesorach REP MOVSD mógł używać funkcji protokołu pamięci podręcznej, która nie jest dostępna dla zwykłego kodu.
  2. Informacje zawarte w tej odpowiedzi dotyczą tylko procesorów Intela, a nie procesorów innych producentów, takich jak AMD, które mogą mieć lepsze lub gorsze implementacje instrukcji REP MOVS*.
  3. przedstawiłem test wyniki dla SkyLake i Kaby Lake tylko dla potwierdzenia-te architektury mają te same dane z cyklu na instrukcję.
  4. Wszystkie nazwy produktów, znaki towarowe i zarejestrowane znaki towarowe są własnością ich odpowiednich właścicieli.
 8
Author: Maxim Masiutin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-06 23:58:34

Mówisz, że chcesz:

Odpowiedź, która pokazuje, kiedy ERMSB jest przydatny

Ale nie jestem pewien, czy to znaczy to, co myślisz. Patrząc na dokumenty 3.7.6.1, do których linkujesz, wyraźnie mówi:

Implementacja memcpy przy użyciu ERMSB może nie osiągnąć takiego samego poziomu przepustowości jak użycie 256-bitowych lub 128-bitowych alternatyw AVX, w zależności od długości i współczynników wyrównania.

Więc tylko dlatego, że CPUID wskazuje wsparcie dla ERMSB, nie jest to gwarancja ten REP MOVSB będzie najszybszym sposobem na skopiowanie pamięci. Oznacza to tylko, że nie będzie ssać tak źle, jak w niektórych poprzednich procesorach.

Jednak tylko dlatego, że mogą istnieć alternatywy, które mogą, pod pewnymi warunkami, działać szybciej, nie oznacza, że REP MOVSB jest bezużyteczny. Teraz, gdy kary za wykonanie, które poniosła ta instrukcja, zniknęły, jest to potencjalnie przydatna instrukcja ponownie.

Pamiętaj, że jest to mały kawałek kodu (2 bajty!) w porównaniu z niektórymi bardziej zaangażowanymi memcpy procedury, które widziałem. Ponieważ ładowanie i uruchamianie dużych fragmentów kodu ma również karę (wyrzucenie niektórych innych kodów z pamięci podręcznej procesora), czasami "korzyść" AVX et al będzie równoważona przez wpływ, jaki ma na resztę kodu. Zależy co robisz.

Pytasz również:

Dlaczego przepustowość jest o wiele niższa z REP MOVSB? Co mogę zrobić, aby go ulepszyć?

Nie będzie możliwe "coś zrobić", aby REP MOVSB uruchomił jakiekolwiek szybciej. Robi to, co robi.

Jeśli chcesz uzyskać wyższe prędkości, które widzisz z memcpy, możesz wykopać dla niego źródło. Gdzieś tam jest. Możesz też prześledzić go z debuggera i zobaczyć, jakie ścieżki kodu są pobierane. Oczekuję, że używa niektórych instrukcji AVX do pracy z 128 lub 256 bitami na raz.

Albo po prostu... Prosiłeś, żebyśmy tego nie mówili.
 7
Author: David Wohlferd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-20 09:08:45

To nie jest odpowiedź na podane pytania, tylko moje wyniki (i osobiste wnioski), gdy próbuję się dowiedzieć.

Podsumowując: GCC już optymalizuje memset()/memmove()/memcpy() (Zobacz np. gcc / config/i386 / i386.c: expand_set_or_movmem_via_rep () {[57] } w źródłach GCC; poszukaj również stringop_algs w tym samym pliku, aby zobaczyć warianty zależne od architektury). Nie ma więc powodu, aby oczekiwać ogromnych zysków, używając własnego wariantu z GCC (chyba że zapomniałeś ważnych rzeczy, takich jak atrybuty wyrównania dla danych wyrównanych lub nie umożliwiają wystarczająco konkretnych optymalizacji, takich jak -O2 -march= -mtune=). Jeśli się zgadzasz, To odpowiedzi na podane pytanie są mniej lub bardziej nieistotne w praktyce.

(szkoda tylko, że nie ma memrepeat(), przeciwieństwo memcpy() w porównaniu do memmove(), które powtarzałoby początkową część bufora, aby wypełnić cały bufor.)


Obecnie mam maszynę Ivy Bridge w użyciu( laptop Core i5 - 6200U, jądro Linux 4.4.0 x86-64, Z erms w /proc/cpuinfo flagi). Ponieważ chciałem się dowiedzieć, czy mogę znaleźć przypadek, w którym Niestandardowy wariant memcpy () oparty na rep movsb przewyższy zwykły memcpy(), napisałem zbyt skomplikowany benchmark.

[53]}główną ideą jest to, że główny program przydziela trzy duże obszary pamięci: original, current, i correct, każdy dokładnie ten sam rozmiar i przynajmniej wyrównane strony. Operacje kopiowania są pogrupowane w zestawy, przy czym każdy zestaw ma odrębne właściwości, podobnie jak wszystkie źródła i cele są wyrównane (do pewnej liczby bajtów), lub wszystkie długości mieszczące się w tym samym zakresie. Każdy zbiór jest opisany za pomocą tablicy src, dst, n trojaczki, gdzie wszystkie src do src+n-1 i dst do dst+n-1 znajdują się całkowicie w obszarze current.

A XORSHIFT* PRNG jest używany do inicjalizacji original do losowych danych. (Jak ostrzegałem powyżej, jest to zbyt skomplikowane, ale chciałem się upewnić, że nie pozostawiam żadnych łatwych skrótów dla kompilatora.) Obszar correct otrzymuje się rozpoczynając od original dane w current, stosując wszystkie tryplety w bieżącym zestawie, używając memcpy() dostarczonego przez Bibliotekę C i kopiując obszar current do correct. Umożliwia to weryfikację poprawnego działania każdej funkcji benchmarkowanej.

Każdy zestaw operacji kopiowania jest mierzony wiele razy przy użyciu tej samej funkcji, a mediana z nich jest używana do porównania. (Moim zdaniem mediana ma największy sens w benchmarkingu i dostarcza sensownej semantyki - funkcja jest przynajmniej tak szybka w przynajmniej połowę czasu.)

Aby uniknąć optymalizacji kompilatora, mam program ładować funkcje i Benchmarki dynamicznie, w czasie wykonywania. Wszystkie funkcje mają tę samą formę, void function(void *, const void *, size_t) -- zauważ, że w przeciwieństwie do memcpy() i memmove(), nic nie zwracają. Benchmarki (nazwane zestawy operacji kopiowania) są generowane dynamicznie przez wywołanie funkcji (które między innymi przenosi wskaźnik do obszaru current i jego rozmiar jako parametry).

Niestety, nie znalazłem jeszcze żadnego zestawu gdzie

static void rep_movsb(void *dst, const void *src, size_t n)
{
    __asm__ __volatile__ ( "rep movsb\n\t"
                         : "+D" (dst), "+S" (src), "+c" (n)
                         :
                         : "memory" );
}

By pokonać

static void normal_memcpy(void *dst, const void *src, size_t n)
{
    memcpy(dst, src, n);
}

Używanie gcc -Wall -O2 -march=ivybridge -mtune=ivybridge używanie GCC 5.4.0 na wyżej wspomnianym laptopie Core i5-6200U z 64-bitowym jądrem linux-4.4.0. Kopiowanie 4096-bajtowych wyrównanych i wielkości kawałków jest jednak bliskie.

Oznacza to, że przynajmniej do tej pory nie znalazłem przypadku, w którym użycie wariantu rep movsb memcpy miałoby sens. To nie znaczy, że nie ma takiego przypadku; po prostu nie znalazłem.

(w tym momencie kod jest spaghetti bałagan Jestem bardziej zawstydzony niż dumny, więc pominę publikowanie źródeł, chyba że ktoś zapyta. Powyższy opis powinien wystarczyć, aby napisać lepszy.)


Nie dziwi mnie to zbytnio. Kompilator C może wywnioskować wiele informacji o wyrównaniu wskaźników operandu i czy liczba bajtów do skopiowania jest stałą czasu kompilacji, wielokrotnością odpowiedniej mocy dwóch. Ta informacja może i będzie/powinna być używana przez kompilator do zastąpienia biblioteki C memcpy() / memmove() funkcje z własnym.

GCC robi dokładnie to (patrz np. gcc/config/i386 / i386.c: expand_set_or_movmem_via_rep () {[57] } w źródłach GCC; poszukaj również stringop_algs w tym samym pliku, aby zobaczyć warianty zależne od architektury). Rzeczywiście., memcpy()/memset()/memmove() została już osobno zoptymalizowana dla całkiem kilku wariantów procesorów x86; zaskoczyłoby mnie, gdyby deweloperzy GCC nie włączyli jeszcze obsługi erms.

GCC udostępnia kilka atrybutów funkcji, które programiści mogą używać do zapewnienia dobrego generowanego kodu. Na przykład alloc_align (n) mówi GCC, że funkcja zwraca pamięć wyrównaną do co najmniej n bajtów. Aplikacja lub biblioteka mogą wybrać implementację funkcji do użycia w czasie wykonywania, tworząc "funkcję resolver" (która zwraca wskaźnik funkcji) i definiując funkcję za pomocą atrybutu ifunc (resolver).

Jednym z najczęstszych wzorów używam w moim kodzie do tego jest

some_type *pointer = __builtin_assume_aligned(ptr, alignment);

Gdzie ptr jest jakimś wskaźnikiem, alignment jest liczba bajtów, do których jest wyrównana; GCC wtedy wie/zakłada, że pointer jest wyrównana do alignment bajtów.

Innym przydatnym wbudowanym, choć znacznie trudniejszym w użyciu poprawnie, jest __builtin_prefetch(). Aby zmaksymalizować ogólną przepustowość / wydajność, odkryłem, że minimalizacja opóźnień w każdej podoperacji daje najlepsze wyniki. (W przypadku kopiowania rozproszonych elementów do kolejnych tymczasowych magazynów jest to trudne, ponieważ prefetching zazwyczaj obejmuje pełną linię pamięci podręcznej; jeśli zbyt wiele elementy są wstępnie ustawiane, większość pamięci podręcznej jest marnowana przez przechowywanie nieużywanych elementów.)

 6
Author: Nominal Animal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-22 13:36:47

Istnieją znacznie bardziej efektywne sposoby przenoszenia danych. Obecnie implementacja memcpy wygeneruje kod specyficzny dla architektury z kompilatora, który jest zoptymalizowany na podstawie wyrównania danych w pamięci i innych czynników. Pozwala to na lepsze wykorzystanie instrukcji nie-temporal cache oraz XMM i innych rejestrów w świecie x86.

Kiedy hard-code rep movsb zapobiega użyciu wewnętrznych.

Dlatego dla czegoś takiego jak memcpy, chyba że piszesz coś, co jeśli nie poświęcisz czasu na napisanie wysoce zoptymalizowanej funkcji memcpy w assembly (lub używając intrinsików poziomu C), będziesz daleko lepiej pozwolić kompilatorowi to rozgryźć.

 3
Author: David Hoelzer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-11 10:57:19

Jako ogólny memcpy() przewodnik:

A) jeśli kopiowane dane są małe (mniej niż 20 bajtów) i mają stały rozmiar, niech to zrobi kompilator. Powód: kompilator może używać zwykłych instrukcji mov i unikać kosztów początkowych.

B) jeśli kopiowane dane są małe (mniej niż około 4 KiB) i gwarantowane, że zostaną wyrównane, użyj rep movsb (Jeśli ermsb jest obsługiwany) lub rep movsd (Jeśli ERMSB nie jest obsługiwany). Powód: Korzystanie z alternatywy SSE lub AVX ma ogromną ilość " kosztów początkowych" zanim coś skopiuje.

C) Jeśli kopiowane dane są małe (mniej niż około 4 KiB) i nie mają gwarancji wyrównania, użyj rep movsb. Powód: używanie SSE lub AVX, lub używanie rep movsd dla większości z nich plus niektóre rep movsb na początku lub końcu, ma zbyt wiele kosztów.

D) dla wszystkich innych przypadków użyj czegoś takiego:

    mov edx,0
.again:
    pushad
.nextByte:
    pushad
    popad
    mov al,[esi]
    pushad
    popad
    mov [edi],al
    pushad
    popad
    inc esi
    pushad
    popad
    inc edi
    pushad
    popad
    loop .nextByte
    popad
    inc edx
    cmp edx,1000
    jb .again

Powód: będzie to tak powolne, że zmusi programistów do znalezienia alternatywy, która nie wymaga kopiowania ogromnych globusów danych; a wynikające z tego oprogramowanie będzie znacznie szybsze, ponieważ uniknięto kopiowania dużych Globów danych.

 1
Author: Brendan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-20 11:28:51