Wpis z mikrobloga

Mam nadzięję, że dobrze spędziliście święta ( )
Dzisiaj mam dla was kolejną wskazówkę z serii #zloteradypassera w temacie języka C++.

Najczęściej piszę o różnych featurach języka, ale dzisiaj będzie o pewnym błędzie w implementacji kompilatorów, który jest na tyle powszechny, że możemy spokojnie go użyć do swoich potrzeb na wielu architekturach. Mowa o tzw memory alignment.

Jak zapewne wszyscy wiemy, każdy obiekt w C++ ma określony rozmiar który zajmuje w pamięci w zależności od typu. Czasami jest to określone bezpośrednio jak to, że char ma zawsze rozmiar 1, czasami jest to określone pośrednio jak to, że int ma rozmiar "przynajmniej 16bitów" chociaż dobrze wiemy, że w większości przypadków jest to rozmiar 32 bitów. Dla uproszczenia tego wpisu załóżmy. że int jest zawsze 32 bitowy i korzystamy z architektury amd64.

Wyboraźmy sobie zatem, że mamy dwie zmiennie, jedną typu char i drugą typu int. Jak łatwo policzyć pierwsza ma rozmiar 1 bajta, druga 4 bajty. Łatwo to sprawdzić korzystając z operatora sizeof.
Co się jednak stanie kiedy obie te zmienne umieścimy w jednej strukturze? Otóż można by przypuszczać, że rozmiar będzie wynosił 1 + 4 czyli 5. Otóż nic bardziej mylnego ( ͡º ͜ʖ͡º) Dzięki czemu coś jest znane jako struct padding/memory alignment rozmiar całej struktury będzie wynosił 8. Dlaczego tak się dzieje?

Otóż memory alignment oznacza wyrównianie pamięci, ale z moich obserwacji wynika że na wszystkich kompilatorach które przetestowałem (Clang, GCC, MSVC) jest to zbugowane! Żadna pamieć nie jest wyrównana! Obie zmienne są rozjechane w pamięci a pomiędzy nimi jest luka!
Na początku to odkrycie było dla mnie bardzo frustrujące, ale nauczony doświadczeniem postawiłem to wykorzystać ku swojej korzyści i okazuje się że w tej luce (z angielska nazywanej paddingiem) możemy przechowywać dodatkowe dane, nie zwiekszając rozmiaru całej struktury ( ͡º ͜ʖ͡º)

Przykładowy kod demonstruje tą technikę. Jak widać na standardowym wyjściu struktura ma rozmiar 8 bajtów, chociaż same zmienne typu char i int zajmują ledwie 5 tak jak policzyliśmy wcześniej. Mamy więc dodatkowe 3 bajty do wykorzystania.
W tym przypadku wykorzystuje bajt nr 3 i przypisuje mu wartość 5. Jest to mała wartość więc spokojnie się tam zmieści.
Następnie mogę to wykorzystać tak jak każdy inny obiekt typu int, w tym przypadku w wyrażeniu numerycznym.

Wykorzystywanie tego błedu kompilatorów może być szczególnie przydatne jeśli projektumy interfejsy/API, bo możemy przechowywać dowolne dane w lukach/dziurach, a użytkownik interfejsu nie będzie ich świadomy. Tak naprawdę bardzo mało programistów C++ (wliczają w to mnie) zagląda w te dziury.

Nie ma jednak róży bez kolców więc jako uczciwy mentor musze też wspomnieć o dwóch ważnych kwestiach abusowania tego buga:
- konstruktory kopiujące nie zawsze poprawnie kopiują dane w dziurach, więc do kopiowania takich obiektów trzeba użyć std::memcpy, ale jest to też pewnego rodzaju zaleta bo możemy sami wybrać czy kopiujemy obiekt z dziurami czy bez
- to jak duże są dane dziury i gdzie są rozmieszczone zależy od typów zmiennych i kolejności ich deklaracji w strukturze, z mojego wieloletniego doświadczenia wynika, że najwięcej miejsca możemy uzyskać jeśli będziemy deklarować zmienne od najmniejszej do największej i uważam, że jest to najlepsza programistyczna praktyka

Kończąc ten przydługawy wpis życzę wam miłego wsadzania danych w dziury i możecie dać w komentarzach co tam pochowaliście przed użytkownikami w swoich strukturach ( )

#programowanie #cpp #cplusplus
Passer93 - Mam nadzięję, że dobrze spędziliście święta (✌ ゚ ∀ ゚)
Dzisiaj mam dla was...

źródło: comment_1650787236o72Kmv50IWaFFngZckbGsF.jpg

Pobierz
  • 10
@Passer93: Ale to nie jest żaden błąd kompilatorów, a celowe działanie, bo domyślnie kompilator języka C optymalizuje kod programu pod kątem szybkości dostępu do pól struktury, a nie pod kątem miejsca przez nią zajmowanego. https://en.wikipedia.org/wiki/Data_structure_alignment

Co więcej standard języka C wprost mówi, że:

There may be unnamed padding at the end of a structure or union.
Defined - @Passer93: Ale to nie jest żaden błąd kompilatorów, a celowe działanie, bo ...

źródło: comment_1650790233H0pt5jtmtsH6vOqqbQcehy.jpg

Pobierz
via Wykop Mobilny (Android)
  • 0
@Passer93: z ciekawości próbowałeś bitfieldy?

Korzystam czesto przy strukturach

Wpisz swojego structa i dodaj:
char a : 8
int c : 24 // skomentuj tą linijke i powinienes zmienic rozmiar structury
int b : 32
@Defined: Nie chodzi nawet tylko o optymalizację, ale również o to, że wiele architektur wymaga aby wskaźniki do danych były wyrównane. Tzn. sprzęt może wręcz w ogóle nie obsługiwać operacji typu "weź skopiuj mi 64-bitowego longa z adresu niepodzielnego przez 8", lub może wymagać osobnej, specjalnej instrukcji do tego typu operacji. Np. w AVX są osobne instrukcje do operowania na wyrównanych i niewyrównanych danych , te do operowania na niewyrównanych są