Jaka jest ścisła zasada aliasingu?

Pytając o powszechne, nieokreślone zachowanie w C, dusze bardziej oświecone niż ja odwołałem się do ścisłej zasady aliasingu.
O czym oni mówią?

Author: Community, 2008-09-19

11 answers

Typową sytuacją, w której napotkasz ścisłe problemy z aliasingiem, jest nakładanie struktury (jak MSG urządzenia / sieci) na bufor o rozmiarze słowa systemu (jak wskaźnik do uint32_tS lub uint16_ts). Gdy nakładasz strukturę na taki bufor lub bufor na taką strukturę poprzez rzutowanie wskaźnikiem, możesz łatwo naruszyć ścisłe 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 to samo kawałek pamięci. Mógłbym wtedy naiwnie kodować coś takiego:

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

Ścisła reguła aliasingu sprawia, że konfiguracja ta jest nielegalna: dereferowanie wskaźnika aliasującego obiekt, który nie jest zgodny z typu lub jednego z innych typów dozwolonych przez C 2011 6.5 paragraph 71 jest niezdefiniowanym zachowaniem. Niestety, nadal możesz kodować w ten sposób, Może uzyskać Ostrzeżenia, dobrze skompilować, tylko po to, aby mieć dziwne nieoczekiwane zachowanie podczas uruchamiania kodu.

(GCC wydaje się nieco niespójna w swojej zdolności do udzielania aliasingowych ostrzeżeń, czasami dając nam przyjazne Ostrzeżenie, a czasami 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 zasadzie, nie trzeba myśleć o wstawianiu instrukcji, aby odświeżyć zawartość buff przy każdym uruchomieniu pętli. Zamiast tego, podczas optymalizacji, z pewnymi irytująco nieskomplikowanymi założeniami dotyczącymi aliasingu, może pominąć te instrukcje, załadować buff[0] i buff[1] do rejestrów CPU raz przed uruchomieniem pętli i przyspieszyć ciało pętli. Zanim wprowadzono ścisłe aliasing, kompilator musiał żyć w stanie paranoi, że zawartość buff może zmieniać się w dowolnym momencie i z dowolnego miejsca przez każdego. Aby uzyskać dodatkową przewagę wydajności i zakładając, że większość ludzi nie wpisuje wskaźników Kalambury, wprowadzono ścisłą regułę aliasingu.

Pamiętaj, jeśli uważasz, że przykład jest wymyślony, może się to zdarzyć nawet, jeśli przekazujesz bufor do innej funkcji wykonującej wysyłanie za Ciebie, jeśli zamiast tego masz.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

I przepisać naszą wcześniejszą pętlę, aby skorzystać z tej wygodnej funkcji

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Kompilator może lub nie może być w stanie lub wystarczająco inteligentny, aby spróbować inline SendMessage i może lub nie zdecydować się ponownie załadować lub nie załadować premii. Jeśli SendMessage jest częścią innego API, które jest kompilowane osobno, prawdopodobnie zawiera instrukcje ładowania zawartości Buffa. Wtedy ponownie, Może jesteś w C++ i jest to jakaś implementacja szablonów tylko nagłówek, że kompilator myśli, że może inline. A może to tylko coś, co napisałeś w swoim .plik c dla Twojej wygody. W każdym razie nieokreślone zachowanie może nadal wystąpić. Nawet jeśli wiemy, co dzieje się pod maską, to nadal jest to naruszenie zasady, więc żadne dobrze zdefiniowane zachowanie nie jest gwarantowane. Więc samo zawijanie w funkcję, która bierze nasze słowo delimited buffer nie musi pomocy.

Jak 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.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Możesz wyłączyć ścisłe aliasing w swoim kompilatorze (f [no -] strict-aliasing w gcc))

  • Możesz użyć char* do aliasingu zamiast słowa systemu. Zasady dopuszczają wyjątek dla char* (w tym signed char i unsigned char). Zawsze zakłada się, że char* aliasy innych typów. Nie będzie to jednak działać w inny sposób: nie ma założenia, że Twoja struktura aliasuje bufor znaków.

Beginner beware

Jest to tylko jedno potencjalne pole minowe, gdy nakładają się na siebie dwa typy. Powinieneś również dowiedzieć się o endianness, w 2011 roku w ramach programu word alignbars = justify stworzono nową wersję programu word alignbars = justify.]} prawidłowo.

Przypis

1 w 2011 roku C 2011 6.5 7 umożliwia dostęp do lvalue:

  • Typ zgodny z efektywnym typem obiektu,
  • kwalifikowana wersja typu kompatybilna z efektywnym typem obiektu,
  • typ, który jest typem podpisanym lub niepodpisanym odpowiadającym efektywnemu typowi obiektu,
  • typ, który jest typem podpisanym lub niepodpisanym odpowiadającym kwalifikowanej wersji efektywny typ obiektu,
  • Typ zbiorczy lub union, który zawiera jeden z wyżej wymienionych typów wśród swoich członków (w tym rekurencyjnie członek podagregatu lub zawartej Unii), lub
  • typ znaku.
 496
Author: Doug T.,
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
2018-06-09 14:32:04

Najlepsze wyjaśnienie, jakie znalazłem, to Mike Acton, rozumienie ścisłego aliasingu . Skupia się trochę na rozwoju PS3, ale to w zasadzie tylko GCC.

Z artykułu:

" Strict aliasing jest założeniem kompilatora C (lub c++), że dereferowanie wskaźników do obiektów różnych typów nigdy nie będzie odnosić się do tego samego miejsca pamięci (tj. aliasów do siebie nawzajem.)"

Więc w zasadzie Jeśli masz int* wskazujący na jakąś pamięć zawiera int, a następnie wskazujesz {[2] } na tę pamięć i używasz jej jako {[3] } łamiesz regułę. Jeśli twój kod nie respektuje tego, to optymalizator kompilatora najprawdopodobniej złamie twój kod.

Wyjątkiem od reguły jest char*, który może wskazywać na dowolny typ.

 219
Author: Niall,
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-10-16 14:46:56

Jest to ścisła reguła aliasingu, znaleziona w sekcji 3.10 standardu C++03 (Inne odpowiedzi dają dobre wyjaśnienie, ale żadna nie podała samej reguły):

Jeśli program próbuje uzyskać dostęp do przechowywanej wartości obiektu przez lvalue innego niż jeden z następujących typów, zachowanie jest niezdefiniowane:

  • Typ dynamiczny obiektu,
  • a CV-kwalifikowana wersja dynamicznego typu obiektu,
  • typ, który jest Typ signed lub unsigned odpowiadający typowi dynamicznemu obiektu,
  • typ, który jest typem podpisanym lub niepodpisanym odpowiadającym kwalifikowanej do CV wersji dynamicznego typu obiektu,
  • Typ zbiorczy lub union, który zawiera jeden z wyżej wymienionych typów wśród swoich członków (w tym rekurencyjnie członek podagregatu lub zawartej Unii),
  • typ, który jest (ewentualnie kwalifikowanym cv) typem klasy bazowej dynamicznego typu obiektu,
  • A char LUB unsigned char typu.

C++11 i C++14 sformułowanie (zmiany):

Jeśli program próbuje uzyskać dostęp do przechowywanej wartości obiektu za pomocą glvalue innego niż jeden z następujących typów, zachowanie jest niezdefiniowane:

  • Typ dynamiczny obiektu,
  • a CV-kwalifikowana wersja dynamicznego typu obiektu,
  • Typ podobny (zdefiniowany w 4.4) do dynamicznego typ obiektu,
  • typ, który jest typem podpisanym lub niepodpisanym odpowiadającym typowi dynamicznemu obiektu,
  • typ, który jest typem podpisanym lub niepodpisanym odpowiadającym kwalifikowanej do CV wersji dynamicznego typu obiektu,
  • Typ agregujący lub union, który zawiera jeden z wyżej wymienionych typów wśród swoich elementów lub niestatycznych elementów danych (w tym, rekurencyjnie, element lub niestatyczny element danych podagregować lub zawrzeć Unię),
  • typ, który jest (ewentualnie kwalifikowanym cv) typem klasy bazowej dynamicznego typu obiektu,
  • A char LUB unsigned char typu.

Dwie zmiany były niewielkie: glvalue zamiast lvalue , oraz wyjaśnienie przypadku zbiorczego/unijnego.

Trzecia zmiana daje silniejszą gwarancję (rozluźnia regułę silnego aliasingu): nowa koncepcja podobnych typów, które są teraz bezpieczne dla alias.


Również C sformułowanie (C99; ISO / IEC 9899: 1999 6.5 / 7; dokładnie to samo sformułowanie jest używane w ISO / IEC 9899:2011 §6.5 ¶7):

Obiekt powinien mieć zapisaną wartość dostępną tylko przez lvalue wyrażenie, które ma jeden z następujących typów 73) lub 88):

  • Typ zgodny z efektywnym typem obiektu,
  • kwalifikowana wersja typu kompatybilna z efektywnym typem obiekt,
  • a typ, który jest typem podpisanym lub niepodpisanym odpowiadającym efektywny typ obiektu,
  • typ, który jest typem podpisanym lub niepodpisanym odpowiadającym kwalifikowana wersja efektywnego typu obiektu,
  • agregat lub typ Unii, który obejmuje jeden z wyżej wymienionych typy wśród jego członków (w tym rekurencyjnie członek podagregować lub zawrzeć Unię), lub
  • typ znaku.

73) or 88) intencja lista ta ma na celu określenie okoliczności, w których obiekt może lub nie może być aliasowany.

 125
Author: Ben Voigt,
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-10-19 22:30:34

Ścisłe aliasing nie odnosi się tylko do wskaźników, wpływa również na odniesienia, napisałem o tym artykuł dla Boost developer wiki i został tak dobrze przyjęty, że przekształciłem go w stronę na mojej stronie konsultingowej. Wyjaśnia całkowicie, co to jest, dlaczego tak bardzo myli ludzi i co z tym zrobić. Ścisły Aliasing Biały Papier . W szczególności wyjaśnia, dlaczego używanie memcpy jest ryzykowne dla C++ i dlaczego używanie memcpy jest jedynym rozwiązaniem przenośnym zarówno w C jak i C++. Nadzieja to jest pomocne.

 40
Author: Patrick,
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
2011-06-19 23:46:55

Jako dodatek do tego, co napisał Doug T., tutaj jest to prosty przypadek testowy, który prawdopodobnie uruchamia go za pomocą gcc:

Sprawdzam.c
#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Skompiluj z gcc -O2 -o check check.c . Zwykle (przy większości wersji gcc, które próbowałem) wypisuje to "strict aliasing problem", ponieważ kompilator zakłada, że" h "nie może być tym samym adresem co" k "w funkcji" check". Z tego powodu kompilator optymalizuje if (*h == 5) i zawsze wywołuje printf.

Dla tych, którzy są zainteresowani tutaj jest x64 kod asemblera, wyprodukowany przez gcc 4.6.3, działający na ubuntu 12.04.2 dla x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Więc warunek if został całkowicie usunięty z kodu asemblera.

 31
Author: Ingo Blackman,
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
2013-05-14 02:37:04

Uwaga

To fragment mojej "Jaka jest ścisła zasada aliasingu i dlaczego nas to obchodzi?" zapis.

Czym jest ścisłe aliasing?

W C i C++ aliasing ma związek z typami wyrażeń, za pośrednictwem których możemy uzyskać dostęp do przechowywanych wartości. Zarówno w C, jak i C++ standard określa, które typy wyrażeń mogą być aliasowane. Kompilator i optymalizator mogą zakładać, że ściśle przestrzegamy zasad aliasingu, stąd termin ścisła zasada aliasingu . Jeśli spróbujemy uzyskać dostęp do wartości za pomocą typu niedozwolonego, jest ona klasyfikowana jako undefined behavior(UB ). Gdy mamy nieokreślone zachowanie, wszystkie zakłady są wyłączone, wyniki naszego programu nie są już wiarygodne.

Niestety z surowymi naruszeniami aliasingu, często uzyskujemy oczekiwane wyniki, pozostawiając możliwość, że przyszła wersja kompilatora z nową optymalizacją złamie kod, który myśleliśmy, że był poprawny. To jest niepożądane i warto zrozumieć surowe zasady aliasingu i jak uniknąć ich naruszania.

Aby dowiedzieć się więcej o tym, dlaczego nam zależy, omówimy problemy, które pojawiają się podczas łamania ścisłych reguł aliasingu, typowania kar, ponieważ popularne techniki używane w typowaniu kar często naruszają ścisłe reguły aliasingu i jak poprawnie wpisywać Kalambury.

Przykłady wstępne

Spójrzmy na kilka przykładów, a następnie możemy porozmawiać o tym, co mówią standardy, przeanalizuj kolejne przykłady, a następnie zobacz, jak uniknąć ścisłego aliasingu i naruszeń, które przegapiliśmy. Oto przykład, który nie powinien być zaskakujący (przykład na żywo):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Mamy int* wskazujący na pamięć zajmowaną przez int i jest to poprawny aliasing. Optymalizator musi zakładać, że przypisania przez ip mogą uaktualnić wartość zajmowaną przez x .

Następny przykład pokazuje aliasing, który prowadzi do undefined behavior (live example):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

W funkcji foo bierzemy int*i float*, w tym przykładzie wywołujemy foo i ustawiamy oba parametry tak, aby wskazywały na tę samą lokalizację pamięci, która w tym przykładzie zawiera int . Zauważ, że reinterpret_cast mówi kompilatorowi, aby traktował wyrażenie tak, jakby miało Typ określony przez parametr szablonu. W tym przypadku mówimy mu, aby traktować wyrażenie &x jakby miało Typ float * . Możemy naiwnie oczekiwać, że wynik drugiego cout będzie 0 ale z optimization enabled using -O2 zarówno gcc i clang dają następujący wynik:

0
1

Które nie może być oczekiwane, ale jest całkowicie poprawne, ponieważ wywołaliśmy niezdefiniowane zachowanie. A float nie może poprawnie nazwać obiektu int . Dlatego optymalizator może przyjąć stałą 1 przechowywaną gdy dereferencja i będzie wartością zwracaną, ponieważ store through f nie może skutecznie wpływać na obiekt int. Podłączenie kodu w Eksploratorze kompilatora pokazuje dokładnie to, co się dzieje (przykład na żywo):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

Optymalizator wykorzystujący analizę aliasów opartych na typach (Tbaa) zakłada 1 zostanie zwrócona i bezpośrednio przeniesie wartość stałą do rejestru eax, który niesie wartość zwracaną. TBAA wykorzystuje języki określają, jakie typy mogą być aliasowane w celu optymalizacji obciążeń i magazynów. W tym przypadku tbaa wie, że float nie może alias i int i optymalizuje obciążenie i.

Teraz, do Księgi reguł

Co dokładnie mówi norma, że wolno nam, a nie wolno nam robić? Język Standardowy nie jest prosty, więc dla każdego elementu postaram się podać przykłady kodu, które demonstrują znaczenie.

Co robi C11 standardowe powiedz?

C11 norma mówi co następuje w sekcji paragraf 6.5 wyrażenia 7:

Obiekt ma zapisaną wartość dostępną tylko przez wyrażenie lvalue, które ma jeden z następujących typów:88) - Typ zgodny z efektywnym typem obiektu,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- kwalifikowana wersja typu kompatybilna z efektywnym typem obiektu,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- typ, który jest typem podpisanym lub niepodpisanym odpowiadającym typowi efektywnemu obiektu,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

Gcc / clang posiada rozszerzenie i również, które umożliwia przypisanie unsigned int * do int*, nawet jeśli nie są one kompatybilnymi typami.

- typ, który jest typem podpisanym lub niepodpisanym odpowiadającym kwalifikowanej wersji efektywnego typu obiektu,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- agregat Typ Unii, który zawiera jeden z wyżej wymienionych typów wśród swoich członków (w tym rekurencyjnie członek podagregatu lub zawartej Unii), lub

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- typ znaku.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Co mówi c++17 Draft Standard

The C++17 draft standard in section [basic.lval] paragraf 11 mówi:

Jeśli program próbuje uzyskać dostęp do zapisanej wartości obiektu za pomocą wartości glvalue innej niż jedna z następujące typy zachowanie jest niezdefiniowane:63 (11.1) - Typ dynamiczny obiektu,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - kwalifikowana do CV wersja dynamicznego typu obiektu,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - Typ podobny (zdefiniowany w 7.5) do typu dynamicznego obiektu,

(11.4) - typ, który jest typem podpisanym lub niepodpisanym odpowiadającym typowi dynamicznemu obiektu,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - Typ a jest to typ signed lub unsigned odpowiadający kwalifikowanej do CV wersji dynamicznego typu obiektu,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
Typ zbiorczy (ang. aggregate lub union type) - Typ zbiorczy, który zawiera jeden z wyżej wymienionych typów wśród swoich elementów lub niestatycznych elementów danych (w tym rekurencyjnie element lub niestatyczny element danych podagregatu lub zawartej Unii), {33]}
struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - Typ będący (ewentualnie CV-kwalifikowaną) klasą bazową typu dynamicznego typ obiektu,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - znak, znak unsigned lub typ STD::byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Warto zauważyć signed char nie znajduje się na powyższej liście, jest to znacząca różnica od C , który mówi typ znaku .

Co to jest typowanie

Doszliśmy do tego punktu i możemy się zastanawiać, dlaczego mielibyśmy chcieć alias dla? Odpowiedź zazwyczaj jest typu kalambur , często zastosowane metody naruszają ścisłe zasady aliasingu.

Czasami chcemy obejść system typów i zinterpretować obiekt jako inny typ. Nazywa się to typem , w celu reinterpretacji segmentu pamięci jako innego typu. Typ {[38] } jest przydatny dla zadań, które chcą mieć dostęp do podstawowej reprezentacji obiektu do przeglądania, transportu lub manipulowania. Typowe obszary, w których znajdujemy Typ, to Kompilatory, serializacja, kod sieciowy itp…

Tradycyjnie odbywa się to poprzez pobranie adresu obiektu, oddanie go do wskaźnika typu, w którym chcemy go ponownie zinterpretować, a następnie uzyskanie dostępu do wartości, innymi słowy przez aliasing. Na przykład:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( “%f\n”, *fp ) ;

Jak widzieliśmy wcześniej, nie jest to poprawne aliasing, więc wywołujemy nieokreślone zachowanie. Ale tradycyjnie Kompilatory nie korzystały ze ścisłych reguł aliasingu i ten typ kodu zwykle po prostu działał, programiści mają niestety przyzwyczaiłem się do robienia rzeczy w ten sposób. W C++ (undefined behavior zobacz przykład na żywo):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member
Nie jest to poprawne w C++ i niektórzy uważają, że celem związków jest wyłącznie implementacja typów wariantowych i uważają, że używanie związków do karania typów jest nadużyciem.

Jak poprawnie wpisać kalambur?

Standardowa Metoda dla typu zarówno w C, jak i C++ to memcpy . Może to wydawać się trochę ciężkie, ale optymalizator powinien rozpoznać użyciememcpy Dlatypu punning i zoptymalizować go i wygenerować rejestr do zarejestrowania ruchu. Na przykład jeśli wiemy, że int64_t jest tego samego rozmiaru co double :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

Możemy użyć memcpy :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

Na wystarczającym poziomie optymalizacji każdy porządny nowoczesny kompilator generuje identyczny kod do wspomnianego wcześniej reinterpret_cast method or union method for type. Analizując wygenerowany kod widzimy, że używa tylko rejestru mov (live Compiler Explorer przykład ).

C++20 i bit_cast

W C++20 możemy zyskać bit_cast (implementacja dostępna w linku z proposal ), który daje prosty i bezpieczny sposób na wpisywanie kalamburu, jak również jest użyteczny w kontekście constexpr.

Poniżej znajduje się przykład jak użyj bit_cast aby wpisać unsigned int do float, (zobacz to na żywo):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

W przypadku, gdy typy to i From nie mają tej samej wielkości, wymaga to użycia struktury pośredniej15. Będziemy używać struktury zawierającej sizeof( unsigned int )tablica znaków ( zakłada 4 bajt unsigned int) jako Z Typui unsigned int jako do Typ.:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

Szkoda, że potrzebujemy tego typu pośredniego, ale jest to obecne ograniczenie bit_cast .

Łapanie Ścisłych Naruszeń Aliasingu

Nie mamy zbyt wielu dobrych narzędzi do przechwytywania ścisłego aliasingu w C++, narzędzia, które mamy, wychwycą niektóre przypadki ścisłego naruszenia aliasingu oraz niektóre przypadki nieprawidłowego dopasowania obciążeń i magazynów.

Gcc za pomocą flagi - fstrict-aliasing i -Wstrict-aliasing można złapać niektóre przypadki, choć nie bez fałszywych pozytywów/negatywów. Na przykład następujące przypadki wygenerują ostrzeżenie w gcc (see it live):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

Chociaż nie złapie tego dodatkowego przypadku ( zobacz go na żywo):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Chociaż clang zezwala na te flagi, najwyraźniej nie implementuje ostrzeżeń.

Innym narzędziem, które mamy do dyspozycji, jest ASan, które może wychwycić niewspółosiowe ładunki i magazyny. Chociaż nie są one bezpośrednio ścisłe naruszenia aliasingu są one częstym wynikiem ścisłych naruszeń aliasingu. Na przykład następujące przypadki generują błędy uruchomieniowe przy użyciu clang przy użyciu - fsanitize = address

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

Ostatnie narzędzie, które polecę, jest specyficzne dla C++ i nie jest ściśle narzędziem, ale praktyką kodowania, nie zezwalaj na odlewy w stylu C. Zarówno gcc, jak i clang będą produkować diagnostykę dla odlewów w stylu C przy użyciu -Wold-style-cast. Wymusi to użycie dowolnego nieokreślonego typu reinterpret_cast, ogólnie reinterpret_cast powinien być flagą dla bliższego przeglądu kodu. Łatwiej jest również przeszukać bazę kodu pod kątem reinterpret_cast w celu przeprowadzenia audytu.

Dla C mamy już wszystkie narzędzia i mamy również tis-interpreter, statyczny analizator, który wyczerpująco analizuje program dla dużej podzbioru języka C. Podano wersję C wcześniejszego przykładu, gdzie użycie - fstrict-aliasing pomija jeden przypadek (Zobacz też live )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

Tis-interpeter jest w stanie przechwycić wszystkie trzy, poniższy przykład wywołuje tis-kernal jako tis-interpreter (wyjście jest edytowane dla zwięzłości):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Wreszcie jest TySan , który jest obecnie w fazie rozwoju. Ten sanitizer dodaje informacje o sprawdzaniu typu w segmencie pamięci cienia i sprawdza dostęp, aby sprawdzić, czy nie naruszają one reguł aliasingu. Narzędzie potencjalnie powinno być w stanie wychwycić wszystkie naruszenia aliasingu, ale może mieć duży czas działania nad głową.

 23
Author: Shafik Yaghmour,
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
2018-08-20 13:14:43

Typ punning za pomocą rzutów wskaźnikowych (w przeciwieństwie do używania Unii) jest głównym przykładem łamania ścisłego aliasingu.

 15
Author: Chris Jester-Young,
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
2014-07-06 18:31:53

Zgodnie z uzasadnieniem C89, autorzy standardu nie chcieli wymagać, aby Kompilatory podały Kod jak:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

Powinno być wymagane ponowne wczytanie wartości x pomiędzy instrukcją assignment I return, tak aby p mogło wskazywać na x, a przypisanie do *p może w konsekwencji zmienić wartość x. Pojęcie, że kompilator powinien mieć prawo zakładać, że nie będzie aliasingu w sytuacjach takich jak powyżej nie było kontrowersyjne.

Niestety, autorzy C89 napisali swoją regułę w sposób, który, jeśli zostanie odczytany dosłownie, sprawi, że nawet następująca funkcja wywoła niezdefiniowane zachowanie:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

Ponieważ używa lvalue typu int, aby uzyskać dostęp do obiektu typu struct S, a int nie należy do typów, które mogą być używane do dostępu do struct S. Ponieważ byłoby absurdem traktować wszelkie użycie nie-znakowych członków struktur i związków jako nieokreślone zachowanie, prawie każdy zdaje sobie sprawę, że istnieją co najmniej pewne okoliczności, w których lvalue jednego typu może być użyty do uzyskania dostępu do obiektu innego typu. Niestety Komitet Normalizacyjny C nie sprecyzował, jakie są te okoliczności.

Większość problemu jest wynikiem raportu defektu #028, który pytał o zachowanie programu takiego jak:]}
int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

Raport defektu # 28 stwierdza, że program wywołuje nieokreślone zachowanie, ponieważ akcja pisania członka Związku typu "double" i odczytanie jednego z typów "int" wywołuje zachowanie zdefiniowane przez implementację. Takie rozumowanie jest bezsensowne, ale stanowi podstawę efektywnych reguł typu, które niepotrzebnie komplikują język, nie robiąc nic, aby rozwiązać pierwotny problem.

Najlepszym sposobem rozwiązania pierwotnego problemu byłoby prawdopodobnie leczenie przypis o celu reguły, jakby była normatywna i wykonana reguła nieegzekwowalna, z wyjątkiem przypadków, które faktycznie wiążą się ze sprzecznymi dostęp za pomocą aliasów. Podano coś w rodzaju:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

Nie ma konfliktu wewnątrz inc_int, ponieważ wszystkie dostępy do magazynu, do którego dostęp uzyskuje się poprzez *p, są wykonywane za pomocą lvalue typu int, i nie ma konfliktu w test, ponieważ p jest wyraźnie pochodną struct S, a przy następnym użyciu s, wszystkie dostępy do magazynu, które kiedykolwiek zostaną wykonane przez p będą już miały miejsce.

Jeśli kod został nieznacznie zmieniony...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Tutaj jest konflikt aliasingu pomiędzy p A dostępem do s.x w zaznaczonej linii, ponieważ w tym momencie wykonania istnieje inne odniesienie , które zostanie użyte do uzyskania dostępu do tego samego magazynu.

Gdyby Defect Report 028 powiedział, że oryginalny przykład odwołał się do UB ze względu na nakładanie się między tworzeniem i używaniem dwóch wskaźników, co uczyniłoby rzeczy o wiele bardziej jasnymi bez konieczności dodawania "skutecznych typów" lub innych takich złożoności.

 10
Author: supercat,
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
2018-03-10 00:42:31

Po przeczytaniu wielu odpowiedzi, czuję potrzebę dodania czegoś:

Ścisłe aliasowanie (które opiszę nieco) jest ważne, ponieważ :

  1. Dostęp do pamięci może być kosztowny( pod względem wydajności), dlatego dane są manipulowane w rejestrach procesora , zanim zostaną zapisane z powrotem do pamięci fizycznej.

  2. Jeśli dane w dwóch różnych rejestrach procesora będą zapisywane do tej samej przestrzeni pamięci, nie możemy przewidzieć, który dane "przetrwają" kiedy kodujemy w C.

    W assembly, gdzie kodujemy ładowanie i rozładowywanie rejestrów CPU ręcznie, będziemy wiedzieć, które dane pozostają nienaruszone. Ale C (na szczęście) abstrahuje ten szczegół.

Ponieważ dwa wskaźniki mogą wskazywać na to samo miejsce w pamięci, może to spowodować złożony kod obsługujący możliwe kolizje.

Ten dodatkowy kod jest powolny i rani wydajność ponieważ wykonuje dodatkową pamięć operacje odczytu / zapisu, które są zarówno wolniejsze, jak i (być może) niepotrzebne.

Ścisła reguła aliasingu pozwala nam uniknąć nadmiarowego kodu maszynowego w przypadkach, w których powinno być bezpiecznie założyć, że dwa wskaźniki nie wskazują na ten sam blok pamięci (zobacz także słowo kluczowe restrict).

Ścisłe aliasowanie stanowi, że można bezpiecznie założyć, że wskaźniki do różnych typów wskazują na różne miejsca w pamięci.

Jeśli kompilator zauważy, że dwa wskaźniki wskazuje na różne typy (na przykład, An int * i a float *), zakłada, że adres pamięci jest inny i nie będzie chronił przed kolizjami adresów pamięci, co skutkuje szybszym kodem maszynowym.

Na przykład :

Przyjmijmy następującą funkcję:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

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, więc kod może skończyć się tak:

  1. Załaduj a i b z pamięci.

  2. Dodaj a do b.

  3. Zapisz b and reload a.

    (zapis z rejestru CPU do pamięci i załaduj z pamięci do rejestru CPU).

  4. Dodaj b do a.

  5. Zapisz a (z rejestru CPU) do pamięci.

Krok 3 jest bardzo powolny, ponieważ musi dostęp do fizycznej pamięci. Jest jednak wymagane, aby chronić przed instancjami, w których a i b wskazują ten sam adres pamięci.

Ścisłe aliasowanie pozwoli nam temu zapobiec, informując kompilator, że te adresy pamięci są wyraźnie różne (co w tym przypadku pozwoli na jeszcze dalszą optymalizację, której nie można wykonać, jeśli wskaźniki mają wspólny adres pamięci).

  1. Można to przekazać kompilatorowi na dwa sposoby, używając różnych typów do wskaż. tj.:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Używając słowa kluczowego restrict. tj.:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

Teraz, spełniając ścisłą regułę aliasingu, można uniknąć kroku 3 i Kod będzie działał znacznie szybciej.

W rzeczywistości, poprzez dodanie słowa kluczowego restrict, cała funkcja może być zoptymalizowana do:
  1. Załaduj a i b z pamięci.

  2. Dodaj a do b.

  3. Zapisz wynik zarówno do a, jak i do b.

Tej optymalizacji nie można było zrobić wcześniej, z powodu możliwej kolizji(gdzie a i b byłyby potrojone zamiast podwoić).

 9
Author: Myst,
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
2018-01-16 14:11:07

Ścisłe aliasowanie nie pozwala na stosowanie różnych typów wskaźników do tych samych danych.

Ten artykuł powinien pomóc ci zrozumieć problem w pełni szczegółowo.

 5
Author: Jason Dagit,
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
2013-11-04 13:38:55

Technicznie w C++, ścisła reguła aliasingu prawdopodobnie nigdy nie ma zastosowania.

Zwróć uwagę na definicję indrection (* operator):

Operator unary * wykonuje indrection: wyrażenie, do którego stosuje się wskaźnik do typu obiektu lub wskaźnik do typ funkcji i wynikiem jest lvalue odnoszący się do obiektu lub funkcja , na którą wskazuje wyrażenie .

Również z definicji glvalue

Glvalue jest wyrażeniem, którego ocena określa tożsamość obiekt, (...snip)

Tak więc w każdym dobrze zdefiniowanym śledzeniu programu, wartość GL odnosi się do obiektu. więc tzw. ścisła reguła aliasingu nigdy nie ma zastosowania. to może nie być to, co projektanci chcieli.

 -1
Author: curiousguy,
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
2018-07-09 03:24:02