Jak ważne jest wyrównanie pamięci? Czy to wciąż ma znaczenie?

15

Od pewnego czasu dużo szukałem i czytałem o wyrównaniu pamięci, o tym, jak to działa i jak z niego korzystać. Najbardziej odpowiedni artykuł, który znalazłem na razie, to ten .

Ale mimo to wciąż mam kilka pytań:

  1. Z wbudowanego systemu często mamy ogromną ilość pamięci w naszym komputerze, co sprawia, że ​​zarządzanie pamięcią jest o wiele mniej krytyczne, jestem całkowicie zoptymalizowany, ale teraz to naprawdę coś, co może zrobić różnicę, jeśli porównamy ten sam program z lub bez uporządkowania i wyrównania pamięci?
  2. Czy wyrównanie pamięci ma inne zalety? Czytałem gdzieś, że procesor działa lepiej / szybciej z wyrównaną pamięcią, ponieważ zajmuje to mniej instrukcji do przetworzenia (jeśli jeden z was ma link do artykułu / testu?), Czy w takim przypadku różnica jest naprawdę znacząca? Czy jest więcej zalet niż te dwa?
  3. W linku do artykułu w rozdziale 5 autor mówi:

    Uwaga: w C ++ klasy wyglądające jak struktury mogą złamać tę regułę! (Czy to robią, czy nie, zależy od sposobu implementacji klas podstawowych i wirtualnych funkcji składowych i zależy od kompilatora.)

  4. Artykuł mówi głównie o strukturach, ale czy na tę deklarację wpływa również deklaracja zmiennych lokalnych?

    Czy masz pojęcie o tym, jak wyrównanie pamięci działa dokładnie w C ++, ponieważ wydaje się, że ma pewne różnice?

To poprzednie pytanie zawiera słowo „wyrównanie”, ale nie zawiera żadnych odpowiedzi na powyższe pytania.

Kane
źródło
Kompilatory C ++ są bardziej skłonne do tego (wstawiaj padding tam, gdzie jest to potrzebne lub korzystne) dla Ciebie. Z podanego linku poszukaj w sekcji 12 „Narzędzia” rzeczy, których możesz użyć.
rwong,

Odpowiedzi:

11

Tak, zarówno wyrównanie, jak i uporządkowanie danych może mieć dużą różnicę w wydajności, nie tylko o kilka procent, ale od kilku do wielu setek procent.

Weź tę pętlę, dwie instrukcje mają znaczenie, jeśli uruchomisz wystarczającą liczbę pętli.

.globl ASMDELAY
ASMDELAY:
    subs r0,r0,#1
    bne ASMDELAY
    bx lr

Z pamięcią podręczną i bez niej oraz z wyrównywaniem z rzutem pamięci podręcznej i bez niego w przewidywaniu gałęzi i można znacznie różnić wydajność tych dwóch instrukcji (tyknięcia zegara):

min      max      difference
00016DDE 003E025D 003C947F

Test wydajności, który możesz bardzo łatwo zrobić sam. dodaj lub usuń kropki wokół testowanego kodu i wykonaj dokładną synchronizację, przenieś testowane instrukcje wzdłuż odpowiednio szerokiego zakresu adresów, aby dotknąć krawędzi linii pamięci podręcznej itp.

To samo dotyczy dostępu do danych. Niektóre architektury narzekają na niezrównany dostęp (na przykład wykonanie 32-bitowego odczytu pod adresem 0x1001), powodując błąd danych. Niektóre z nich można wyłączyć i przejąć wydajność. Inne, które umożliwiają dostęp bez wyrównania, po prostu dostają wydajność.

Czasami są to „instrukcje”, ale przez większość czasu są to cykle zegara / autobusu.

Spójrz na implementacje memcpy w gcc dla różnych celów. Powiedzmy, że kopiujesz strukturę 0x43 bajtów, możesz znaleźć implementację, która kopiuje jeden bajt, pozostawiając 0x42, a następnie kopiuje 0x40 bajtów w dużych wydajnych porcjach, a ostatnia 0x2 może to zrobić jako dwa pojedyncze bajty lub jako transfer 16-bitowy. Wyrównanie i cel wchodzą w grę, jeśli adresy źródłowy i docelowy są na tym samym wyrównaniu, powiedzmy 0x1003 i 0x2003, wtedy możesz zrobić jeden bajt, następnie 0x40 w dużych porcjach, a następnie 0x2, ale jeśli jeden to 0x1002, a drugi 0x1003, to dostaje naprawdę brzydkie i naprawdę wolne.

Przez większość czasu są to cykle autobusowe. Lub gorsza liczba przelewów. Weź procesor z 64-bitową magistralą danych, taką jak ARM, i wykonaj transfer czterech słów (odczyt lub zapis, LDM lub STM) pod adresem 0x1004, to jest adres wyrównany do słów i całkowicie legalny, ale jeśli magistrala ma 64 szerokość bitów jest prawdopodobne, że pojedyncza instrukcja zamieni się w trzy transfery w tym przypadku 32-bitowy przy 0x1004, 64-bitowy przy 0x1008 i 32-bitowy przy 0x100A. Ale gdybyś miał tę samą instrukcję, ale pod adresem 0x1008, mógłby wykonać pojedynczy transfer czterech słów pod adresem 0x1008. Każdy transfer ma przypisany czas konfiguracji. Tak więc różnica adresów od 0x1004 do 0x1008 może być kilka razy szybsza, nawet / esp podczas korzystania z pamięci podręcznej i wszystkie są trafieniami do pamięci podręcznej.

Mówiąc o tym, nawet jeśli wykonasz dwa słowa odczytane pod adresem 0x1000 vs 0x0FFC, 0x0FFC z brakami pamięci podręcznej spowoduje odczyt dwóch linii pamięci podręcznej, gdzie 0x1000 to jedna linia pamięci podręcznej, i tak zostaniesz ukarany za odczytywanie linii losowej losowo dostęp (odczyt większej ilości danych niż używanie), ale to podwaja się. Sposób, w jaki struktury są wyrównane lub dane w ogóle, a także częstotliwość uzyskiwania dostępu do tych danych itp., Mogą powodować przeładowanie pamięci podręcznej.

Możesz skończyć z rozbijaniem danych, tak że podczas przetwarzania danych możesz tworzyć eksmisje, możesz mieć naprawdę pecha i skończyć z wykorzystaniem tylko niewielkiej części pamięci podręcznej, a podczas przeskakiwania przez nią następna kropla danych zderza się z poprzednią kroplą . Przez zmieszanie danych lub ponowne uporządkowanie funkcji w kodzie źródłowym itp. Możesz tworzyć lub usuwać kolizje, ponieważ nie wszystkie pamięci podręczne są równe, kompilator nie pomoże ci tutaj. Nawet wykrywanie spadku wydajności lub poprawy zależy od Ciebie.

Wszystko, co dodaliśmy, aby poprawić wydajność, szersze magistrale danych, potoki, pamięci podręczne, przewidywanie rozgałęzień, wiele jednostek / ścieżek wykonawczych itp. Najczęściej pomogą, ale wszystkie mają słabe punkty, które można wykorzystać celowo lub przypadkowo. Kompilator lub biblioteki niewiele mogą na to poradzić, jeśli interesuje Cię wydajność, musisz dostroić, a jednym z największych czynników dostrajających jest wyrównanie kodu i danych, a nie tylko 32, 64, 128, 256 granice bitów, ale także tam, gdzie rzeczy są względem siebie nawzajem, mocno używane pętle lub ponownie wykorzystywane dane nie powinny lądować w ten sam sposób pamięci podręcznej, każda z nich chce mieć własną. Kompilatory mogą pomóc np. W zamawianiu instrukcji dla architektury super skalarnej, ponownym rozmieszczaniu instrukcji, które nie mają znaczenia,

Największym niedopatrzeniem jest założenie, że procesor jest wąskim gardłem. Nie było to prawdą przez dekadę lub dłużej, karmienie procesora jest problemem i tam właśnie pojawiają się problemy, takie jak uderzenia wydajności wyrównania, przerzucanie pamięci podręcznej itp. Przy odrobinie pracy, nawet na poziomie kodu źródłowego, ponowne uporządkowanie danych w strukturze, porządkowanie deklaracji zmiennych / struktur, porządkowanie funkcji w kodzie źródłowym i trochę dodatkowego kodu do wyrównywania danych, może kilkukrotnie poprawić wydajność więcej.

old_timer
źródło
+1, jeśli tylko za ostatni akapit. Przepustowość pamięci jest najważniejszym problemem dla każdego, kto próbuje dziś napisać szybki kod, a nie liczbę instrukcji. A to oznacza, że ​​optymalizacja rzeczy w celu zmniejszenia braków w pamięci podręcznej, co można zrobić poprzez modyfikację wyrównania w wielu okolicznościach, jest niezwykle ważna.
Jules
Jeśli kod i dane zostaną zapisane w pamięci podręcznej i wykonasz wystarczającą liczbę pętli / cykli na tych danych, liczą się instrukcje i gdzie instrukcje leżą w linii pobierania, gdzie gałęzie lądują w rurze w stosunku do tego, na czym polegają, mają znaczenie. Ale w systemach opartych na technologii dram i / lub flash najpierw musisz się martwić zasileniem procesora tak.
old_timer
15

Tak, wyrównanie pamięci nadal ma znaczenie.

Niektóre procesory faktycznie nie mogą wykonywać odczytów z adresów nieprzystosowanych. Jeśli pracujesz na takim sprzęcie i przechowujesz liczby całkowite niewyrównane, prawdopodobnie będziesz musiał je przeczytać z dwiema instrukcjami, a następnie kilkoma instrukcjami, aby wprowadzić różne bajty we właściwe miejsca, abyś mógł z nich faktycznie skorzystać . Tak dostosowane dane mają krytyczne znaczenie dla wydajności.

Dobra wiadomość jest taka, że ​​tak naprawdę nie musisz się tym przejmować. Prawie każdy kompilator dla prawie dowolnego języka będzie wytwarzał kod maszynowy, który będzie spełniał wymagania dotyczące wyrównania systemu docelowego. Musisz o tym pomyśleć tylko wtedy, gdy przejmujesz bezpośrednią kontrolę nad reprezentacją danych w pamięci, co nie jest konieczne tak często, jak kiedyś. To interesująca rzecz, o której warto wiedzieć, i absolutnie niezbędna, aby wiedzieć, czy chcesz zrozumieć wykorzystanie pamięci z różnych tworzonych struktur i jak może reorganizować rzeczy, aby były bardziej wydajne (unikając wypełniania). Ale chyba, że ​​potrzebujesz takiej kontroli (a dla większości systemów, których po prostu nie potrzebujesz), możesz z radością przejść całą karierę, nie wiedząc o tym ani nie dbając o nią.

Matthew Walton
źródło
1
W szczególności ARM nie obsługuje niezaangażowanego dostępu. A to procesor prawie do wszystkich zastosowań mobilnych.
Jan Hudec
Zauważ również, że Linux emuluje niezaangażowany dostęp przy pewnym koszcie środowiska wykonawczego, ale Windows (CE i telefon) tego nie robi i próba nieza wyrównanego dostępu po prostu spowoduje awarię aplikacji.
Jan Hudec
2
Chociaż jest to w większości prawdą, należy pamiętać, że niektóre platformy (w tym x86) mają różne wymagania dotyczące wyrównywania w zależności od instrukcji, które będą używane , co nie jest łatwe dla kompilatora, aby sam się wypracował, więc czasami trzeba użyć padu, aby upewnić się niektóre operacje (np. instrukcje SSE, z których wiele wymaga 16-bajtowego wyrównania) mogą być wykorzystane do niektórych operacji. Ponadto dodanie dodatkowego dopełnienia, aby dwa często używane razem elementy występowały w tej samej linii pamięci podręcznej (również 16 bajtów), może mieć ogromny wpływ na wydajność w niektórych przypadkach, a także nie jest zautomatyzowane.
Jules
3

Tak, nadal ma to znaczenie, aw niektórych algorytmach krytycznych dla wydajności nie można polegać na kompilatorze.

Wymienię tylko kilka przykładów:

  1. Z tej odpowiedzi :

Zwykle mikrokod pobierze odpowiednią 4-bajtową liczbę z pamięci, ale jeśli nie zostanie wyrównany, będzie musiał pobrać dwie 4-bajtowe lokalizacje z pamięci i zrekonstruować pożądaną 4-bajtową liczbę z odpowiednich bajtów dwóch lokalizacji

  1. Zestaw instrukcji SSE wymaga specjalnego dostosowania. Jeśli nie jest spełniony, musisz użyć specjalnych funkcji, aby załadować i zapisać dane w niewyrównanej pamięci. Oznacza to dwie dodatkowe instrukcje.

Jeśli nie pracujesz nad algorytmami krytycznymi dla wydajności, po prostu zapomnij o wyrównaniu pamięci. Nie jest tak naprawdę potrzebny do normalnego programowania.

BЈовић
źródło
1

Staramy się unikać sytuacji, w których ma to znaczenie. Jeśli to ma znaczenie, to ma znaczenie. Niedopasowane dane zdarzały się na przykład podczas przetwarzania danych binarnych, co wydaje się być obecnie unikane (ludzie często używają XML lub JSON).

JEŚLI w jakiś sposób uda ci się utworzyć nieprzypisaną tablicę liczb całkowitych, to na typowym procesorze Intel proces przetwarzania tej tablicy będzie działał nieco wolniej niż w przypadku wyrównanych danych. Na procesorze ARM działa nieco wolniej, jeśli powiesz kompilatorowi, że dane nie są wyrównane. Może albo działać okropnie, dużo wolniej, albo dawać błędne wyniki, w zależności od modelu procesora i systemu operacyjnego, jeśli użyjesz niepasowanych danych bez informowania kompilatora.

Wyjaśnienie odwołania do C ++: W C wszystkie pola w strukturze muszą być przechowywane w porządku rosnącym. Więc jeśli masz pola char / double / char i chcesz wszystko wyrównać, będziesz miał jeden bajt char, siedem bajtów nieużywanych, osiem bajtów double, jeden bajt char, siedem bajtów nieużywanych. W strukturach C ++ jest to samo dla kompatybilności. Ale w przypadku struktur kompilator może zmieniać kolejność pól, więc możesz mieć jeden bajt char, inny bajt char, sześć bajtów nieużywanych, 8 bajtów podwójnie. Używanie 16 zamiast 24 bajtów. W strukturach C programiści zwykle unikają takiej sytuacji i przede wszystkim umieszczają pola w innej kolejności.

gnasher729
źródło
1
Nieprzypisane dane są przechowywane w pamięci. Programy, które nie mają odpowiednio spakowanych struktur danych, mogą podlegać ogromnym karom za wydajność nawet za pozornie nieistotne uporządkowanie wartości. Na przykład w lithreadowanym kodzie dwie wartości w jednej linii pamięci podręcznej spowodują masowe przeciągnięcia potoku, gdy dwa wątki będą miały do ​​nich dostęp jednocześnie (oczywiście ignorując problemy z bezpieczeństwem wątków).
greyfade,
Kompilator C ++ może zmieniać kolejność pól tylko pod pewnymi warunkami, które prawdopodobnie nie zostaną spełnione, jeśli nie znasz tych reguł. Co więcej, nie znam żadnego kompilatora C ++, który faktycznie korzysta z tej swobody.
Sjoerd
1
Nigdy nie widziałem, żeby pola kompilatora C zamawiały ponownie. Widziałem jednak wiele wstawek i wyrównanie między
znakami
1

Jak ważne jest wyrównanie pamięci? Czy to wciąż ma znaczenie?

Tak. Nie. To zależy.

Z wbudowanego systemu często mamy ogromną ilość pamięci w naszym komputerze, co sprawia, że ​​zarządzanie pamięcią jest o wiele mniej krytyczne, jestem całkowicie zoptymalizowany, ale teraz to naprawdę coś, co może zrobić różnicę, jeśli porównamy ten sam program z lub bez uporządkowania i wyrównania pamięci?

Twoja aplikacja będzie miała mniejszą pamięć i będzie działać szybciej, jeśli zostanie odpowiednio wyrównana. W typowej aplikacji komputerowej nie będzie to miało znaczenia poza rzadkimi / nietypowymi przypadkami (np. Aplikacja zawsze kończy się tym samym wąskim gardłem wydajności i wymaga optymalizacji). Oznacza to, że aplikacja będzie mniejsza i szybsza, jeśli zostanie odpowiednio wyrównana, ale w większości praktycznych przypadków nie powinna wpływać na użytkownika w taki czy inny sposób.

Czy wyrównanie pamięci ma inne zalety? Czytałem gdzieś, że procesor działa lepiej / szybciej z wyrównaną pamięcią, ponieważ zajmuje to mniej instrukcji do przetworzenia (jeśli jeden z was ma link do artykułu / testu?), Czy w takim przypadku różnica jest naprawdę znacząca? Czy jest więcej zalet niż te dwa?

To może być. Podczas pisania kodu należy o tym pamiętać (być może), ale w większości przypadków po prostu nie powinno to mieć znaczenia (to znaczy, wciąż zmieniam zmienne składowe według wielkości pamięci i częstotliwości dostępu - co powinno ułatwić buforowanie - ale robię to dla łatwość użycia / odczytu i refaktoryzacji kodu, nie do celów buforowania).

Czy masz pojęcie o tym, jak wyrównanie pamięci działa dokładnie w C ++, ponieważ wydaje się, że ma pewne różnice?

Czytałem o tym, kiedy wyszedł alignof rzeczy (C ++ 11?), Nie przejmowałem się tym od tego czasu (obecnie zajmuję się głównie aplikacjami komputerowymi i tworzeniem serwerów backendowych).

utnapistim
źródło