Pytając o typowe, niezdefiniowane zachowanie w C , ludzie czasami odnoszą się do ścisłej zasady aliasingu.
O czym oni rozmawiają?
803
Pytając o typowe, niezdefiniowane zachowanie w C , ludzie czasami odnoszą się do ścisłej zasady aliasingu.
O czym oni rozmawiają?
c
ic++faq
.Odpowiedzi:
Typową sytuacją, w której napotykasz ścisłe problemy z aliasingiem, jest nakładanie struktury (takiej jak msg urządzenia / sieci) na bufor wielkości słowa twojego systemu (jak wskaźnik do
uint32_t
s lubuint16_t
s). Kiedy nakładasz strukturę na taki bufor lub bufor na taką strukturę za pomocą rzutowania wskaźnika, możesz łatwo złamać surowe reguły aliasingu.Więc w tego rodzaju konfiguracji, jeśli chcę wysłać wiadomość do czegoś, musiałbym mieć dwa niekompatybilne wskaźniki wskazujące na ten sam fragment pamięci. Mógłbym wtedy naiwnie kodować coś takiego (w systemie z
sizeof(int) == 2
):Surowa zasada aliasingu powoduje, że ta konfiguracja jest nielegalna: dereferencja wskaźnika, który aliuje obiekt, który nie jest zgodnego typu lub jeden z innych typów dozwolonych w C 2011 6.5 akapit 7 1 jest zachowaniem niezdefiniowanym. Niestety, nadal możesz kodować w ten sposób, być może otrzymujesz ostrzeżenia, kompilujesz się dobrze, tylko po to, by mieć dziwne nieoczekiwane zachowanie po uruchomieniu kodu.
(GCC wydaje się nieco niespójna w swojej zdolności do udzielania ostrzeżeń aliasingowych, czasami dając nam przyjazne ostrzeżenie, a czasem nie.)
Aby zobaczyć, dlaczego to zachowanie jest niezdefiniowane, musimy pomyśleć o tym, co ścisła reguła aliasingu kupuje kompilator. Zasadniczo dzięki tej regule nie trzeba myśleć o wstawianiu instrukcji, aby odświeżyć zawartość
buff
każdego uruchomienia pętli. Zamiast tego, podczas optymalizacji, z pewnymi irytująco niewymuszonymi założeniami dotyczącymi aliasingu, może pominąć te instrukcje, załadowaćbuff[0]
ibuff[1
] do rejestrów procesora jeden raz przed uruchomieniem pętli i przyspieszyć jej ciało. Przed wprowadzeniem ścisłego aliasingu kompilator musiał żyć w stanie paranoi, w której zawartośćbuff
może się zmienić w dowolnym momencie z dowolnego miejsca przez kogokolwiek. Aby uzyskać dodatkową przewagę wydajności i przy założeniu, że większość ludzi nie pisze wskaźników, wprowadzono surową zasadę aliasingu.Pamiętaj, że jeśli uważasz, że ten przykład jest wymyślony, może się to zdarzyć nawet wtedy, gdy przekażesz bufor innej funkcji wykonującej wysyłanie za ciebie, jeśli zamiast tego masz.
I przepisałem wcześniejszą pętlę, aby skorzystać z tej wygodnej funkcji
Kompilator może, ale nie musi, być wystarczająco inteligentny, aby spróbować wstawić SendMessage i może, ale nie musi, ponownie ładować lub nie ładować buffa. Jeśli
SendMessage
jest częścią innego API skompilowanego osobno, prawdopodobnie zawiera instrukcje ładowania zawartości buffa. Z drugiej strony, być może jesteś w C ++ i jest to jakaś implementacja zawierająca tylko szablony, które według kompilatora mogą być wbudowane. A może jest to po prostu coś, co napisałeś w pliku .c dla Twojej wygody. W każdym razie nadal może wystąpić niezdefiniowane zachowanie. Nawet jeśli wiemy, co dzieje się pod maską, nadal stanowi to naruszenie zasady, więc nie można zagwarantować żadnego dobrze zdefiniowanego zachowania. Zatem samo zawinięcie w funkcję, która bierze nasz bufor rozdzielany słowami, niekoniecznie pomaga.Jak mam to obejść?
Użyj związku. Większość kompilatorów obsługuje to bez narzekania na ścisłe aliasing. Jest to dozwolone w C99 i wyraźnie dozwolone w C11.
Możesz wyłączyć ścisłe aliasing w swoim kompilatorze ( f [no-] ścisłe aliasing w gcc))
Możesz użyć
char*
do aliasingu zamiast słowa systemu. Reguły dopuszczają wyjątek dlachar*
(w tymsigned char
iunsigned char
). Zawsze zakłada się, żechar*
aliasy innych typów. Jednak to nie zadziała w drugą stronę: nie ma założenia, że twoja struktura aliasuje bufor znaków.Początkujący strzeż się
To tylko jedno potencjalne pole minowe, gdy nakładają się na siebie dwa typy. Powinieneś także dowiedzieć się o endianness , dopasowywaniu słów i jak radzić sobie z problemami z wyrównaniem poprzez prawidłowe pakowanie struktur .
Notatka
1 Rodzaje, do których dostęp C 2011 6.5 7 umożliwia dostęp do wartości, to:
źródło
unsigned char*
można użyć dalekochar*
? Zwykle używamunsigned char
raczej niżchar
jako podstawowy typ,byte
ponieważ moje bajty nie są podpisane i nie chcę dziwności podpisanych zachowań (zwłaszcza wrt do przepełnienia)unsigned char *
jest w porządku.uint32_t* buff = malloc(sizeof(Msg));
i kolejneunsigned int asBuffer[sizeof(Msg)];
deklaracje buforów związków będą miały różne rozmiary i żadna z nich nie jest poprawna.malloc
Rozmowa jest poleganie na wyrównanie 4 bajtowy pod maską (nie rób tego) i Unia będzie 4 razy większa niż to musi być ... Rozumiem, że to dla jasności, ale robaki mnie żaden-the- mniej ...Najlepsze wytłumaczenie, jakie znalazłem, to Mike Acton, Understanding Strict Aliasing . Koncentruje się trochę na rozwoju PS3, ale to po prostu GCC.
Z artykułu:
Zasadniczo, jeśli masz
int*
wskazanie na pewną pamięć zawierającą an,int
a następnie wskazujeszfloat*
na tę pamięć i używasz jej jakofloat
zasady łamania reguły. Jeśli Twój kod tego nie przestrzega, optymalizator kompilatora najprawdopodobniej złamie kod.Wyjątkiem od reguły jest a
char*
, który może wskazywać na dowolny typ.źródło
Jest to reguła ścisłego aliasingu, znaleziona w sekcji 3.10 standardu C ++ 03 (inne odpowiedzi zawierają dobre wyjaśnienie, ale żadna nie podała samej reguły):
C ++ 11 i C ++ 14 (podkreślone zmiany):
Dwie zmiany były niewielkie: glvalue zamiast lwartość i wyjaśnienie sprawy agregat / związkowej.
Trzecia zmiana stanowi silniejszą gwarancję (rozluźnia zasadę silnego aliasingu): Nowa koncepcja podobnych typów, które są teraz bezpieczne dla aliasu.
Również sformułowanie C (C99; ISO / IEC 9899: 1999 6.5 / 7; dokładnie to samo sformułowanie zastosowano w ISO / IEC 9899: 2011 § 6.5 ¶7):
źródło
wow(&u->s1,&u->s2)
musiałoby być legalne, nawet gdy wskaźnik jest modyfikowanyu
, a to negowałoby większość optymalizacji, które zasada aliasingu została zaprojektowana w celu ułatwienia.Uwaga
Jest to fragment mojej „Co to jest zasada ścisłego aliasowania i dlaczego nas to obchodzi?” napisać
Co to jest ścisłe aliasing?
W C i C ++ aliasing ma związek z typami wyrażeń, przez które mamy dostęp do przechowywanych wartości. Zarówno w C, jak i C ++ standard określa, które typy wyrażeń mogą być używane do aliasu, które typy. Kompilator i optymalizator mogą założyć, że ściśle przestrzegamy zasad aliasingu, stąd termin ścisła zasada aliasingu . Jeśli spróbujemy uzyskać dostęp do wartości przy użyciu niedozwolonego typu, zostanie to zaklasyfikowane jako zachowanie niezdefiniowane ( UB ). Po niezdefiniowanym zachowaniu wszystkie zakłady są wyłączone, wyniki naszego programu nie są już wiarygodne.
Niestety przy surowych naruszeniach aliasingu często uzyskujemy oczekiwane wyniki, pozostawiając możliwość, że przyszła wersja kompilatora z nową optymalizacją zepsuje kod, który naszym zdaniem był prawidłowy. Jest to niepożądane i warto poznać ścisłe zasady aliasingu i unikać ich łamania.
Aby dowiedzieć się więcej o tym, dlaczego nas to obchodzi, omówimy problemy, które pojawiają się w przypadku naruszenia ścisłych reguł aliasingu, pisania na klawiaturze, ponieważ popularne techniki stosowane przy pisaniu na klawiaturze często naruszają ścisłe reguły aliasingu i sposób prawidłowego pisania.
Wstępne przykłady
Spójrzmy na kilka przykładów, a następnie możemy porozmawiać o tym, co mówią standardy, przeanalizować dalsze przykłady, a następnie zobaczyć, jak uniknąć ścisłego aliasingu i złapać naruszenia, które przegapiliśmy. Oto przykład, który nie powinien dziwić ( przykład na żywo ):
Mamy int * wskazujące na pamięć zajmowaną przez int i jest to prawidłowe aliasing. Optymalizator musi założyć, że przypisania przez ip mogą zaktualizować wartość zajmowaną przez x .
Następny przykład pokazuje aliasing prowadzący do nieokreślonego zachowania ( przykład na żywo ):
W funkcji foo bierzemy int * i liczbę zmiennoprzecinkową * , w tym przykładzie wywołujemy foo i ustawiamy oba parametry tak, aby wskazywały tę samą lokalizację pamięci, która w tym przykładzie zawiera int . Uwaga: reinterpret_cast mówi kompilatorowi, aby traktował wyrażenie tak, jakby miał typ określony przez parametr szablonu. W tym przypadku mówimy, aby traktował wyrażenie & x tak, jakby miał typ float * . Możemy naiwnie oczekiwać, że wynik drugiego cout wyniesie 0, ale przy włączonej optymalizacji przy użyciu -O2 zarówno gcc, jak i clang dają następujący wynik:
Tego nie można się spodziewać, ale jest to całkowicie poprawne, ponieważ wywołaliśmy niezdefiniowane zachowanie. Element zmiennoprzecinkowy nie może poprawnie aliasu int obiektu . Dlatego optymalizator może przyjąć stałą 1 przechowywaną, gdy dereferencje i będą wartością zwracaną, ponieważ przechowywanie przez f nie może poprawnie wpływać na obiekt int . Podłączenie kodu w Eksploratorze kompilatorów pokazuje, że dokładnie tak się dzieje ( przykład na żywo ):
Optymalizator używa analizę aliasów typu (TBAA) zakłada, że 1 zostanie zwrócony i bezpośrednio przenosi wartość stałą do rejestru eax, który przenosi wartość zwracaną. TBAA korzysta z reguł językowych dotyczących typów dozwolonych dla aliasu w celu optymalizacji obciążeń i sklepów. W tym przypadku TBAA wie, że liczba zmiennoprzecinkowa nie może aliasu i int, i optymalizuje obciążenie i .
Teraz do Zestawu Zasad
Co dokładnie mówi standard, że wolno nam, a nie wolno? Standardowy język nie jest prosty, więc dla każdego elementu postaram się podać przykłady kodu, które demonstrują znaczenie.
Co mówi standard C11?
Standard C11 mówi w sekcji 6.5 Wyrażenia, paragraf 7 :
gcc / dzyń ma rozszerzenie a także , że pozwala na przypisanie unsigned int * do int * , chociaż nie są one kompatybilne typy.
Co mówią Craft 17 Draft Standard
Projekt standardu C ++ 17 w sekcji [basic.lval] paragraf 11 mówi:
Warto zauważyć, że znak char nie jest uwzględniony na powyższej liście, jest to zauważalna różnica w stosunku do C, który mówi o typie postaci .
Co to jest Type Punning
Dotarliśmy do tego punktu i możemy się zastanawiać, po co chcieć alias? Zazwyczaj odpowiedź brzmi: pun , często stosowane metody naruszają surowe reguły aliasingu.
Czasami chcemy obejść system typów i zinterpretować obiekt jako inny typ. Nazywa się to pisaniem na klawiaturze , aby ponownie zinterpretować segment pamięci jako inny typ. Pisanie na czcionkach jest przydatne w przypadku zadań, które chcą uzyskać dostęp do podstawowej reprezentacji obiektu w celu przeglądania, transportu lub manipulowania. Typowe obszary, w których spotykamy się ze znakowaniem, to kompilatory, serializacja, kod sieci itp.
Tradycyjnie zostało to osiągnięte poprzez pobranie adresu obiektu, rzutując go na wskaźnik typu, który chcemy ponownie zinterpretować jako, a następnie dostęp do wartości, lub innymi słowy poprzez aliasing. Na przykład:
Jak widzieliśmy wcześniej, nie jest to prawidłowe aliasing, dlatego przywołujemy niezdefiniowane zachowanie. Ale tradycyjnie kompilatory nie korzystały z surowych reguł aliasingu i ten typ kodu zwykle po prostu działał, programiści niestety przyzwyczaili się do robienia tego w ten sposób. Powszechną alternatywną metodą znakowania czcionkami są związki, które są poprawne w C, ale niezdefiniowane zachowanie w C ++ ( patrz przykład na żywo ):
Nie jest to poprawne w C ++ i niektórzy uważają, że celem związków jest wyłącznie implementacja typów wariantów i uważają, że używanie związków do znakowania czcionkami jest nadużyciem.
Jak poprawnie wpisujemy Pun?
Standardową metodą znakowania czcionkami zarówno w C, jak i C ++ jest memcpy . To może wydawać się trochę trudne, ale optymalizator powinien rozpoznać użycie memcpy do znakowania i zoptymalizować go i wygenerować rejestr rejestrujący ruch. Na przykład, jeśli wiemy, że int64_t ma ten sam rozmiar co double :
możemy użyć memcpy :
Przy wystarczającym poziomie optymalizacji każdy przyzwoity nowoczesny kompilator generuje identyczny kod do wspomnianej wcześniej metody reinterpret_cast lub metody unii dla znakowania typu . Analizując wygenerowany kod, widzimy, że używa tylko rejestracji mov (przykład na żywo Eksploratora kompilatora ).
C ++ 20 i bit_cast
W C ++ 20 możemy uzyskać bit_cast ( implementacja dostępna w linku z propozycji ), co daje prosty i bezpieczny sposób na pisanie na klawiaturze, a także jest możliwe do użycia w kontekście constexpr.
Poniżej znajduje się przykład użycia bit_cast do wpisania pun un unsigned int do float , ( zobacz na żywo ):
W przypadku, gdy typy Do i Od nie mają tego samego rozmiaru, wymaga od nas użycia struktury pośredniej15. Użyjemy struktury zawierającej tablicę znaków sizeof (unsigned int) (przy założeniu, że 4-bajtowy unsigned int ) jest typu From i unsigned int jako Type To . :
Szkoda, że potrzebujemy tego typu pośredniego, ale takie jest obecne ograniczenie bit_cast .
Łapanie surowych naruszeń aliasingowych
Nie mamy wielu dobrych narzędzi do przechwytywania ścisłego aliasingu w C ++, narzędzia, które mamy, wychwytują niektóre przypadki ścisłego naruszenia aliasingu oraz niektóre przypadki nieprawidłowego ładowania i przechowywania.
gcc za pomocą flag -fstrict-aliasing i -Wstrict-aliasing może przechwycić niektóre przypadki, chociaż nie bez fałszywych trafień / negatywów. Na przykład następujące przypadki wygenerują ostrzeżenie w gcc ( zobacz na żywo ):
chociaż nie złapie tego dodatkowego przypadku ( zobacz na żywo ):
Chociaż clang pozwala na te flagi, najwyraźniej tak naprawdę nie implementuje ostrzeżeń.
Kolejnym narzędziem, które mamy do dyspozycji, jest ASan, który może wychwycić niedopasowane ładunki i sklepy. Chociaż nie są to bezpośrednio surowe naruszenia aliasingu, są one powszechnym wynikiem ścisłego naruszenia aliasingu. Na przykład następujące przypadki wygenerują błędy środowiska wykonawczego, gdy zostaną zbudowane z clang przy użyciu opcji -fsanitize = adres
Ostatnie narzędzie, które polecę, jest specyficzne dla C ++ i nie jest wyłącznie narzędziem, ale praktyką kodowania, nie zezwalaj na rzutowania w stylu C. Zarówno gcc, jak i clang wygenerują diagnostykę dla rzutów w stylu C przy użyciu -Wold-style-cast . Zmusi to wszystkie niezdefiniowane kalambury do użycia reinterpret_cast, ogólnie reinterpret_cast powinien być flagą do dokładniejszego przeglądu kodu. Łatwiej jest również przeszukać bazę kodu pod kątem reinterpret_cast w celu przeprowadzenia audytu.
W przypadku C mamy już wszystkie narzędzia, a także mamy interpreter tis, analizator statyczny, który wyczerpująco analizuje program dla dużej części języka C. Biorąc pod uwagę wersje C wcześniejszego przykładu, w którym użycie -fstrict-aliasing pomija jeden przypadek ( zobacz na żywo )
tis-interpeter jest w stanie złapać wszystkie trzy, poniższy przykład wywołuje tis-kernal jako tis-interpreter (dane wyjściowe są edytowane dla zwięzłości):
Wreszcie istnieje TySan, który jest obecnie w fazie rozwoju. Ten środek dezynfekujący dodaje informacje o sprawdzaniu typu w segmencie pamięci cienia i sprawdza dostęp, aby sprawdzić, czy naruszają reguły aliasingu. Narzędzie potencjalnie powinno być w stanie wychwycić wszystkie naruszenia aliasingu, ale może mieć duże obciążenie w czasie wykonywania.
źródło
reinterpret_cast
może zrobić ani cocout
może znaczyć. (W porządku jest wspominanie o C ++, ale pierwotne pytanie dotyczyło C i IIUC, przykłady te można równie dobrze napisać w C.)Ścisłe aliasing nie odnosi się tylko do wskaźników, ale także wpływa na referencje, napisałem o tym artykuł na wiki dla deweloperów boost i został tak dobrze przyjęty, że zmieniłem go w stronę mojej witryny konsultingowej. Wyjaśnia całkowicie, co to jest, dlaczego tak bardzo myli ludzi i co z tym zrobić. Biała Księga ścisłego aliasingu . W szczególności wyjaśnia, dlaczego związki są ryzykownym zachowaniem dla C ++ i dlaczego używanie memcpy jest jedyną poprawką przenośną w C i C ++. Mam nadzieję, że to jest pomocne.
źródło
Jako dodatek do tego, co już napisał Doug T., oto prosty przypadek testowy, który prawdopodobnie uruchamia go za pomocą gcc:
czek. c
Kompiluj z
gcc -O2 -o check check.c
. Zwykle (z większością wersji gcc, które próbowałem) powoduje to „ścisły problem aliasingu”, ponieważ kompilator zakłada, że „h” nie może być tego samego adresu co „k” w funkcji „sprawdź”. Z tego powodu kompilator optymalizujeif (*h == 5)
away i zawsze wywołuje printf.Dla zainteresowanych jest kod asemblera x64, stworzony przez gcc 4.6.3, działający na Ubuntu 12.04.2 dla x64:
Zatem warunek if całkowicie zniknął z kodu asemblera.
źródło
long long*
Iint64_t
*). Można się spodziewać, że rozsądny kompilator powinien rozpoznać, żelong long*
iint64_t*
może uzyskać dostęp do tej samej pamięci, jeśli są one przechowywane identycznie, ale takie traktowanie nie jest już modne.Pisanie na klawiaturze za pomocą rzutów wskaźnikowych (w przeciwieństwie do używania związku) jest głównym przykładem przełamania ścisłego aliasingu.
źródło
fpsync()
dyrektywę między zapisem jako fp a odczytem jako int lub vice versa [w implementacjach z oddzielnymi potokami liczb całkowitych i FPU i pamięci podręcznej , taka dyrektywa może być droga, ale nie tak kosztowna, jak to, że kompilator wykonuje taką synchronizację przy każdym dostępie do związku]. Lub implementacja może określić, że wynikowa wartość nigdy nie będzie użyteczna, z wyjątkiem okoliczności wykorzystujących wspólne sekwencje początkowe.Zgodnie z uzasadnieniem C89 autorzy Standardu nie chcieli wymagać, aby kompilatory otrzymywały kod taki jak:
powinno być wymagane do ponownego załadowania wartości
x
między poleceniem przypisania a instrukcją return, aby umożliwić możliwość, którap
może wskazywaćx
, i przypisanie, które*p
może w konsekwencji zmienić wartośćx
. Pojęcie, że kompilator powinien mieć prawo domniemywać, że w takich sytuacjach jak wyżej nie będzie aliasu, nie było kontrowersyjne.Niestety, autorzy C89 napisali swoją regułę w taki sposób, że jeśli czytany dosłownie, sprawia, że nawet następująca funkcja wywołuje Niezdefiniowane Zachowanie:
ponieważ używa wartości typu
int
aby uzyskać dostęp do obiektu typustruct S
, orazint
nie należy do typów, które mogą być używane do uzyskania dostępu dostruct S
. Ponieważ absurdem byłoby traktować wszelkie użycie struktur typu i związków innych niż znaki jako Zachowanie nieokreślone, prawie wszyscy zdają sobie sprawę, że istnieją przynajmniej niektóre okoliczności, w których wartość jednego typu może być wykorzystana do uzyskania dostępu do obiektu innego typu . Niestety Komitet ds. Norm C nie zdefiniował, jakie są te okoliczności.Duża część problemu wynika z Raportu Defektu # 028, który pytał o zachowanie programu takiego jak:
Raport Defektów # 28 stwierdza, że program wywołuje Nieokreślone Zachowanie, ponieważ czynność napisania członka związku typu „podwójny” i odczytu jednego typu „int” wywołuje zachowanie Zdefiniowane w implementacji. Takie rozumowanie jest bezsensowne, ale stanowi podstawę dla reguł typu efektywnego, które niepotrzebnie komplikują język, nie robiąc nic, aby rozwiązać pierwotny problem.
Najlepszym sposobem na rozwiązanie pierwotnego problemu byłoby prawdopodobnie potraktowanie przypisu dotyczącego celu reguły tak, jakby była ona normatywna, i uczyniła regułę niemożliwą do wyegzekwowania, z wyjątkiem przypadków, które faktycznie wymagają sprzecznych dostępów przy użyciu aliasów. Biorąc pod uwagę coś takiego:
Wewnątrz nie ma konfliktu,
inc_int
ponieważ wszystkie dostępy do pamięci, do której*p
uzyskano dostęp, są wykonywane z użyciem wartości typuint
, i nie ma konfliktu,test
ponieważp
jest wyraźnie wywiedziony zstruct S
, a przy następnyms
użyciu, wszystkie dostępu do pamięci, która kiedykolwiek zostanie wykonana przezp
już się wydarzyło.Jeśli kod został nieznacznie zmieniony ...
W tym przypadku występuje konflikt aliasingu między
p
dostępem dos.x
oznaczonej linii, ponieważ w tym momencie wykonania istnieje inne odwołanie, które zostanie wykorzystane do uzyskania dostępu do tego samego magazynu .Gdyby Raport Defektów 028 powiedział, że oryginalny przykład przywołał UB z powodu nakładania się między tworzeniem i użyciem dwóch wskaźników, to uczyniłoby sprawę o wiele bardziej przejrzystą bez konieczności dodawania „Skutecznych typów” lub innej takiej złożoności.
źródło
Po przeczytaniu wielu odpowiedzi czuję potrzebę dodania czegoś:
Ścisłe aliasing (który opiszę za chwilę) jest ważne, ponieważ :
Dostęp do pamięci może być kosztowny (pod względem wydajności), dlatego dane są rejestrowane w rejestrach procesora, zanim zostaną zapisane z powrotem w pamięci fizycznej.
Jeśli dane w dwóch różnych rejestrach procesora zostaną zapisane w tym samym obszarze pamięci, nie możemy przewidzieć, które dane „przetrwają” gdy kodujemy w C.
W asemblerze, w którym ręcznie kodujemy ładowanie i rozładowywanie rejestrów procesora, będziemy wiedzieć, które dane pozostają nienaruszone. Ale C (na szczęście) streszcza ten szczegół.
Ponieważ dwa wskaźniki mogą wskazywać to samo miejsce w pamięci, może to skutkować złożonym kodem, który obsługuje możliwe kolizje .
Ten dodatkowy kod jest powolny i obniża wydajność, ponieważ wykonuje dodatkowe operacje odczytu / zapisu w pamięci, które są zarówno wolniejsze, jak i (prawdopodobnie) niepotrzebne.
Reguła aliasing Strict pozwala nam uniknąć nadmiarowego kodu maszynowego w przypadkach, w których powinny być bezpiecznie założyć, że dwa wskaźniki nie wskazują na ten sam blok pamięci (patrz również
restrict
słowa kluczowego).W przypadku ścisłego aliasingu można bezpiecznie założyć, że wskaźniki różnych typów wskazują różne lokalizacje w pamięci.
Jeśli kompilator zauważy, że dwa wskaźniki wskazują różne typy (na przykład an
int *
i afloat *
), przyjmie, że adres pamięci jest inny i nie ochroni przed kolizjami adresów pamięci, co spowoduje szybszy kod maszynowy.Na przykład :
Załóżmy następującą funkcję:
Aby obsłużyć przypadek, w którym
a == b
(oba wskaźniki wskazują na tę samą pamięć), musimy zamówić i przetestować sposób ładowania danych z pamięci do rejestrów procesora, aby kod mógł wyglądać następująco:ładuj
a
ib
z pamięci.dodaj
a
dob
.zapisz
b
i załaduj ponowniea
.(zapisz z rejestru procesora do pamięci i załaduj z pamięci do rejestru procesora).
dodaj
b
doa
.zapisz
a
(z rejestru procesora) do pamięci.Krok 3 jest bardzo powolny, ponieważ musi uzyskać dostęp do pamięci fizycznej. Wymagana jest jednak ochrona przed przypadkami, w których
a
ib
wskazywanie tego samego adresu pamięci.Ścisłe aliasing pozwoliłoby nam temu zapobiec, mówiąc kompilatorowi, że te adresy pamięci są wyraźnie różne (co w tym przypadku pozwoli na dalszą optymalizację, której nie można wykonać, jeśli wskaźniki dzielą adres pamięci).
Można to powiedzieć kompilatorowi na dwa sposoby, używając różnych typów wskazań. to znaczy:
Za pomocą
restrict
słowa kluczowego. to znaczy:Teraz, spełniając regułę ścisłego aliasingu, można uniknąć kroku 3, a kod będzie działał znacznie szybciej.
W rzeczywistości przez dodanie
restrict
słowa kluczowego można zoptymalizować całą funkcję, aby:ładowanie
a
ib
z pamięci.dodaj
a
dob
.zapisz wynik zarówno do, jak
a
i dob
.Ta optymalizacja nie mogła być wcześniej wykonana z powodu możliwej kolizji (gdzie
a
ib
byłby potrojony zamiast podwojony).źródło
b
(nie przeładowujemy) i przeładowujemya
. Mam nadzieję, że teraz jest jaśniej.restrict
, ale sądzę, że ten drugi w większości przypadków byłby bardziej skuteczny, a złagodzenie niektórych ograniczeńregister
pozwoliłoby mu wypełnić niektóre przypadki, w którychrestrict
nie pomogłoby. Nie jestem pewien, czy kiedykolwiek „ważne” było potraktowanie Standardu jako pełnego opisu wszystkich przypadków, w których programiści powinni oczekiwać, że kompilatory rozpoznają dowody aliasingu, a nie tylko opisywania miejsc, w których kompilatory muszą zakładać aliasing, nawet jeśli nie ma konkretnych dowodów na to .restrict
słowo kluczowe minimalizuje nie tylko szybkość operacji, ale także ich liczbę, co może mieć znaczenie ... To znaczy, w końcu najszybsza operacja to w ogóle żadna operacja :)Ścisłe aliasing nie pozwala różnym typom wskaźników na te same dane.
Ten artykuł powinien pomóc Ci w szczegółowym zrozumieniu problemu.
źródło
int
Struktura zawierająca anint
).Technicznie w C ++ zasada ścisłego aliasingu prawdopodobnie nigdy nie ma zastosowania.
Zwróć uwagę na definicję pośrednictwa ( * operator ):
Również z definicji glvalue
Zatem w każdym dobrze zdefiniowanym śladzie programu glvalue odnosi się do obiektu. Tak więc nigdy nie obowiązuje tak zwana zasada ścisłego aliasingu. To może nie być to, czego chcieli projektanci.
źródło
int foo;
, do czego służy wyrażenie lvalue*(char*)&foo
? Czy to obiekt typuchar
? Czy ten przedmiot powstaje w tym samym czasie cofoo
? Czy pisaniefoo
zmieniłoby wartość przechowywaną wyżej wspomnianego obiektu typuchar
? Jeśli tak, to czy istnieje jakakolwiek reguła, która pozwala na dostęp do zapisanej wartości obiektu typuchar
za pomocą wartości typuint
?int i;
tworzy cztery obiekty każdego typu znakówin addition to one of type
int? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) i i` orazi
. Wreszcie, w standardzie nie ma nic, co pozwala nawetvolatile
kwalifikowanemu wskaźnikowi na dostęp do rejestrów sprzętowych, które nie spełniają definicji „obiektu”.