Dlaczego w przypadku kodu osadzonego powinienem używać typów „uint_t” zamiast „unsigned int”?

22

Piszę aplikację c dla STM32F105, używając gcc.

W przeszłości (z prostszych projektów), zawsze zdefiniowane jako zmienne char, int, unsigned int, i tak dalej.

Widzę, że to jest wspólne korzystanie z typów zdefiniowanych w stdint.h, takie jak int8_t, uint8_t, uint32_t, itd. To prawda, że w wielokrotnością API, które używam, a także w bibliotece ARM CMSIS od ST.

Wierzę, że rozumiem, dlaczego powinniśmy to robić; aby umożliwić kompilatorowi lepszą optymalizację przestrzeni pamięci. Spodziewam się, że mogą istnieć dodatkowe powody.

Jednak ze względu na zasady promocji liczb całkowitych c, ciągle napotykam ostrzeżenia o konwersji za każdym razem, gdy próbuję dodać dwie wartości, wykonać operację bitową itp. Ostrzeżenie brzmi mniej więcej tak conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]. Zagadnienie to jest omawiane tutaj i tutaj .

Nie dzieje się tak przy użyciu zmiennych zadeklarowanych jako intlub unsigned int.

Podając kilka przykładów, biorąc pod uwagę:

uint16_t value16;
uint8_t value8;

Musiałbym to zmienić:

value16 <<= 8;
value8 += 2;

do tego:

value16 = (uint16_t)(value16 << 8);
value8 = (uint8_t)(value8 + 2);

To brzydkie, ale mogę to zrobić w razie potrzeby. Oto moje pytania:

  1. Czy istnieje przypadek, w którym konwersja z niepodpisanego na podpisany i z powrotem na niepodpisany spowoduje niepoprawny wynik?

  2. Czy są jakieś inne ważne powody, dla których warto używać typów całkowitych stdint.h?

W oparciu o odpowiedzi, które otrzymuję, wygląda na to, że typy stdint.h są ogólnie preferowane, nawet jeśli c konwertuje uintdo intiz powrotem. To prowadzi do większego pytania:

  1. Mogę zapobiec ostrzeżeniom kompilatora, używając rzutowania czcionek (np value16 = (uint16_t)(value16 << 8);.). Czy po prostu ukrywam problem? Czy jest lepszy sposób, aby to zrobić?
bitsmack
źródło
Użyj niepodpisanych literałów: IE 8ui 2u.
Stop Harming Monica,
Dzięki, @OrangeDog, chyba nie rozumiem. Próbowałem obu value8 += 2u;i value8 = value8 + 2u;, ale dostaję te same ostrzeżenia.
bitsmack,
Użyj ich mimo to, aby uniknąć podpisanych ostrzeżeń, gdy jeszcze nie masz ostrzeżeń o szerokości :)
Stop Harming Monica

Odpowiedzi:

11

Kompilator zgodny ze standardami, gdzie intbył od 17 do 32 bitów, może legalnie zrobić wszystko, co chce, używając następującego kodu:

uint16_t x = 46341;
uint32_t y = x*x; // temp result is signed int, which can't hold 2147488281

Implementacja, która tego chciała, mogłaby zgodnie z prawem wygenerować program, który nie zrobiłby nic oprócz wielokrotnego wypisywania ciągu „Fred” na każdym porcie portu za pomocą każdego możliwego protokołu. Prawdopodobieństwo przeniesienia programu do implementacji, która zrobiłaby coś takiego, jest wyjątkowo niskie, ale teoretycznie jest to możliwe. Jeśli chciałbyś napisać powyższy kod, aby zagwarantować, że nie będziesz się angażować w Niezdefiniowane Zachowanie, konieczne będzie napisanie tego ostatniego wyrażenia jako (uint32_t)x*xlub 1u*x*x. W kompilatorze, który intma od 17 do 31 bitów, to ostatnie wyrażenie odciąłoby górne bity, ale nie zaangażowałoby się w niezdefiniowane zachowanie.

Myślę, że ostrzeżenia gcc prawdopodobnie próbują sugerować, że napisany kod nie jest w pełni w 100% przenośny. Są chwile, kiedy naprawdę należy napisać kod, aby uniknąć zachowań, które byłyby niezdefiniowane w niektórych implementacjach, ale w wielu innych przypadkach należy po prostu stwierdzić, że jest mało prawdopodobne, aby kod został użyty w implementacjach, które powodowałyby zbyt irytujące rzeczy.

Zauważ, że użycie typów takich jak inti shortmoże wyeliminować niektóre ostrzeżenia i rozwiązać niektóre problemy, ale prawdopodobnie spowodują inne. Interakcje między typami podobnymi uint16_ta regułami promocji liczb całkowitych C są trudne, ale takie typy są prawdopodobnie lepsze niż jakakolwiek alternatywa.

supercat
źródło
6

1) Jeśli po prostu rzutujesz z liczby całkowitej bez znaku na liczbę całkowitą o tej samej długości tam iz powrotem, bez żadnych operacji pomiędzy, otrzymasz ten sam wynik za każdym razem, więc nie ma problemu. Ale różne operacje logiczne i arytmetyczne działają inaczej na operandach podpisanych i niepodpisanych.
2) Głównym powodem do użytku stdint.htypów jest to, że rozmiar nieco takich typu A są zdefiniowane i równa na wszystkich platformach, co nie jest prawdą dla int, longetc, a także charma żadnego standardu signess, to może być podpisane lub podpisane przez domyślna. Ułatwia manipulowanie danymi, znając dokładny rozmiar, bez dodatkowych kontroli i założeń.

Eugene Sh.
źródło
2
Rozmiary int32_ti uint32_tsą równe na wszystkich platformach, na których są zdefiniowane . Jeśli procesor nie ma dokładnie pasującego typu sprzętu, typy te nie są zdefiniowane. Stąd przewaga intitp., A być może int_least32_titd.
Pete Becker
1
@ PeteteBecker - jest to jednak zapewne zaleta, ponieważ powstałe błędy kompilacji natychmiast uświadamiają ci problem. Wolę to, niż moje typy zmieniają na mnie rozmiar.
sapi
@sapi - w wielu sytuacjach podstawowy rozmiar nie ma znaczenia; Programiści C dobrze sobie radzili bez ustalonych rozmiarów przez wiele lat.
Pete Becker
6

Ponieważ numer 2 Eugene'a jest prawdopodobnie najważniejszym punktem, chciałbym tylko dodać, że jest to wskazówka

MISRA (directive 4.6): "typedefs that indicate size and signedness should be used in place of the basic types".

Również Jack Ganssle wydaje się być zwolennikiem tej reguły: http://www.ganssle.com/tem/tem265.html

Tom L.
źródło
2
Szkoda, że ​​nie ma żadnych typów dla określenia „N-bitowa liczba całkowita bez znaku, która może być bezpiecznie pomnożona przez inną liczbę całkowitą tego samego rozmiaru, aby dać wynik o tym samym rozmiarze”. Reguły promocji liczb całkowitych współdziałają okropnie z istniejącymi typami, takimi jak uint32_t.
supercat
3

Prostym sposobem na wyeliminowanie ostrzeżeń jest uniknięcie użycia opcji -Wconversion w GCC. Myślę, że musisz włączyć tę opcję ręcznie, ale jeśli nie, możesz użyć -Wno-konwersji, aby ją wyłączyć. Możesz włączyć ostrzeżenia dla konwersji precyzyjnych znaków i FP za pomocą innych opcji , jeśli nadal chcesz.

Ostrzeżenia -Wconversion są prawie zawsze fałszywie pozytywne, dlatego prawdopodobnie nawet -Wextra domyślnie go nie włącza. Przepełnienie stosu pytanie ma wiele sugestii dotyczących dobrych zestawów opcji. Z własnego doświadczenia wynika, że ​​jest to dobre miejsce na rozpoczęcie:

-std = c99 -pedtyk -Wall -Wextra -Wshadow

Dodaj więcej, jeśli ich potrzebujesz, ale szanse na to, że nie będziesz.

Jeśli musisz zachować -Wconversion, możesz nieco skrócić swój kod, rzutując tylko na operand numeryczny:

value16 <<= (uint16_t)8;
value8 += (uint8_t)2;

Nie jest to jednak łatwe do odczytania bez podświetlania składni.

Adam Haun
źródło
2

w każdym projekcie oprogramowania bardzo ważne jest stosowanie przenośnych definicji typów. (nawet kolejna wersja tego samego kompilatora wymaga tego.) Dobry przykład, kilka lat temu pracowałem nad projektem, w którym obecny kompilator zdefiniował „int” jako 8 bitów. Następna wersja kompilatora zdefiniowała „int” jako 16 bitów. Ponieważ nie użyliśmy żadnych przenośnych definicji „int”, ram (skutecznie) podwoił rozmiar i wiele sekwencji kodu zależnych od 8-bitowej int nie powiodło się. Zastosowanie definicji typu przenośnego pozwoliłoby uniknąć tego problemu (setki roboczogodzin na naprawę).

Richard Williams
źródło
Nie należy używać rozsądnego kodu intw odniesieniu do typu 8-bitowego. Nawet jeśli robi to kompilator inny niż C, taki jak CCS, rozsądny kod powinien używać jednego charlub typu piszącego na maszynie dla 8 bitów, a typu piszącego na maszynie (nie „długiego”) dla 16 bitów. Z drugiej strony, przenoszenie kodu z czegoś takiego jak CCS do prawdziwego kompilatora może być problematyczne, nawet jeśli używa odpowiednich typów czcionek, ponieważ takie kompilatory są często „niezwykłe” pod innymi względami.
supercat
1
  1. Tak. N-bitowa liczba całkowita ze znakiem może reprezentować mniej więcej połowę liczby nieujemnych liczb całkowitych jako n-bitowa liczba całkowita bez znaku, a poleganie na charakterystyce przepełnienia jest niezdefiniowanym zachowaniem, więc wszystko może się zdarzyć. Zdecydowana większość obecnych i przeszłych procesorów używa podwójnych uzupełnień, więc wiele operacji robi to samo na podpisanych i niepodpisanych typach całkowych, ale nawet wtedy nie wszystkie operacje dadzą identyczne bitowo wyniki. Naprawdę prosisz o dodatkowe problemy później, kiedy nie możesz dowiedzieć się, dlaczego twój kod nie działa zgodnie z przeznaczeniem.

  2. Chociaż int i niepodpisane mają zdefiniowane rozmiary implementacji, często są one wybierane „inteligentnie” przez implementację ze względu na rozmiar lub szybkość. Generalnie trzymam się tych, chyba że mam dobry powód, aby zrobić inaczej. Podobnie, rozważając, czy użyć int czy niepodpisany, ogólnie wolę int, chyba że mam dobry powód, aby zrobić inaczej.

W przypadkach, w których naprawdę potrzebuję lepszej kontroli nad rozmiarem lub podpisem typu, zwykle wolę albo użyć zdefiniowanego przez system typedef (size_t, intmax_t itp.), Albo stworzyć własny typedef, który wskazuje funkcję danego typ (prng_int, adc_int itp.).

helloworld922
źródło
0

Często kod jest używany na ARM thumb i AVR (i x86, powerPC i innych architekturach), a 16 lub 32 bity mogą być bardziej wydajne (oba sposoby: flash i cykle) na STM32 ARM nawet dla zmiennej, która mieści się w 8 bitach ( 8-bit jest bardziej wydajny w AVR) . Jednak jeśli SRAM jest prawie pełny, powrót do 8 bitów w przypadku zmiennych globalnych może być rozsądny (ale nie w przypadku zmiennych lokalnych). Jeśli chodzi o przenośność i konserwację (szczególnie w przypadku 8-bitowych wersji), zaletą (bez wad) jest określenie MINIMALNEGO odpowiedniego rozmiaru zamiast dokładnego rozmiaru i typedef w jednym miejscu .h (zwykle pod ifdef) w celu dostrojenia (prawdopodobnie uint_fast8_t / uint_least8_t) podczas przenoszenia / kompilacji, np .:

// apparently uint16_t is just as efficient as 32 bit on STM32, but 8 bit is punished (with more flash and cycles)
typedef uint16_t uintG8_t; // 8bit if SRAM is scarce (use fol global vars that fit in 8 bit)
typedef uint16_t uintL8_t; // 8bit on AVR (local var, 16 or 32 bit is more efficient on STM + less flash)
// might better reserve 32 bits on some arch, STM32 seems efficient with 16 bits:
typedef uint16_t uintG16_t; // 16bit if SRAM is scarce (use fol global vars that fit in 16 bit)
typedef uint16_t uintL16_t; // 16bit on AVR (local var, 16 or 32 bit whichever is more efficient on other arch)

Biblioteka GNU trochę pomaga, ale zazwyczaj typedefs ma sens:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// ale uint_fast8_t dla OBA, gdy SRAM nie stanowi problemu.

Marcell
źródło