Недостатки или компромиссы использования типов с явным размером на языках семейства C

Я разрабатываю несколько проектов на C и С++, которые необходимо переносить на нескольких настольных и мобильных платформах. Я знаю, что важно использовать типы с явным размером u32_t i64_ t и т.д., Когда я читаю и записываю данные на диск.

Было бы неплохо использовать типы с явным размером всех целых типов для обеспечения последовательного выполнения? Я слышал, что типы с явным образом могут влиять на производительность, потому что процессоры оптимизированы для ожидаемого типа int и т.д. Я также прочитал, что хорошей стратегией является использование типов с явным размером внутри для членов данных класса, но не в интерфейсах.

Есть ли какая-либо передовая практика в отношении типов с явным размером элементов данных и интерфейсов? (Я предполагаю, что не было бы большой разницы между C или С++ в этих ситуациях, но дайте мне знать, если есть)

Ответ 1

Хорошая вещь о базовом типе "int" заключается в том, что он почти всегда будет самым быстрым целым типом для любой платформы, на которой вы сейчас компилируете.

С другой стороны, преимущество использования, скажем, int32_t (а не только int) заключается в том, что ваш код может рассчитывать на то, что int32_t всегда имеет ширину в 32 бита независимо от того, на какой платформе он скомпилирован, что означает, что вы можете безопасно сделайте больше предположений о поведении значения, чем вы могли бы с помощью int. При использовании типов фиксированного размера, если ваш код вообще компилируется на новой платформе Y, тогда он будет более точно вести себя так же, как на старой платформе X.

Недопустимым (теоретическим) недостатком int32_t является то, что новая платформа X может не поддерживать 32-битные целые числа (в этом случае ваш код вообще не будет компилироваться на этой платформе), или он может поддерживать их, но обрабатывать их медленнее, чем это будет обрабатывать простые старые функции.

Приведенные выше примеры немного надуманны, так как почти все современные аппаратные средства обрабатывают 32-битные целые числа на полной скорости, но там (и существуют) существуют платформы, где манипуляция с int64_ts медленнее, чем манипуляция с int, поскольку (a) процессор имеет 32-разрядные регистры и поэтому должны разделить каждую операцию на несколько этапов и, конечно, (b) 64-разрядное целое число будет занимать вдвое больше памяти, чем 32-разрядное целое число, что может оказать дополнительное давление на кеши.

Но: имейте в виду, что для 99% пользователей программного обеспечения эта проблема не будет иметь никакого заметного влияния на производительность, просто потому, что 99% программного обеспечения там не связаны с ЦП в эти дни, и даже для кода, который маловероятен, что целочисленная ширина будет большой проблемой производительности. Итак, что это действительно означает, как вы хотите, чтобы ваша целочисленная математика вела себя?

  • Если вы хотите, чтобы компилятор гарантировал, что ваши целые значения всегда занимают 32 бита ОЗУ и всегда будут "обтекать" в 2 ^ 31 (или 2 ^ 32 для беззнакового), независимо от того, на какой платформе вы 'компилируем, переходите к int32_t (и т.д.).

  • Если вы не заботитесь об обертывании (потому что вы знаете, что ваши целые числа никогда не будут завернуты, из-за характера данных, которые они хранят), и вы хотите сделать код немного более переносимым для нечетных/необычных целей компиляции и, по крайней мере, теоретически быстрее (хотя, вероятно, не в реальной жизни), тогда вы можете придерживаться простого старого short/int/long.

Лично я использую типы фиксированного размера (int32_t и т.д.) по умолчанию, если только не существует очень ясной причины, потому что я хочу минимизировать количество вариантов поведения на разных платформах. Например, этот код:

for (uint32_t i=0; i<4000000000; i++) foo();

... всегда будет вызывать foo() ровно 4000000000 раз, тогда как этот код:

for (unsigned int i=0; i<4000000000; i++) foo();

может вызывать foo() 4000000000 раз, или он может перейти в бесконечный цикл, в зависимости от того, является ли (sizeof (int) >= 4) или нет. Конечно, можно было бы вручную проверить, что второй фрагмент не делает этого на какой-либо конкретной платформе, но, тем не менее, с учетом разницы в производительности, равной нулю между двумя стилями, я предпочитаю первый подход, поскольку предсказание его поведения - легкая задача. Я думаю, что подход char/short/int/long был полезен еще в начале C, когда компьютерная архитектура была более разнообразной, а процессоры были достаточно медленными, чтобы достижение полной собственной производительности было более важным, чем безопасное кодирование.

Ответ 2

Используйте inttypes.h или stdint.h. Это ANSI-C, поэтому он будет поддерживаться любой инструментальной цепочкой, которая будет соответствовать требованиям ANSI.

Кроме того, он по-прежнему сохраняет работу по восстановлению колеса.

Единственное, что вы должны сделать, это

#include <inttypes.h>

uint32_t integer_32bits_nosign;
  • Еще одна проблема с переносимостью: Так важно, как ширина данных данных endianess. Вы должны проверить целевую цель со стандартными макросами:

    struct {
    #if defined( __BIG_ENDIAN__ ) || defined( _BIG_ENDIAN )
        // Data disposition for Big Endian   
    #else
        // Data disposition for Little Endian   
    #endif
    };
    

Это особенно удобно, если вы используете бит-поля.


EDIT:

Конечно, вы можете использовать <csdtint>, как предложили другие, если вы планируете использовать его только на С++.

Ответ 3

Довольно неприятный "gotcha" с фиксированными типами заключается в том, что, хотя они создают впечатление, что код не зависит от размера "int", это на самом деле иллюзия. Кусок кода, например:

uint32_t mul(uint16_t a, uint16_t b)
{ return a*b; }

будет иметь определенное значение для всех значений "a" и "b" на всех платформах где "int" составляет 40 бит или больше, а также будет определять значение для всех значений "a" и "b" на всех платформах, где "int" - 16 бит, хотя значения будут отличаться, когда арифметическое произведение равно 65535. Авторы Стандарт C89 отметил, что, хотя этого не требуется, большинство реализации этой эпохи определяли их целочисленное математическое поведение, так что большинство подписанные операции - с несколькими конкретными исключениями - будут вести себя одинаково с их неподписанными аналогами, даже когда результат был между INT_MAX + 1 и UINT_MAX - и, следовательно, на этих компиляторах поведение для всех значений "a" и "b" будет соответствовать поведению на машинах с более крупными типами "int". В нем есть однако, для 32-битных компиляторов для генерации кода, который будет перерыв со значениями, превышающими INT_MAX, поскольку Стандарт не запрещает их от этого.

Ответ 4

В stdint.h есть тип fast_ типа размерного размера, компилятор выберет наиболее быстрое целое число с требуемым размером в платформе, например (извлечено из stdint.h)

typedef signed char     int_fast8_t;
#if __WORDSIZE == 64
typedef long int        int_fast16_t;
typedef long int        int_fast32_t;
typedef long int        int_fast64_t;
#else
typedef int         int_fast16_t;
typedef int         int_fast32_t;
__extension__
typedef long long int       int_fast64_t;
#endif