Czy jakieś znaczące rozszerzenia C obejmują typy liczb całkowitych, których zachowanie jest niezależne od wielkości słowa maszynowego

12

Interesującą cechą języka C w porównaniu z innymi językami jest to, że wiele jego typów danych opiera się na wielkości słowa architektury docelowej, a nie jest określanych w kategoriach bezwzględnych. Chociaż pozwala to na użycie języka do pisania kodu na maszynach, które mogą mieć trudności z niektórymi typami, bardzo utrudnia projektowanie kodu, który będzie działał konsekwentnie na różnych architekturach. Rozważ kod:

uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;

W architekturze, w której intjest 16 bitów (nadal tak jest w przypadku wielu małych mikrokontrolerów), kod ten przypisałby wartość 1 przy użyciu dobrze zdefiniowanego zachowania. Na komputerach, na których intjest 64 bity, przypisałby wartość 4294836225, ponownie używając dobrze zdefiniowanego zachowania. Na komputerach, na których intjest 32 bity, prawdopodobnie przypisałby wartość -131071 (nie wiem, czy byłoby to zachowanie zdefiniowane w implementacji czy niezdefiniowane). Mimo że kod nie używa niczego poza tymi, które nominalnie mają być „stałymi” typami, standard wymagałby, aby dwa różne rodzaje kompilatorów używanych obecnie dały dwa różne wyniki, a wiele popularnych kompilatorów dzisiaj dałoby trzeci.

Ten konkretny przykład jest nieco wymyślony, ponieważ nie spodziewałbym się, że w kodzie rzeczywistym przypisze produkt dwóch 16-bitowych wartości bezpośrednio do wartości 64-bitowej, ale został wybrany jako krótki przykład pokazujący trzy sposoby liczb całkowitych promocje mogą wchodzić w interakcje z niepodpisanymi typami o podobnej wielkości. Istnieją pewne sytuacje w świecie rzeczywistym, w których matematyka na niepodpisanych typach musi być wykonywana zgodnie z regułami arytmetyki liczb całkowitych matematycznych, inne, w których konieczne jest, aby były wykonywane zgodnie z regułami arytmetyki modułowej, i niektóre, w których tak naprawdę nie jest ” to ważne. Wiele rzeczywistych kodów, takich jak sumy kontrolne, polega na uint32_tzawijaniu arytmetycznym mod 2³² i na możliwości wykonywania dowolnych zadańuint16_t arytmetyka i otrzymuj wyniki, które są, co najmniej, zdefiniowane jako dokładne mod 65536 (w przeciwieństwie do wyzwalania Nieokreślonego Zachowania).

Chociaż ta sytuacja wydaje się wyraźnie niepożądana (i stanie się bardziej, gdy 64-bitowe przetwarzanie stanie się normą dla wielu celów), komitet normalizacyjny C z tego, co zaobserwowałem, woli wprowadzić funkcje językowe, które są już używane w niektórych znaczących produkcjach środowiska, zamiast wymyślać je „od zera”. Czy istnieją jakieś znaczące rozszerzenia języka C, które pozwoliłyby kodowi określić nie tylko sposób przechowywania typu, ale także sposób jego zachowania w scenariuszach z możliwymi promocjami? Widzę co najmniej trzy sposoby, w jakie rozszerzenie kompilatora może rozwiązać takie problemy:

  1. Dodając dyrektywę, która instruuje kompilator, aby wymusił określone „podstawowe” typy liczb całkowitych na określone rozmiary.

  2. Dodając dyrektywę, która instruuje kompilator, aby oceniał różne scenariusze promocji, tak jakby typy komputerów miały określone rozmiary, niezależnie od rzeczywistych rozmiarów typów w architekturze docelowej.

  3. Umożliwiając za pomocą deklarowania typów o określonych cechach (np. Deklarując, że typ powinien zachowywać się jak pierścień algebraiczny zwijający mod-65536, niezależnie od rozmiaru słowa podstawowego, i nie powinien być domyślnie konwertowany na inne typy; dodanie a wrap32do intpowinno dać wynik typu, wrap32niezależnie od tego, czy intjest większy niż 16 bitów, podczas gdy dodawanie wrap32bezpośrednio do a wrap16powinno być nielegalne (ponieważ żaden z nich nie może przekonwertować na inny).

Moja własna preferencja byłaby trzecią alternatywą, ponieważ pozwoliłaby nawet maszynom o nietypowych rozmiarach słów pracować z dużą ilością kodu, który oczekuje, że zmienne „zawiną” się tak, jak w przypadku potęgi dwóch rozmiarów; kompilator może wymagać dodania instrukcji maskowania bitów, aby typ zachowywał się odpowiednio, ale jeśli kod potrzebuje typu, który otacza mod 65536, lepiej jest, aby kompilator generował takie maskowanie na komputerach, które go potrzebują, niż zaśmiecać nim kod źródłowy lub po prostu mieć taki kod przez nieużywalny na komputerach, na których takie maskowanie byłoby potrzebne. Zastanawiam się jednak, czy istnieją jakieś powszechne rozszerzenia, które osiągnęłyby przenośne zachowanie za pomocą dowolnego z powyższych środków, lub za pomocą środków, o których nie myślałem.

Aby wyjaśnić, czego szukam, jest kilka rzeczy; w szczególności:

  1. Chociaż istnieje wiele sposobów pisania kodu, aby zapewnić pożądaną semantykę (np. Definiowanie makr w celu wykonania obliczeń matematycznych na operandach niepodpisanych o określonej wielkości, aby uzyskać wynik, który jawnie zawija się lub nie) lub przynajmniej zapobiega niepożądanym semantyka (np. warunkowo zdefiniuj typ, wrap32_tktóry ma znaleźć się uint32_tw kompilatorach, w których a uint32_tnie zostałby awansowany, i stwierdź, że lepiej jest dla kodu, który wymaga wrap32_tniepowodzenia kompilacji na komputerach, na których ten typ zostałby awansowany, niż uruchomić go i dać fałszywe zachowanie), jeśli istnieje jakikolwiek sposób na napisanie kodu, który grałby najkorzystniej w przyszłych rozszerzeniach języka, użycie tego byłoby lepsze niż opracowanie własnego podejścia.

  2. Mam kilka solidnych pomysłów na to, jak można rozszerzyć język, aby rozwiązać wiele problemów z liczbami całkowitymi, umożliwiając kodowi uzyskanie identycznej semantyki na maszynach o różnych rozmiarach słów, ale zanim poświęcę znaczący czas na ich pisanie, chciałbym wiedzieć, jakie wysiłki w tym kierunku zostały już podjęte.

W żaden sposób nie chcę być postrzegany jako dyskredytujący Komitet ds. Norm C lub pracę, którą wykonali; Oczekuję jednak, że w ciągu kilku lat konieczne będzie poprawne działanie kodu na komputerach, na których „naturalny” typ promocyjny miałby 32 bity, a także na tych, na których byłby to 64 bity. Myślę, że przy niektórych skromnych rozszerzeniach języka (bardziej skromnych niż wiele innych zmian między C99 i C14) możliwe byłoby nie tylko zapewnienie czystego sposobu wydajnego korzystania z architektur 64-bitowych, ale także okazja ułatwienia interakcji z maszyny o „nietypowym rozmiarze słowa”, które normalnie od dawna stawiały na głowie, aby obsługiwać [np. umożliwiając maszynom z 12-bitowym charkodem uruchomienie, które oczekujeuint32_towinąć mod 2³²]. W zależności od kierunku, w którym podążą przyszłe rozszerzenia, oczekiwałbym również, że powinna istnieć możliwość zdefiniowania makr, które pozwoliłyby na użytkowanie dzisiejszego kodu w dzisiejszych kompilatorach, w których domyślne typy liczb całkowitych zachowują się jak „oczekiwane”, ale także w przyszłych kompilatorach, w których liczba całkowita typy będą domyślnie zachowywać się inaczej, ale gdzie mogą zapewnić wymagane zachowania.

supercat
źródło
4
@RobertHarvey Jesteś pewien? Jak rozumiem promocję liczb całkowitych , jeśli intjest większa niż uint16_t, operandy mnożenia będą promowane do, inta mnożenie zostanie przeprowadzone jako intmnożenie, a wynikowa intwartość zostanie przekonwertowana int64_tna inicjalizację who_knows.
3
@RobertHarvey How? W kodzie OP nie ma wzmianki o tym int, ale wciąż się wślizguje. (Ponownie zakładając, że rozumiem standard C jest poprawny.)
2
@RobertHarvey Pewnie to brzmi źle, ale jeśli nie możesz wskazać takiego sposobu, nie wnosisz nic, mówiąc „nie, musisz robić coś złego”. Samo pytanie brzmi: jak uniknąć promocji na liczbę całkowitą lub obejść jej efekty!
3
@RobertHarvey: Jeden z historycznych celów Komitet Standardów C było umożliwienie prawie każda maszyna mieć „kompilator C” i mają być specyficzne reguły wystarczy, że niezależnie rozwinięty kompilatory C dla każdej poszczególnej komputerze docelowym będzie być w większości wymienne. Komplikowało to fakt, że ludzie zaczęli pisać kompilatory C dla wielu maszyn przed opracowaniem standardów, a Komitet Normalizacyjny nie chciał zabronić kompilatorom robienia czegokolwiek, na czym mógłby polegać istniejący kod . Niektóre raczej podstawowe aspekty standardu ...
supercat
3
... są takie, jakie są, nie dlatego, że ktoś próbował sformułować zbiór zasad, które „miały sens”, ale raczej dlatego, że Komitet starał się ustalić wszystkie rzeczy, które łączyły już niezależnie napisane kompilatory . Niestety takie podejście doprowadziło do powstania standardów, które są jednocześnie zbyt niejasne, aby umożliwić programistom określenie, co należy zrobić, ale zbyt szczegółowe, aby umożliwić kompilatorom „po prostu to zrobić”.
supercat

Odpowiedzi:

4

Jako typowa intencja takiego kodu

uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;

jest wykonanie mnożenia w 64 bitach (rozmiar zmiennej, w której zapisywany jest wynik), zwykle sposobem uzyskania poprawnego wyniku (niezależnego od platformy) jest rzutowanie jednego z operandów w celu wymuszenia 64-bitowego mnożenia:

uint16_t ffff16 = 0xFFFF;
int64_t i_know = (int64_t)ffff16 * ffff16;

Nigdy nie spotkałem żadnych rozszerzeń C, dzięki którym ten proces byłby automatyczny.

Bart van Ingen Schenau
źródło
1
Moje pytanie nie było sposobu, aby wymusić poprawną ocenę jednego konkretnego wyrażenia arytmetycznego (w zależności jaki rodzaj wyniku chce, albo rzucić argument do uint32_talbo użycie makra zdefiniowane jako albo #define UMUL1616to16(x,y)((uint16_t)((uint16_t)(x)*(uint16_t)(y)))czy #define UMUL1616to16(x,y)((uint16_t)((uint32_t)(x)*(uint16_t)(y)))w zależności od wielkości int), ale raczej, czy istnieją wszelkie nowe standardy, jak użytecznie radzić sobie z takimi rzeczami, a nie definiować własne makra.
supercat
Powinienem również wspomnieć, że w przypadku takich rzeczy, jak obliczanie wartości skrótu i ​​sumy kontrolnej, często celem będzie uzyskanie wyniku i obcięcie go do wielkości operandów. Typową intencją takiego wyrażenia (ushort1*ushort2) & 65535ubyłoby wykonanie arytmetyki mod-65536 dla wszystkich wartości argumentów. Czytając uzasadnienie C89, myślę, że jest całkiem jasne, że chociaż autorzy zauważyli, że taki kod może zawieść w niektórych implementacjach, jeśli wynik przekroczy 2147483647, spodziewali się, że takie implementacje będą coraz rzadsze. Taki kod czasami jednak zawodzi w nowoczesnym gcc.
supercat