Szukaj…


Uwagi

Naruszenie zasad aliasingu i naruszenia skutecznego typu obiektu to dwie różne rzeczy i nie należy ich mylić.

  • Aliasing jest właściwością dwóch wskaźników a i b które odnoszą się do tego samego obiektu, to znaczy, że a == b .

  • Efektywny typ obiektu danych jest używany przez C do określenia, jakie operacje można wykonać na tym obiekcie. W szczególności typ skuteczny służy do ustalenia, czy dwa wskaźniki mogą się aliasować.

Aliasing może być problemem dla optymalizacji, ponieważ zmiana przedmiotu przez jeden wskaźnik, a powiedzieć, można zmienić obiekt, który jest widoczny przez inny wskaźnik, b . Gdyby twój kompilator C musiał założyć, że wskaźniki zawsze mogą się aliasować, niezależnie od ich rodzaju i pochodzenia, wiele możliwości optymalizacji zostałoby utraconych, a wiele programów działałoby wolniej.

Surowe reguły aliasingu C odnoszą się do przypadków w kompilatorze, które mogą zakładać, które obiekty aliują się (lub nie). Istnieją dwie ogólne zasady, o których zawsze należy pamiętać w przypadku wskaźników danych.

O ile nie podano inaczej, dwa wskaźniki o tym samym typie bazowym mogą mieć alias.

Dwa wskaźniki o innym typie podstawowym nie mogą być aliasami, chyba że co najmniej jeden z dwóch typów jest typem znaków.

Tutaj typ podstawowy oznacza, że odkładamy na bok kwalifikacje typu, takie jak const , np. Jeśli a jest double* a b jest const double* , kompilator musi ogólnie zakładać, że zmiana *a może zmienić *b .

Naruszenie drugiej zasady może mieć katastrofalne skutki. Tutaj naruszono surowe środki reguły aliasing, który przedstawi dwa wskaźniki i a b różnego typu do kompilatora, który w punkcie rzeczywistość do tego samego obiektu. Kompilator może wtedy zawsze założyć, że oba wskazują na różne obiekty i nie zaktualizuje swojego pomysłu *b jeśli zmienisz obiekt za pomocą *a .

Jeśli to zrobisz, zachowanie twojego programu stanie się niezdefiniowane. Dlatego C nakłada dość surowe ograniczenia na konwersje wskaźników, aby pomóc ci uniknąć takiej sytuacji, która mogłaby wystąpić przypadkowo.

O ile typ źródłowy lub docelowy nie jest void , wszystkie konwersje wskaźników między wskaźnikami o innym typie podstawowym muszą być jawne .

Innymi słowy, potrzebują rzutowania , chyba że wykonasz konwersję, która po prostu doda kwalifikator taki jak const do typu docelowego.

Ogólnie rzecz biorąc, unikanie konwersji wskaźnika, a zwłaszcza rzutów, chroni przed problemami z aliasingiem. Chyba że naprawdę ich potrzebujesz, a te przypadki są bardzo wyjątkowe, powinieneś ich unikać, jak możesz.

Typy postaci nie są dostępne za pośrednictwem typów innych niż postacie.

Jeśli obiekt jest zdefiniowany z czasem przechowywania statycznym, wątkowym lub automatycznym i ma typ char : char , unsigned char lub signed char , może nie być dostępny dla typu innego niż znak. W poniższym przykładzie tablica char jest ponownie interpretowana jako typ int , a zachowanie jest niezdefiniowane przy każdym dereferencji wskaźnika int b .

int main( void )
{
    char a[100];
    int* b = ( int* )&a;
    *b = 1;      

    static char c[100];
    b = ( int* )&c;
    *b = 2;

    _Thread_local char d[100];
    b = ( int* )&d;
    *b = 3;
}

Jest to niezdefiniowane, ponieważ narusza zasadę „typu efektywnego”, żaden obiekt danych o typie skutecznym nie może być dostępny za pośrednictwem innego typu, który nie jest typem znaku. Ponieważ drugim typem jest int , nie jest to dozwolone.

Nawet jeśli wiadomo, że pasują do siebie rozmiary wyrównania i wskaźnika, nie byłoby to wyłączone z tej reguły, zachowanie byłoby nadal niezdefiniowane.

Oznacza to w szczególności, że w standardzie C nie ma możliwości zarezerwowania obiektu bufora typu znakowego, którego można użyć za pomocą wskaźników o różnych typach, ponieważ można użyć bufora otrzymanego przez malloc lub podobną funkcję.

Prawidłowym sposobem osiągnięcia tego samego celu, co w powyższym przykładzie, byłoby użycie union .

typedef union bufType bufType;
union bufType {
   char c[sizeof(int[25])];
   int i[25];
};

int main( void )
{
    bufType a = { .c = { 0 } }; // reserve a buffer and initialize
    int* b = a.i;      // no cast necessary
    *b = 1;      

    static bufType a = { .c = { 0 } };
    int* b = a.i;
    *b = 2;

    _Thread_local bufType a = { .c = { 0 } };
    int* b = a.i;
    *b = 3;
}

Tutaj union zapewnia, że kompilator od samego początku wie, że do bufora można uzyskać dostęp za pośrednictwem różnych widoków. Ma to również tę zaletę, że teraz bufor ma „widok” ai który jest już typu int i nie jest wymagana konwersja wskaźnika.

Skuteczny typ

Efektywny typ obiektu danych to informacja o ostatnim typie, która była z nim powiązana, jeśli taka istnieje.

// a normal variable, effective type uint32_t, and this type never changes
uint32_t a = 0.0;

// effective type of *pa is uint32_t, too, simply
// because *pa is the object a
uint32_t* pa = &a;

// the object pointed to by q has no effective type, yet
void* q = malloc(sizeof uint32_t);
// the object pointed to by q still has no effective type,
// because nobody has written to it
uint32_t* qb = q;
// *qb now has effective type uint32_t because a uint32_t value was written
*qb = 37;

// the object pointed to by r has no effective type, yet, although
// it is initialized
void* r = calloc(1, sizeof uint32_t);
// the object pointed to by r still has no effective type,
// because nobody has written to or read from it
uint32_t* rc = r;
// *rc now has effective type uint32_t because a value is read
// from it with that type. The read operation is valid because we used calloc.
// Now the object pointed to by r (which is the same as *rc) has
// gained an effective type, although we didn't change its value.
uint32_t c = *rc;

// the object pointed to by s has no effective type, yet.
void* s = malloc(sizeof uint32_t);
// the object pointed to by s now has effective type uint32_t
// because an uint32_t value is copied into it.
memcpy(s, r, sizeof uint32_t);

Zauważ, że w przypadku tego ostatniego nie było konieczne, abyśmy mieli nawet wskaźnik uint32_t* do tego obiektu. Fakt, że skopiowaliśmy kolejny obiekt uint32_t jest wystarczający.

Naruszenie surowych zasad aliasingu

W poniższym kodzie przyjmijmy dla uproszczenia, że float i uint32_t mają ten sam rozmiar.

void fun(uint32_t* u, float* f) {
    float a = *f
    *u = 22;
    float b = *f;
    print("%g should equal %g\n", a, b);
}

u i f mają różne typy bazowe, a zatem kompilator może założyć, że wskazują one na różne obiekty. Nie ma możliwości, *f mógł zmienić między dwoma inicjalizacji z a i b , a więc kompilator może zoptymalizować kod na coś odpowiednik

void fun(uint32_t* u, float* f) {
    float a = *f
    *u = 22;
    print("%g should equal %g\n", a, a);
}

Oznacza to, że druga operacja obciążenia *f może być całkowicie zoptymalizowana.

Jeśli nazwiemy tę funkcję „normalnie”

 float fval = 4;
 uint32_t uval = 77;
 fun(&uval, &fval);

wszystko idzie dobrze i coś w tym rodzaju

4 powinno być równe 4

jest drukowane. Ale jeśli oszukujemy i przekazujemy ten sam wskaźnik, po konwersji,

 float fval = 4;
 uint32_t* up = (uint32_t*)&fval;
 fun(up, &fval);

naruszamy surową zasadę aliasingu. Wtedy zachowanie staje się niezdefiniowane. Dane wyjściowe mogą być takie, jak powyżej, gdyby kompilator zoptymalizował drugi dostęp lub coś zupełnie innego, a więc twój program stanie się całkowicie niewiarygodny.

ograniczyć kwalifikacje

Jeśli mamy dwa argumenty wskaźnika tego samego typu, kompilator nie może przyjąć żadnego założenia i zawsze będzie musiał założyć, że zmiana na *e może zmienić *f :

void fun(float* e, float* f) {
    float a = *f
    *e = 22;
    float b = *f;
    print("is %g equal to %g?\n", a, b);
}

float fval = 4;
float eval = 77;
 fun(&eval, &fval);

wszystko idzie dobrze i coś w tym rodzaju

jest 4 równa się 4?

jest drukowane. Jeśli miniemy ten sam wskaźnik, program nadal zrobi właściwą rzecz i wydrukuje

jest 4 równa 22?

Może się to okazać nieefektywne, jeśli na podstawie informacji zewnętrznych wiemy, że e i f nigdy nie będą wskazywać na ten sam obiekt danych. Możemy odzwierciedlić tę wiedzę, dodając kwalifikatory restrict do parametrów wskaźnika:

void fan(float*restrict e, float*restrict f) {
    float a = *f
    *e = 22;
    float b = *f;
    print("is %g equal to %g?\n", a, b);
}

Wtedy kompilator może zawsze zakładać, że e i f wskazują na różne obiekty.

Zmiana bajtów

Gdy obiekt ma efektywny typ, nie powinieneś próbować go modyfikować za pomocą wskaźnika innego typu, chyba że ten inny typ jest typem char , char , znakiem signed char lub unsigned char .

#include <inttypes.h>
#include <stdio.h>

int main(void) {
  uint32_t a = 57;
  // conversion from incompatible types needs a cast !
  unsigned char* ap = (unsigned char*)&a;
  for (size_t i = 0; i < sizeof a; ++i) {
    /* set each byte of a to 42 */
    ap[i] = 42;
  }
  printf("a now has value %" PRIu32 "\n", a);
}

To jest prawidłowy program, który drukuje

a ma teraz wartość 707406378

Działa to, ponieważ:

  • Dostęp jest uzyskiwany do poszczególnych bajtów widzianych za pomocą typu unsigned char więc każda modyfikacja jest dobrze zdefiniowana.
  • Oba widoki na obiekcie, poprzez i poprzez a *ap , alias, ale ponieważ ap jest wskaźnikiem do typu postaci, ścisłe reguły aliasing nie ma zastosowania. Dlatego kompilator musi założyć, że wartość a mogła zostać zmieniona w pętli for . Zmodyfikowana wartość a musi być zbudowana z bajtów, które zostały zmienione.
  • Typ a , uint32_t nie ma bitów wypełniających. Wszystkie bity reprezentacji liczą się dla wartości, tutaj 707406378 , i nie może być reprezentacji pułapki.


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow