Kontekst
Przenosimy kod C, który został pierwotnie skompilowany przy użyciu 8-bitowego kompilatora C dla mikrokontrolera PIC. Typowy idiom, który był używany, aby zapobiec cofaniu się globalnych zmiennych bez znaku (na przykład liczników błędów), jest następujący:
if(~counter) counter++;
Operator bitowy odwraca tutaj wszystkie bity, a instrukcja jest prawdziwa tylko wtedy, gdy counter
jest mniejsza niż wartość maksymalna. Co ważne, działa to niezależnie od wielkości zmiennej.
Problem
Obecnie celujemy w 32-bitowy procesor ARM za pomocą GCC. Zauważyliśmy, że ten sam kod daje różne wyniki. O ile możemy stwierdzić, wygląda na to, że bitowe uzupełnienie zwraca wartość o innym rozmiarze niż się spodziewalibyśmy. Aby to odtworzyć, kompilujemy w GCC:
uint8_t i = 0;
int sz;
sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1
sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4
W pierwszym wierszu danych wyjściowych otrzymujemy to, czego byśmy oczekiwali: i
1 bajt. Jednak bitowe uzupełnienie i
to w rzeczywistości cztery bajty, co powoduje problem, ponieważ porównania z tym teraz nie przyniosą oczekiwanych rezultatów. Na przykład, jeśli robisz (gdzie i
jest poprawnie zainicjowany uint8_t
):
if(~i) i++;
Zobaczymy i
„zawinięcie” z 0xFF z powrotem do 0x00. To zachowanie jest inne w GCC w porównaniu do tego, kiedy działało tak, jak zamierzaliśmy w poprzednim kompilatorze i 8-bitowym mikrokontrolerze PIC.
Wiemy, że możemy rozwiązać ten problem, przesyłając w ten sposób:
if((uint8_t)~i) i++;
Lub przez
if(i < 0xFF) i++;
Jednak w obu tych obejściach wielkość zmiennej musi być znana i podatna na błędy dla twórców oprogramowania. Tego rodzaju kontrole górnych granic występują w całej bazie kodu. Istnieje wiele zmiennych rozmiarach (np. uint16_t
I unsigned char
itd.) I zmieniając je w kodzie inaczej roboczej nie jest coś czekamy na.
Pytanie
Czy nasze rozumienie problemu jest prawidłowe i czy są dostępne opcje rozwiązania tego problemu, które nie wymagają ponownego odwiedzania każdego przypadku, w którym użyliśmy tego idiomu? Czy nasze założenie jest prawidłowe, że operacja taka jak bitowe uzupełnienie powinna zwrócić wynik o takim samym rozmiarze jak operand? Wygląda na to, że ulegnie to awarii, w zależności od architektury procesorów. Czuję, że biorę szalone pigułki i że C powinno być trochę bardziej przenośne niż to. Ponownie nasze rozumienie tego może być błędne.
Z pozoru może to nie wydawać się wielkim problemem, ale ten wcześniej działający idiom jest używany w setkach lokalizacji i chętnie to rozumiemy, zanim przejdziemy do drogich zmian.
Uwaga: Wydaje się, że na pozór podobne, ale nie do końca zduplikowane pytanie: Bitowa operacja na char daje 32-bitowy wynik
Nie widziałem prawdziwego sedna omawianego problemu, a mianowicie, że rozmiar wyniku bitowego uzupełnienia różni się od tego, co przekazano operatorowi.
źródło
Odpowiedzi:
To, co widzisz, jest wynikiem promocji liczb całkowitych . W większości przypadków, gdy w wyrażeniu używana jest wartość całkowita, jeśli typ wartości jest mniejszy niż
int
promowana jest wartośćint
. Jest to udokumentowane w sekcji 6.3.1.1p2 normy C :Więc jeśli zmienna ma typ
uint8_t
i wartość 255, użycie dowolnego operatora innego niż rzutowanie lub przypisanie spowoduje najpierw konwersję na typint
o wartości 255 przed wykonaniem operacji. Dlategosizeof(~i)
daje 4 zamiast 1.Sekcja 6.5.3.3 opisuje, że promocje na liczby całkowite dotyczą
~
operatora:Więc zakładając 32 bit
int
, jeślicounter
ma wartość 8 bitów0xff
, jest konwertowany na wartość 32 bitów0x000000ff
, a zastosowanie~
do niego daje ci0xffffff00
.Prawdopodobnie najprostszym sposobem na poradzenie sobie z tym bez konieczności znajomości typu jest sprawdzenie, czy po zwiększeniu wartość wynosi 0, a jeśli tak, to zmniejsz.
Zawijanie liczb całkowitych bez znaku działa w obu kierunkach, więc zmniejszenie wartości 0 daje największą wartość dodatnią.
źródło
if (!++counter) --counter;
może być mniej dziwny dla niektórych programistów niż używanie przecinka.++counter; counter -= !counter;
.increment_unsigned_without_wraparound
lubincrement_with_saturation
. Osobiście użyłbym ogólnejclamp
funkcji trójoperandowej .w rozmiarze (i); żądasz wielkości zmiennej i , więc 1
w sizeof (~ i); poprosić o wielkości od rodzaju ekspresji, która jest int , w przypadku 4
Używać
wiedzieć, czy ja nie cenią 255 (w przypadku z AN uint8_t) nie jest bardzo czytelny, zrób
i będziesz mieć przenośny i czytelny kod
Aby zarządzać dowolnym znakiem niepodpisanym:
Wyrażenie jest stałe, więc obliczane w czasie kompilacji.
#include <limits.h> dla CHAR_BIT i #include <stdint.h> dla uintmax_t
źródło
!= 255
jest niewystarczające.unsigned
obiektów, ponieważ przesunięcia pełnej szerokości obiektu nie są zdefiniowane przez standard C, ale można to naprawić za pomocą(2u << sizeof(i)*CHAR_BIT-1) - 1
.((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1
.Oto kilka opcji implementacji „Dodaj 1,
x
ale ogranicz do maksymalnej reprezentatywnej wartości”, biorąc pod uwagę, żex
jest to pewien typ liczb całkowitych bez znaku:Dodaj jedną, jeśli i tylko jeśli
x
jest mniejsza niż maksymalna wartość reprezentowana w jej typie:Definicja: patrz następująca pozycja
Maximum
. Ta metoda ma dużą szansę na zoptymalizowanie przez kompilator pod kątem wydajnych instrukcji, takich jak porównanie, jakaś forma warunkowego zestawu lub przeniesienia oraz dodanie.Porównaj z największą wartością tego typu:
(Oblicza to 2 N , gdzie N jest liczbą bitów w środku
x
, poprzez przesunięcie 2 o N −1 bitów. Robimy to zamiast przesunięcia 1 N bitów, ponieważ przesunięcie o liczbę bitów w typie nie jest zdefiniowane przez C standard.CHAR_BIT
Makro może być dla niektórych nieznane; jest to liczba bitów w bajcie, podobniesizeof x * CHAR_BIT
jak liczba bitów w rodzajux
.)Można to owinąć w makro zgodnie z potrzebami pod względem estetyki i przejrzystości:
Zwiększ
x
i popraw, jeśli zawija się do zera, używającif
:Zwiększ
x
i popraw, jeśli zawija się do zera, używając wyrażenia:Jest to nominalnie bezgałęziowe (czasem korzystne dla wydajności), ale kompilator może zaimplementować go tak samo jak powyżej, używając w razie potrzeby rozgałęzienia, ale być może z bezwarunkowymi instrukcjami, jeśli architektura docelowa ma odpowiednie instrukcje.
Opcja bez rozgałęzienia, wykorzystująca powyższe makro, to:
Jeśli
x
jest to maksimum tego typu, wartość ta jest obliczana nax += 1-1
. W przeciwnym razie tak jestx += 1-0
. Jednak podział na wiele architektur jest nieco powolny. Kompilator może zoptymalizować to do instrukcji bez podziału, w zależności od kompilatora i architektury docelowej.źródło
-Wshift-op-parentheses
. Dobrą wiadomością jest to, że optymalizujący kompilator nie wygeneruje tutaj podziału, więc nie musisz się martwić, że będzie powolny.sizeof x
nie można zaimplementować w funkcji C, ponieważx
musiałby to być parametr (lub inne wyrażenie) z jakimś stałym typem. Nie można wygenerować rozmiaru dowolnego typu argumentu używanego przez program wywołujący. Makro może.Przed wersją stdint.h zmienne rozmiary mogą się różnić w zależności od kompilatora, a rzeczywiste typy zmiennych w C są nadal int, długie itp. I są nadal definiowane przez autora kompilatora co do ich wielkości. Brak niektórych założeń standardowych lub docelowych. Autor (autorzy) muszą następnie utworzyć plik stdint.h, aby zmapować dwa światy, to jest cel pliku stdint.h, aby zmapować uint_this na int, długi, krótki.
Jeśli przenosisz kod z innego kompilatora i używa on znaków char, short, int, long, musisz przejść przez każdy typ i zrobić port sam, nie ma możliwości obejścia tego. I albo otrzymujesz odpowiedni rozmiar zmiennej, deklaracja się zmienia, ale kod jak napisany działa ...
lub ... podaj maskę lub rzut bezpośrednio
Na koniec dnia, jeśli chcesz, aby ten kod działał, musisz przenieść go na nową platformę. Twój wybór jak. Tak, musisz poświęcić czas na trafienie w każdą sprawę i zrobić to dobrze, w przeciwnym razie będziesz wracał do tego kodu, który jest jeszcze droższy.
Jeśli wyodrębnisz typy zmiennych w kodzie przed przeniesieniem i jaki jest rozmiar typów zmiennych, to wyodrębnij zmienne, które to robią (powinny być łatwe do grepowania) i zmień ich deklaracje za pomocą definicji stdint.h, które, mam nadzieję, nie zmienią się w przyszłości, i byłbyś zaskoczony, ale czasami używane są niewłaściwe nagłówki, więc nawet czeków, abyś mógł lepiej spać w nocy
I chociaż ten styl kodowania działa (if (~ counter) counter ++;), dla przenośności teraz i w przyszłości najlepiej jest użyć maski, aby konkretnie ograniczyć rozmiar (i nie polegać na deklaracji), rób to, gdy kod jest zapisywany w pierwszej kolejności lub po prostu dokończ port, a później nie będziesz musiał go ponownie portować. Lub, aby kod był bardziej czytelny, zrób to, jeśli x <0xFF wtedy lub x! = 0xFF lub coś takiego, to kompilator może zoptymalizować go do tego samego kodu, który byłby dla każdego z tych rozwiązań, po prostu czyni go bardziej czytelnym i mniej ryzykownym ...
Zależy od tego, jak ważny jest produkt lub od tego, ile razy chcesz wysyłać łatki / aktualizacje, toczyć ciężarówkę lub iść do laboratorium, aby ustalić, czy próbujesz szybko znaleźć rozwiązanie, czy po prostu dotknąć dotkniętych linii kodu. jeśli jest tylko sto lub kilka, to nie jest tak duży port.
źródło
C 2011 Online Draft
Problem polega na tym, że operand
~
jest promowanyint
przed zastosowaniem operatora.Niestety nie sądzę, aby można było z tego łatwo wyjść. Pisanie
nie pomoże, ponieważ obowiązują tam również promocje. Jedyne, co mogę zasugerować, to utworzenie stałych symbolicznych dla maksymalnej wartości, którą ten obiekt ma reprezentować, i przetestowanie na tym:
źródło
-1
nie jest on potrzebny, ponieważ spowodowałoby to, że licznik osiągnąłby wartość 254 (0xFE). W każdym razie takie podejście, jak wspomniano w moim pytaniu, nie jest idealne ze względu na różne wielkości zmiennych w bazie kodu, które uczestniczą w tym idiomie.