Czy istnieje standardowy sposób lub standardowa alternatywa dla pakowania struktury w c?

13

Kiedy programowanie w CI uznało za nieocenione pakowanie struktur za pomocą __attribute__((__packed__))atrybutu GCC, dzięki czemu mogę łatwo przekonwertować ustrukturyzowaną część pamięci ulotnej na tablicę bajtów, która ma być przesłana przez magistralę, zapisana w pamięci lub zastosowana do bloku rejestrów. Spakowane struktury gwarantują, że traktowane jako tablica bajtów nie będą zawierać żadnych dopełnień, co jest zarówno marnotrawstwem, możliwym zagrożeniem dla bezpieczeństwa, jak i być może niekompatybilnym ze sprzętem interfejsowym.

Czy nie ma żadnego standardu pakowania struktur, który działałby we wszystkich kompilatorach języka C? Jeśli nie, to myślę, że jest to krytyczna cecha programowania systemów? Czy pierwsi użytkownicy języka C nie znaleźli potrzeby pakowania struktur lub czy istnieje jakaś alternatywa?

satur9nine
źródło
używanie struktur w domenach kompilacji jest bardzo złym pomysłem, w szczególności wskazywanie na sprzęt (który jest inną domeną kompilacji). struktury pakietów to tylko jedna sztuczka, aby to zrobić, mają wiele złych skutków ubocznych, więc istnieje wiele innych rozwiązań problemów z mniejszymi efektami ubocznymi, i które są bardziej przenośne.
old_timer

Odpowiedzi:

12

W struct liczy się przesunięcie każdego członka z adresu każdej instancji struct. Nie tyle kwestia tego, jak ciasno są zapakowane rzeczy.

Tablica ma jednak znaczenie, w jaki sposób jest „spakowana”. Zasadą w C jest to, że każdy element tablicy ma dokładnie N bajtów z poprzedniego, gdzie N jest liczbą bajtów używanych do przechowywania tego typu.

Ale przy strukturze nie ma takiej potrzeby jednolitości.

Oto jeden przykład dziwnego schematu pakowania:

Freescale (który produkuje mikrokontrolery samochodowe) tworzy mikro, które ma koprocesor Time Processing Unit (Google dla eTPU lub TPU). Ma dwa rodzime rozmiary danych, 8 bitów i 24 bity, i dotyczy tylko liczb całkowitych.

Ta struktura:

struct a
{
  U24 elementA;
  U24 elementB;
};

zobaczy, że każdy U24 przechowuje swój 32-bitowy blok, ale tylko w najwyższym obszarze adresu.

To:

struct b
{
  U24 elementA;
  U24 elementB;
  U8  elementC;
};

mają dwa U24s w sąsiednich blokach 32-bitowych, a U8 zostaną zapisane w „dziury” przed pierwszym U24, elementA.

Ale możesz powiedzieć kompilatorowi, aby spakował wszystko do własnego 32-bitowego bloku, jeśli chcesz; jest droższy w pamięci RAM, ale używa mniej instrukcji dostępu.

„pakowanie” nie oznacza „ciasno pakuj” - oznacza jedynie pewien schemat rozmieszczenia elementów struktury w przesunięciu.

Nie ma ogólnego schematu, jest on zależny od kompilatora + architektury.

RichColours
źródło
1
Jeśli kompilator TPU przestawi się, struct baby przenieść się elementCprzed dowolnym innym elementem, oznacza to, że nie jest zgodnym kompilatorem C. Przegrupowanie elementów jest niedozwolone w C
Bart van Ingen Schenau
Ciekawe, ale U24 nie jest standardowym typem C en.m.wikipedia.org/wiki/C_data_types, więc nie jest zaskakujące, że kompilator jest zmuszony traktować go w nieco dziwny sposób.
satur9nine
Dzieli pamięć RAM z głównym rdzeniem procesora, który ma rozmiar słowa 32 bity. Ale ten procesor ma jednostkę ALU, która obsługuje tylko 24 bity lub 8 bitów. Ma więc schemat układania liczb 24-bitowych w 32-bitowych słowach. Niestandardowy, ale świetny przykład pakowania i wyrównywania. Uzgodniony, jest bardzo niestandardowy.
RichColours,
6

Gdy programowanie w CI okazało się nieocenione pakowanie struktur za pomocą GCC __attribute__((__packed__))[...]

Ponieważ wspomniałeś __attribute__((__packed__)), zakładam, że twoim celem jest wyeliminowanie całego dopełniania w obrębie struct(aby każdy element miał wyrównanie 1-bajtowe).

Czy nie ma żadnego standardu pakowania struktur, który działałby we wszystkich kompilatorach języka C?

... a odpowiedź brzmi „nie”. Ważnym powodem jest wypełnienie i wyrównanie danych w stosunku do struktury (i ciągłe tablice struktur w stosie lub stercie). Na wielu komputerach niewyrównany dostęp do pamięci może prowadzić do potencjalnie znacznego obniżenia wydajności (choć staje się mniejszy na niektórych nowszych urządzeniach). W niektórych rzadkich przypadkach nieprawidłowy dostęp do pamięci prowadzi do błędu magistrali, którego nie można naprawić (może nawet spowodować awarię całego systemu operacyjnego).

Ponieważ standard C koncentruje się na przenośności, nie ma sensu mieć standardowego sposobu na wyeliminowanie wszystkich wypełnień w strukturze i po prostu zezwala na wyrównanie dowolnych pól, ponieważ może to spowodować ryzyko, że kod C nie będzie przenośny.

Najbezpieczniejszym i najbardziej przenośnym sposobem na wyprowadzenie takich danych do zewnętrznego źródła w sposób, który eliminuje wszelkie wypełnianie, jest serializacja do / ze strumieni bajtów zamiast tylko próby przesłania surowej zawartości pamięci structs. Zapobiegnie to również pogorszeniu wydajności programu poza tym kontekstem serializacji, a także pozwoli na swobodne dodawanie nowych pól structbez wyrzucania i zakłócania działania całego oprogramowania. Daje ci również trochę miejsca na walkę z endianizmem i takimi rzeczami, jeśli kiedykolwiek stanie się to problemem.

Jest jeden sposób na wyeliminowanie wszystkich dopełnień bez sięgania po dyrektywy specyficzne dla kompilatora, chociaż ma to zastosowanie tylko wtedy, gdy względna kolejność między polami nie ma znaczenia. Biorąc pod uwagę coś takiego:

struct Foo
{
    double x;  // assume 8-byte alignment
    char y;    // assume 1-byte alignment
               // 7 bytes of padding for first field
};

... potrzebujemy dopełnienia dla wyrównanego dostępu do pamięci w stosunku do adresu struktury zawierającej te pola, tak jak poniżej:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______y.......x_______y.......x_______y.......x_______y.......

... gdzie .wskazuje wypełnienie. Każdy xmusi wyrównać się do 8-bajtowej granicy wydajności (a czasem nawet prawidłowego zachowania).

Możesz wyeliminować dopełnienie w przenośny sposób, używając takiej reprezentacji SoA (struktura tablicy) (załóżmy, że potrzebujemy 8 Fooinstancji):

struct Foos
{
   double x[8];
   char y[8];
};

Skutecznie zburzyliśmy konstrukcję. W takim przypadku reprezentacja pamięci wygląda następująco:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______x_______x_______x_______x_______x_______x_______x_______

... i to:

01234567
yyyyyyyy

... koniec z dopełnianiem i bez angażowania nierównomiernego dostępu do pamięci, ponieważ nie uzyskujemy już dostępu do tych pól danych jako przesunięcie adresu struktury, ale zamiast tego jako przesunięcie adresu podstawowego dla tego, co faktycznie jest tablicą.

Ma to również tę zaletę, że jest szybsze w dostępie sekwencyjnym w wyniku zarówno mniejszej ilości danych do zużywania (koniec zbędnego uzupełniania w miksie, aby spowolnić odpowiednie zużycie danych przez maszynę), a także potencjał kompilatora wektoryzacji przetwarzania w bardzo trywialny sposób .

Minusem jest to, że jest to kod PITA. Jest również potencjalnie mniej wydajny w przypadku losowego dostępu, z większym krokiem między polami, gdzie często przedstawiciele AoS lub AoSoA radzą sobie lepiej. Ale to jeden ze standardowych sposobów na wyeliminowanie wyściółki i spakowanie rzeczy tak ciasno, jak to możliwe bez wkręcania z wyrównaniem wszystkiego.

ChrisF
źródło
2
Twierdzę, że wyraźne określenie układu struktury znacznie poprawiłoby przenośność. Podczas gdy niektóre układy prowadziłyby do bardzo wydajnego kodu na niektórych komputerach i bardzo nieefektywnego kodu na innych, kod działałby na wszystkich komputerach i byłby wydajny na co najmniej niektórych. Natomiast w przypadku braku takiej funkcji jedynym sposobem, aby kod działał na wszystkich komputerach, może być albo uczynienie go nieefektywnym na wszystkich komputerach, albo użycie zestawu makr i kompilacji warunkowej w celu połączenia szybkiej, nieprzenośnej aplikacji program i wolny przenośny z tego samego źródła.
supercat
Koncepcyjnie tak, jeśli moglibyśmy sprecyzować wszystko do reprezentacji bitów i bajtów, wymagań wyrównania, endianizmu itp. I mieć funkcję, która pozwala na tak wyraźną kontrolę w C, opcjonalnie oddzielając ją od podstawowej architektury ... Ale ja tylko mówiłem o ATM - obecnie najbardziej przenośnym rozwiązaniem dla serializatora jest zapisanie go w taki sposób, aby nie zależało od dokładnych reprezentacji bitów i bajtów oraz wyrównania typów danych. Niestety brakuje nam środków, aby bankomat mógł efektywnie zrobić inaczej (w C).
5

Nie wszystkie architektury są takie same, po prostu włącz opcję 32-bitową w jednym module i zobacz, co się stanie, gdy użyjesz tego samego kodu źródłowego i tego samego kompilatora. Kolejność bajtów jest kolejnym znanym ograniczeniem. Rzut reprezentacji zmiennoprzecinkowej i problemy się pogarszają. Przesyłanie danych binarnych za pomocą funkcji pakowania jest nieprzenośne. Aby ustandaryzować go tak, aby był praktycznie użyteczny, należy ponownie zdefiniować specyfikację języka C.

Chociaż powszechne jest używanie Pack do wysyłania danych binarnych, to zły pomysł, jeśli chcesz bezpieczeństwa, przenośności lub długowieczności danych. Jak często czytasz binarny obiekt blob ze źródła do swojego programu. Jak często sprawdzasz, czy wszystkie wartości są rozsądne, czy haker lub zmiana programu nie „dotarła” do danych? Zanim zakodujesz procedurę sprawdzania, równie dobrze możesz używać procedur importowania i eksportowania.

mattnz
źródło
0

Bardzo popularną alternatywą jest „nazwane wypełnienie”:

struct s {
  short s1;
  char  c2;
  char  reserved; // Padding
};

To nie zakładamy, że struktura nie zostanie wypełniona do 8 bajtów.

MSalters
źródło