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?
źródło
Odpowiedzi:
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:
zobaczy, że każdy U24 przechowuje swój 32-bitowy blok, ale tylko w najwyższym obszarze adresu.
To:
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.
źródło
struct b
aby przenieść sięelementC
przed dowolnym innym elementem, oznacza to, że nie jest zgodnym kompilatorem C. Przegrupowanie elementów jest niedozwolone w CPonieważ wspomniałeś
__attribute__((__packed__))
, zakładam, że twoim celem jest wyeliminowanie całego dopełniania w obrębiestruct
(aby każdy element miał wyrównanie 1-bajtowe).... 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ólstruct
bez 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:
... potrzebujemy dopełnienia dla wyrównanego dostępu do pamięci w stosunku do adresu struktury zawierającej te pola, tak jak poniżej:
... gdzie
.
wskazuje wypełnienie. Każdyx
musi 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
Foo
instancji):Skutecznie zburzyliśmy konstrukcję. W takim przypadku reprezentacja pamięci wygląda następująco:
... i to:
... 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.
źródło
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.
źródło
Bardzo popularną alternatywą jest „nazwane wypełnienie”:
To nie zakładamy, że struktura nie zostanie wypełniona do 8 bajtów.
źródło