Как заставить unsigned арифметику использовать типы фиксированной ширины?

Следующий (C99 и более новый) код хочет вычислить квадрат, ограниченный тем же числом бит, что и исходный тип фиксированной ширины.

    #include <stdint.h>
     uint8_t  sqr8( uint8_t x) { return x*x; }
    uint16_t sqr16(uint16_t x) { return x*x; }
    uint32_t sqr32(uint32_t x) { return x*x; }
    uint64_t sqr64(uint64_t x) { return x*x; }

Проблема заключается в следующем: в зависимости от размера int некоторые из умножений могут быть выполнены в аргументах, продвигаемых до (подписанных) int, при этом результат переполняет (подписанный) int, таким образом undefined результат в отношении стандарта; и, возможно, неправильный результат, особенно на (все реже) машинах, не использующих два дополнения.

Если int - 32-разрядный (соответственно 16-разрядный, 64-разрядный, 80 или 128-разрядный), который встречается для sqr16 (соответственно sqr8, sqr32, sqr64) когда x составляет 0xFFFFF (соответственно 0xFF, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF). Ни одна из 4 функций формально не переносима в C99!

Может ли C11 или более поздняя версия или какой-либо выпуск С++ исправить эту неудачную ситуацию?


Простое рабочее решение:

    #include <stdint.h>
     uint8_t  sqr8( uint8_t x) { return 1u*x*x; }
    uint16_t sqr16(uint16_t x) { return 1u*x*x; }
    uint32_t sqr32(uint32_t x) { return 1u*x*x; }
    uint64_t sqr64(uint64_t x) { return 1u*x*x; }

Это стандартно-совместимое, поскольку 1u не продвигается до int и остается без знака; таким образом, левое умножение, затем правое, выполняется как без знака, поэтому они хорошо определены, чтобы дать правильный результат в нужном количестве младших разрядов; то же самое для окончательного неявного приведения к ширине результата.

Обновлено: как предлагается в комментарии от Marc Glisse, я попробовал этот вариант с восемью компиляторами (три версии GCC для x86, начиная с 3.1, MS C/С++ 19.00, Keil ARM компилятор 5, два компилятора Cosmic для вариантов ST7, Microchip MCC18). Все они генерировали тот же самый код, что и оригинал (с оптимизациями, которые я использую в режиме выпуска для реальных проектов). Однако составители могли бы, по-видимому, генерировать худший код, чем оригинал; и у меня есть несколько других моих встроенных компиляторов, чтобы попробовать, включая некоторые 68K и PowerPC.

Какие другие варианты у нас есть, что обеспечивает разумный баланс между вероятной улучшенной производительностью, читабельностью и простотой?

Ответ 1

Вы определили фундаментальное короткое замыкание псевдонимов целочисленного типа в <stdint.h>: они не содержат никакой информации о ранге преобразования типа. Таким образом, вы не можете контролировать, будут ли значения этих типов проходить интегральные рекламные акции, и, как вы правильно заметили, выражение может иметь поведение undefined, когда интегральная продвижение приводит к подписанному типу.

Вкратце: вы не можете использовать типы псевдонимов для выполнения обычных арифметических операций по модулю 2 N. Вам нужно использовать тип, чей (известный!) Ранг конверсии по крайней мере равен int.

Решение в общем случае заключалось бы в том, чтобы преобразовать ваши операнды в наименьшее из unsigned int, unsigned long int или unsigned long long int (если ваша платформа не имеет расширенных интегральных типов), затем оцените выражение, а затем преобразуйте назад к исходному типу (который имеет правильное модульное поведение). На С++ вы, вероятно, можете написать тип типа, который отображает правильный тип переносимым способом.

Как более дешевый трюк и, опять же, предполагая отсутствие (более широкого) расширенного интегрального типа, вы можете просто продвигать все на unsigned long long int и надеяться, что ваш компилятор сделает расчеты эффективным способом.

Ответ 2

Вы не можете избежать неизбежного продвижения по типу int для более узких неподписанных типов.

Это больше свойство оператора умножения, чем что-либо еще.

Чтобы избежать случаев, связанных с углами поведения undefined, единственное, что вы можете сделать, никогда не использовать умножение при использовании неподписанных типов, где квадрат их максимума может переполнять int.

К счастью (если вы не работаете во встроенном мире, вы всегда можете проконсультироваться с документацией для точного поведения), вы можете в значительной степени отправить unsigned short в историю: int, а ее кузена unsigned, скорее всего, не будет медленнее и, возможно, быстрее.

Ответ 3

Как заставить unsigned арифметику на типы фиксированной ширины?
Какие другие варианты у нас есть,...?

Используя типы фиксированной ширины, которые по меньшей мере равны unsigned для типа аргументов функции.

Это приводит к преобразованию не менее unsigned как части передачи параметров. Типы формальных параметров и возвращаемые типы остаются классическими типами фиксированной ширины. Фактические дополнения функции также являются типами фиксированной ширины, но, возможно, более широкими типами фиксированной ширины.

#if UINT16_MAX >= UINT_MAX
   typedef uint8  uint16_t
   typedef uint16 uint16_t
   typedef uint32 uint32_t
   typedef uint64 uint64_t
#elif UINT32_MAX >= UINT_MAX 
   typedef uint8  uint32_t
   typedef uint16 uint32_t
   typedef uint32 uint32_t
   typedef uint64 uint64_t
#elif UINT64_MAX >= UINT_MAX 
   typedef uint8  uint64_t
   typedef uint16 uint64_t
   typedef uint32 uint64_t
   typedef uint64 uint64_t
#endif

uint16_t sqr16(uint16 x) { return x*x; }
uint16_t sqr32(uint32 x) { return x*x; }
uint16_t sqr64(uint64 x) { return x*x; }

// usage
uint16_t x16 = ...;
uint32_t x32 = ...;
uint64_t x64 = ...;
x16 = sqr16(x16);
x32 = sqr32(x32);
x64 = sqr64(x64);

В зависимости от функции это проблема, но если функция вызывается с более широким типом, как показано ниже. uint16_t foo16(uint16 x) может не принять меры предосторожности против получения значения вне диапазона uint16_t.

x16 = foo16(x32);

Если все это лучше? Я по-прежнему предпочитаю явный 1u, как в

uint16_t sqr16(uint16_t x) { return 1u*x*x; }