Подтвердить что ты не робот

Почему uint32_t предпочтительнее, чем uint_fast32_t?

Кажется, что uint32_t гораздо более распространен, чем uint_fast32_t (я понимаю, что это анекдотическое доказательство). Это кажется мне интригующим.

Почти всегда, когда я вижу, что реализация использует uint32_t, все, что она действительно хочет, представляет собой целое число, которое может содержать значения до 4 294 967 295 (обычно намного ниже, где-то между 65,535 и 4,294,967,295).

Кажется странным использовать uint32_t, поскольку гарантия "ровно 32 бита" не нужна, а "самая быстрая доступная >= 32 бит" гарантия uint_fast32_t кажется правильной идеей. Более того, в то время как он обычно реализуется, uint32_t на самом деле не гарантируется.

Почему бы предпочесть uint32_t? Это просто лучше известно или есть технические преимущества по сравнению с другими?

4b9b3361

Ответ 1

uint32_t гарантированно будет иметь почти одинаковые свойства на любой платформе, которая его поддерживает. 1

uint_fast32_t очень мало гарантий того, как он ведет себя в разных системах по сравнению.

Если вы переключаетесь на платформу, где uint_fast32_t имеет другой размер, весь код, который использует uint_fast32_t должен быть повторно протестирован и проверен. Все предположения о стабильности будут вне окна. Вся система будет работать по-другому.

При написании кода у вас может даже не быть доступа к системе uint_fast32_t которой не равен 32 битам.

uint32_t не будет работать по-другому (см. сноску).

Правильность важнее скорости. Таким образом, преждевременная корректность - лучший план, чем преждевременная оптимизация.

Если бы я писал код для систем, в которых значение uint_fast32_t составляло 64 или более бит, я мог бы проверить свой код для обоих случаев и использовать его. Запретить как потребность, так и возможность, это плохой план.

Наконец, uint_fast32_t когда вы храните его в течение любого промежутка времени или количества экземпляров, может быть медленнее, чем uint32 просто из-за проблем с размером кэша и пропускной способности памяти. Сегодняшние компьютеры гораздо чаще связаны с памятью, чем с процессором, и uint_fast32_t может быть быстрее изолированно, но не после того, как вы uint_fast32_t накладные расходы памяти.


1 Как отметил @chux в комментарии, если unsigned больше, чем uint32_t, арифметика для uint32_t проходит обычные целочисленные преобразования, а если нет, она остается как uint32_t. Это может вызвать ошибки. Нет ничего идеального.

Ответ 2

Почему многие люди используют uint32_t а не uint32_fast_t?

Примечание: uint32_fast_t названный uint32_fast_t должен быть uint_fast32_t.

uint32_t имеет более жесткую спецификацию, чем uint_fast32_t и поэтому обеспечивает более согласованную функциональность.


uint32_t:

  • Различные алгоритмы определяют этот тип. ИМО - лучшая причина для использования.
  • Точная ширина и диапазон известны.
  • Массивы этого типа не имеют отходов.
  • целое число без знака с его переполнением более предсказуемо.
  • Более точное совпадение по дальности и математике 32 типов -bit других языков.
  • Никогда не дополняется.

Минусы uint32_t:

  • Не всегда доступно (но это редко в 2018 году).
    Например: Платформы не имеющие 8/16/32 -bit целых чисел (9/18/36 -bit, другие).
    Например: платформы, использующие не -2 дополнение. старый 2200

uint_fast32_t профи:

  • Всегда доступен.
    Это всегда позволяет всем платформам, новым и старым, использовать быстрые/минимальные типы.
  • "Самый быстрый" тип, поддерживающий диапазон 32 -bit.

uint_fast32_t минусы:

  • Диапазон только минимально известен. Например, это может быть 64 -bit тип.
  • Массивы этого типа могут быть бесполезны в памяти.
  • Все ответы (мои тоже поначалу), пост и комментарии использовали неверное имя uint32_fast_t. Похоже, многие просто не нуждаются и используют этот тип. Мы даже не использовали правильное имя!
  • Возможна прокладка - (редко).
  • В некоторых случаях наиболее быстрым типом может быть другой тип. Таким образом, uint_fast32_t является только приближением 1-го порядка.

В конце концов, что лучше, зависит от цели кодирования. Если кодирование для очень широкой переносимости или какой-либо нишевой функции производительности, используйте uint32_t.


Есть еще одна проблема при использовании этих типов, которая вступает в игру: их рейтинг по сравнению с int/unsigned

Предположительно, uint_fastN_t будет, по крайней мере, рангом unsigned. Это не указано, но определенное и проверяемое условие.

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

В связи с этим: преимущество переносимости uint_fastN_t с математическими операциями выбора.


Дополнительное примечание о int32_t а не int_fast32_t: на редких машинах INT_FAST32_MIN может быть -2, 147,483,647, а не -2, 147,483,648. Большая точка: (u)intN_t типы (u)intN_t четко определены и ведут к переносимому коду.

Ответ 3

Почему многие люди используют uint32_t, а не uint32_fast_t?

Глупый ответ:

  • Нет стандартного типа uint32_fast_t, правильное написание uint_fast32_t.

Практический ответ:

  • Многие люди используют uint32_t или int32_t для своей точной семантики, ровно 32 бита с беззнаковым обертом вокруг арифметики (uint32_t) или 2 дополнения (int32_t). Типы xxx_fast32_t могут быть больше и, следовательно, нецелесообразно хранить в двоичных файлах, использовать в упакованных массивах и структурах или отправлять по сети. Кроме того, они могут быть даже не быстрее.

Прагматический ответ:

  • Многие люди просто не знают (или просто не заботятся) о uint_fast32_t, как показано в комментариях и ответах, и, вероятно, предполагают, что plain unsigned int имеет одну и ту же семантику, хотя многие современные архитектуры все еще имеют 16 -bit int и некоторые редкие образцы Музея имеют другие странные размеры int менее 32.

Ответ UX:

  • Хотя, возможно, быстрее, чем uint32_t, uint_fast32_t работает медленнее: требуется больше времени для ввода, особенно учитывая поиск орфографии и семантики в документации по C; -)

Элегантность имеет значение (очевидно, основанное на мнениях):

  • uint32_t выглядит достаточно плохо, что многие программисты предпочитают определять свой собственный тип u32 или uint32... С этой точки зрения, uint_fast32_t выглядит неуклюжим, чем исправление. Не удивительно, что он сидит на скамейке со своими друзьями uint_least32_t и такими.

Ответ 4

Одна из причин заключается в том, что unsigned int уже "самый быстрый" без необходимости в каких-либо специальных typedefs или необходимости что-то включать. Поэтому, если вам это нужно быстро, просто используйте базовый тип int или unsigned int.
Хотя стандарт явно не гарантирует, что он является самым быстрым, он косвенно делает это, заявляя, что "Plain ints имеет естественный размер, предложенный архитектурой среды исполнения" в п. 3.9.1. Другими словами, int (или его неподписанный аналог) - это то, что наиболее удобно с процессором.

Теперь, конечно, вы не знаете, какой размер unsigned int может быть. Вы знаете, что он по крайней мере такой же большой, как short (и я, кажется, помню, что short должен быть не менее 16 бит, хотя я не могу найти это в стандарте сейчас!). Обычно это просто просто 4 байта, но теоретически может быть больше или в крайнем случае даже меньше (, хотя я лично никогда не сталкивался с архитектурой, где это было так, даже на 8-разрядных компьютерах в 1980-е годы... возможно, некоторые микроконтроллеры, которые знают, что, я страдаю от деменции, int был очень четко 16 бит в то время).

В стандарте С++ не указывается, какие типы <cstdint> или что они гарантируют, он просто упоминает "то же, что и в C".

uint32_t, по стандарту C, гарантирует, что вы получите ровно 32 бита. Ничего особенного, ни меньше, ни бит. Иногда это именно то, что вам нужно, и, следовательно, оно очень ценно.

uint_least32_t гарантирует, что независимо от размера, он не может быть меньше 32 бит (но он может быть очень большим). Иногда, но гораздо реже, чем точный witdh или "не волнует", это то, что вы хотите.

Наконец, uint_fast32_t является несколько лишним, на мой взгляд, за исключением целей, связанных с документацией. Стандартные состояния C "обозначает целочисленный тип, который обычно является самым быстрым" (обратите внимание на слово "обычно" ) и прямо упоминает, что он не должен быть самым быстрым для всех целей. Другими словами, uint_fast32_t примерно такой же, как uint_least32_t, что также является самым быстрым, только никакая гарантия не указана (но не гарантирует ни одного способа).

Поскольку большую часть времени вам либо не нужен точный размер, либо вы хотите ровно 32 (или 64, иногда 16) бита, а так как тип "не заботится" unsigned int самый быстрый, так или иначе, это объясняет, почему uint_fast32_t используется не так часто.

Ответ 5

Я не видел доказательств того, что uint32_t будет использоваться для своего диапазона. Вместо этого большую часть времени, которое я видел uint32_t, используется, чтобы удерживать ровно 4 октета данных в разных алгоритмах с гарантированной семантикой wraparound и shift!

Есть и другие причины использовать uint32_t вместо uint_fast32_t: часто он обеспечивает стабильную ABI. Кроме того, использование памяти может быть точно известно. Это очень сильно компенсирует прирост скорости от uint_fast32_t, если этот тип будет отличаться от значения uint32_t.

Для значений < 65536, есть уже удобный тип, он называется unsigned int (unsigned short требуется иметь хотя бы этот диапазон, но unsigned int имеет собственный размер слова). Для значений < 4294967296, есть еще один под названием unsigned long.


И, наконец, люди не используют uint_fast32_t, потому что досадно долго печатать и легко ошибаться: D

Ответ 6

Несколько причин.

  • Многие люди не знают, что существуют "быстрые" типы.
  • Это более подробный тип.
  • Сложнее рассуждать о поведении ваших программ, когда вы не знаете фактического размера этого типа.
  • Стандарт фактически не фиксируется быстрее, и не может ли он действительно, какой тип на самом деле быстрее всего может быть очень зависимым от контекста.
  • Я не видел никаких доказательств того, что разработчики платформ задумываются о размерах этих типов при определении своих платформ. Например, на x86-64 Linux "быстрые" типы - все 64-разрядные, хотя x86-64 имеет аппаратную поддержку для быстрых операций с 32-битными значениями.

Таким образом, "быстрые" типы - бесполезный мусор. Если вам действительно нужно выяснить, какой тип является самым быстрым для данного приложения, вам нужно сравнить свой код с вашим компилятором.

Ответ 7

С точки зрения правильности и простоты кодирования, uint32_t имеет множество преимуществ перед uint_fast32_t, в частности, из-за более точно определенного размера и арифметической семантики, как указывало многие пользователи выше.

То, что, возможно, было упущено, состоит в том, что одно из возможных преимуществ uint_fast32_t - то, что оно может быть быстрее, просто никогда не материализовалось каким-либо значимым образом. Большинство 64-разрядных процессоров, которые доминировали в 64-битной эре (в основном, x86-64 и Aarch64), выросли из 32-разрядных архитектур и имеют быстрые 32-разрядные собственные операции даже в режиме 64-разрядной версии. Таким образом, uint_fast32_t является таким же, как uint32_t на этих платформах.

Даже если некоторые из "также запущенных" платформ, таких как POWER, MIPS64, SPARC предлагают только 64-битные операции ALU, подавляющее большинство интересных 32-битных операций может быть выполнено отлично в 64-битных регистрах: нижний 32-бит будет иметь желаемые результаты (и все основные платформы, по крайней мере, позволят вам загрузить/сохранить 32-битные). Левый сдвиг является основным проблемным, но даже это может быть оптимизировано во многих случаях оптимизацией отслеживания значения/диапазона в компиляторе.

Я сомневаюсь, что иногда немного медленнее сдвиг влево или 32x32 → 64 умножение перевешивает вдвое больше использования памяти для таких значений во всех, кроме самых неясных приложений.

Наконец, я хочу отметить, что хотя компромисс в значительной степени характеризовался как "потенциал использования памяти и векторизации" (в пользу uint32_t) по сравнению с количеством команд/скоростью (в пользу uint_fast32_t) - даже это isn Мне ясно. Да, на некоторых платформах вам понадобятся дополнительные инструкции для некоторых 32-разрядных операций, но вы также сохраните некоторые инструкции, потому что:

  • Использование меньшего типа часто позволяет компилятору умело комбинировать смежные операции, используя одну 64-битную операцию для выполнения двух 32-битных. Примером такого типа "векторизации бедных" является не редкость. Например, создать константу struct two32{ uint32_t a, b; } в rax, например two32{1, 2} можно оптимизировать в один mov rax, 0x20001, в то время как 64 -битной версии требуется две инструкции. В принципе это также должно быть возможным для смежных арифметических операций (одна операция, другой операнд), но я не видел ее на практике.
  • Более низкое использование "памяти" также часто приводит к меньшему количеству инструкций, даже если память или кеш-память не являются проблемой, потому что копируется любая структура типа или массивы этого типа, вы получаете в два раза больше, чем ваш доллар за регистр, скопированный.
  • Меньшие типы данных часто используют лучшие современные соглашения о вызовах, такие как SysV ABI, которые эффективно упаковывают данные структуры данных в регистры. Например, вы можете вернуть до 16-байтовой структуры в регистры rdx:rax. Для функции, возвращающей структуру с 4 uint32_t значениями (инициализированными из константы), которая преобразуется в

    ret_constant32():
        movabs  rax, 8589934593
        movabs  rdx, 17179869187
        ret
    

    В той же структуре с 4-х 64-разрядными uint_fast32_t требуется переместить регистр и четыре магазина в память, чтобы сделать то же самое (и вызывающий может, вероятно, прочитать значения обратно из памяти после возврата):

    ret_constant64():
        mov     rax, rdi
        mov     QWORD PTR [rdi], 1
        mov     QWORD PTR [rdi+8], 2
        mov     QWORD PTR [rdi+16], 3
        mov     QWORD PTR [rdi+24], 4
        ret
    

    Аналогично, при передаче аргументов структуры 32-битные значения упаковываются примерно вдвое плотно в регистры, доступные для параметров, поэтому он делает менее вероятным, что вы закончите аргументы регистра и должны вылиться в стек 1.

  • Даже если вы решите использовать uint_fast32_t для мест, где "скорость имеет значение", у вас также будут места, где вам нужен тип фиксированного размера. Например, при передаче значений для внешнего вывода, с внешнего входа, как части вашего ABI, как части структуры, которая нуждается в определенном макете, или потому, что вы с пользой используете uint32_t для больших совокупностей значений для сохранения на памяти. В тех местах, где вам нужны интерфейсы типа uint_fast32_t и `uint32_t`, вы можете найти (помимо сложности разработки) ненужные расширения знака или другой код, связанный с несоответствием размера. Составители делают работу OK во время оптимизации во многих случаях, но это все еще не является необычным, чтобы видеть это в оптимизированном выпуске при смешивании типов разных размеров.

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


1Чтобы быть ясным, соглашение о структуре упаковки в регистры не всегда является явным выигрышем для меньших значений. Это означает, что меньшие значения могут быть "извлечены" до их использования. Например, простая функция, возвращающая сумму двух членов структуры, нуждается в mov rax, rdi; shr rax, 32; add edi, eax, тогда как для 64-битной версии каждый аргумент получает свой собственный регистр и просто нуждается в одном add или lea. Тем не менее, если вы согласны с тем, что дизайн "плотно упакованных конструкций при прохождении" имеет смысл в целом, то более мелкие значения будут использовать больше преимуществ этой функции.

Ответ 8

Насколько я понимаю, int изначально предполагалось быть "родным" целым типом с дополнительной гарантией того, что он должен быть размером не менее 16 бит - то, что тогда считалось "разумным".

Когда 32-разрядные платформы стали более распространенными, можно сказать, что "разумный" размер изменился на 32 бита:

  • Современные Windows используют 32-разрядные int на всех платформах.
  • POSIX гарантирует, что int не менее 32 бит.
  • С#, Java имеет тип int, который гарантированно составляет ровно 32 бита.

Но когда 64-битная платформа стала нормой, никто не расширил int, чтобы быть 64-разрядным целым, из-за:

  • Портативность: много кода зависит от размера int размером 32 бит.
  • Потребление памяти: удвоение использования памяти для каждого int может быть необоснованным для большинства случаев, так как в большинстве случаев используемые числа намного меньше, чем 2 миллиарда.

Теперь, почему вы предпочитаете uint32_t до uint_fast32_t? По той же причине языки, С# и Java всегда используют фиксированные целые числа: программист не пишет код, думая о возможных размерах разных типов, они пишут для одной платформы и тестового кода на этой платформе. Большая часть кода неявно зависит от конкретных размеров типов данных. И именно поэтому uint32_t является лучшим выбором для большинства случаев - он не допускает никакой двусмысленности в отношении его поведения.

Кроме того, uint_fast32_t действительно самый быстрый тип на платформе с размером, равным или большим, до 32 бит? На самом деле, нет. Рассмотрим этот компилятор кода GCC для x86_64 в Windows:

extern uint64_t get(void);

uint64_t sum(uint64_t value)
{
    return value + get();
}

Сгенерированная сборка выглядит следующим образом:

push   %rbx
sub    $0x20,%rsp
mov    %rcx,%rbx
callq  d <sum+0xd>
add    %rbx,%rax
add    $0x20,%rsp
pop    %rbx
retq

Теперь, если вы измените get() возвращаемое значение на uint_fast32_t (это 4 байта на Windows x86_64), вы получите следующее:

push   %rbx
sub    $0x20,%rsp
mov    %rcx,%rbx
callq  d <sum+0xd>
mov    %eax,%eax        ; <-- additional instruction
add    %rbx,%rax
add    $0x20,%rsp
pop    %rbx
retq

Обратите внимание, что сгенерированный код почти такой же, за исключением дополнительной инструкции mov %eax,%eax после вызова функции, которая предназначена для расширения 32-битного значения в 64-битное значение.

Нет такой проблемы, если вы используете только 32-битные значения, но вы, вероятно, будете использовать те, у которых есть переменные size_t (размеры массива, вероятно?), и 64 бит на x86_64. В Linux uint_fast32_t - 8 байтов, поэтому ситуация другая.

Многие программисты используют int, когда им нужно вернуть небольшое значение (скажем, в диапазоне [-32,32]). Это было бы отлично, если int был бы родным целым размером платформы, но поскольку он не находится на 64-битных платформах, другой тип, который соответствует родному типу платформы, является лучшим выбором (если только он не используется с другими целыми числами меньшего размера).

В принципе, независимо от того, что говорит стандарт, uint_fast32_t в любом случае разбивается на некоторые реализации. Если вам нужна дополнительная инструкция, сгенерированная в некоторых местах, вы должны определить свой собственный "родной" целочисленный тип. Или вы можете использовать size_t для этой цели, так как он обычно будет соответствовать размеру native (я не включаю старые и неясные платформы, такие как 8086, только платформы, которые могут запускать Windows, Linux и т.д.).


Другим признаком, который показывает, что int должен быть нативный целочисленный тип, является "целым правилом продвижения". Большинство процессоров могут выполнять только операции на native, поэтому 32-битный процессор обычно может выполнять только 32-битные дополнения, вычитания и т.д. (Здесь здесь используются процессоры Intel). Целочисленные типы других размеров поддерживаются только с помощью инструкций по загрузке и хранению. Например, 8-битное значение должно быть загружено с помощью соответствующей 8-разрядной подписной "нагрузки 8" или "8-разрядной беззнаковой" команды и будет расширять значение до 32 бит после загрузки. Без целочисленного правила рассылки компиляторам C пришлось бы добавить немного больше кода для выражений, которые используют типы, меньшие, чем собственный. К сожалению, в 64-битных архитектурах это больше не выполняется, поскольку компиляторы теперь должны выдать дополнительные инструкции в некоторых случаях (как было показано выше).

Ответ 9

В практических целях uint_fast32_t совершенно бесполезен. Он определен неправильно на самой распространенной платформе (x86_64) и на самом деле не дает никаких преимуществ нигде, если у вас не очень качественный компилятор. Понятно, что никогда не имеет смысла использовать "быстрые" типы в структурах данных/массивах - любая экономия, которую вы получаете от более эффективного для работы типа, будет затмевана из-за стоимости (промахов в кеше и т.д.) Увеличения размера ваш рабочий набор данных. И для отдельных локальных переменных (счетчики циклов, временные параметры и т.д.) Компилятор, не являющийся игрушкой, обычно может просто работать с более крупным типом в сгенерированном коде, если он более эффективен и только усекает до номинального размера, когда это необходимо для правильности (и с подписанных типов, он никогда не нужен).

Один вариант, который теоретически полезен, uint_least32_t, когда вам нужно иметь возможность хранить любое 32-битное значение, но хотите быть переносимым на машины, которым не хватает 32-битного типа точного размера. Практически, говоря, однако, это не то, о чем вам нужно беспокоиться.

Ответ 10

Во многих случаях, когда алгоритм работает с массивом данных, лучший способ повысить производительность - это минимизировать количество промахов в кэше. Чем меньше каждый элемент, тем больше он может вписаться в кеш. Вот почему много кода все еще написано для использования 32-разрядных указателей на 64-битных машинах: им не нужно ничего около 4 гигабайт данных, но стоимость создания всех указателей и смещений требует восьми байтов вместо четырех существенным.

Существуют также некоторые ABI и протоколы, которые необходимы, чтобы требовать ровно 32 бита, например, IPv4-адреса. То, что действительно означает uint32_t: используйте ровно 32 бита, независимо от того, эффективны ли они на процессоре или нет. Они обычно объявлялись как long или unsigned long, что вызвало множество проблем во время 64-битного перехода. Если вам нужен только неподписанный тип, который содержит номера не менее чем на 2 ³-1, это было определение unsigned long, так как появился первый стандарт C. На практике, однако, достаточно старый код предполагал, что long может содержать любой сдвиг указателя или файла или временную метку, и достаточно старый код предполагает, что он был ровно 32 бита в ширину, что компиляторы не могут сделать long так же, как int_fast32_t не сломав слишком много материала.

В теории было бы более перспективным для программы использовать uint_least32_t и, возможно, даже загружать элементы uint_least32_t в переменную uint_fast32_t для вычислений. Реализация, которая не имела типа uint32_t вообще, могла даже заявить о себе в формальном соответствии со стандартом! (Он просто не смог бы скомпилировать многие существующие программы.) На практике больше нет архитектуры, где int, uint32_t и uint_least32_t не являются одинаковыми, и в настоящее время нет преимуществ для производительности uint_fast32_t. Итак, зачем перегружать вещи?

Однако посмотрите, почему все типы 32_t должны существовать, когда у нас уже есть long, и вы увидите, что эти предположения взорвались на наших лицах раньше. Возможно, ваш код в конечном итоге начнет работать на компьютере, где вычисление 32-битной точности по длине будет меньше, чем размер родного слова, и вам было бы лучше использовать uint_least32_t для хранения и uint_fast32_t для расчёта религиозно. Или, если вы пересечете этот мост, когда вы доберетесь до него, и просто хотите что-то простое, theres unsigned long.

Ответ 11

Чтобы дать прямой ответ: я думаю, что настоящая причина, по которой uint32_t используется uint_fast32_t или uint_least32_t заключается в том, что его проще набирать, а из-за того, что он короче, гораздо приятнее читать: если вы создаете структуры с некоторыми типами и некоторыми из них uint_fast32_t или аналогичные, поэтому зачастую трудно точно выровнять их по bool int bool или другим типам в C, которые довольно короткие (пример: char или character). Я, конечно, не могу подтвердить это достоверными данными, но другие ответы могут только догадываться о причине.

Что касается технических причин, предпочитающих uint32_t, я не думаю, что они есть - когда вам абсолютно необходим 32-битный беззнаковый тип int, тогда этот тип - ваш единственный стандартизированный выбор. Почти во всех других случаях другие варианты технически предпочтительнее - в частности, uint_fast32_t если вас беспокоит скорость, и uint_least32_t если вас беспокоит пространство для хранения. Использование uint32_t в любом из этих случаев может привести к невозможности компиляции, так как тип не обязан существовать.

На практике uint32_t и связанные с ним типы существуют на всех текущих платформах, за исключением некоторых очень редких (в настоящее время) DSP или реализаций шуток, поэтому при использовании точного типа существует небольшой реальный риск. Точно так же, хотя вы можете столкнуться со штрафами за скорость с типами фиксированной ширины, они (на современном процессоре) больше не наносят вред.

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