Чому uint32_t буде кращим, а не uint_fast32_t?


82

Здається, uint32_tце набагато більше, ніж uint_fast32_t(я усвідомлюю, що це анекдотичні докази). Однак це здається мені протиречувальним.

Майже завжди, коли я бачу використання реалізації uint32_t, все, що йому насправді потрібно, це ціле число, яке може містити значення до 4 294 967 295 (зазвичай набагато нижча межа десь між 65 535 і 4294 967 295).

Це здається дивним , щоб потім використовувати uint32_t, так як « в точності 32 біта» гарантія не потрібна, і «швидкі наявні> = 32 біта» гарантія , як uint_fast32_tвидається, саме правильна ідея. Більше того, хоча це зазвичай впроваджується, uint32_tнасправді не гарантується існування.

Чому тоді було uint32_tб кращим? Це просто більше відомо чи є технічні переваги перед іншим?


24
Проста відповідь, можливо, їм потрібно ціле число, яке має рівно 32 біт?
Stargateur

7
По-перше, я чув uint32_fast_t, що, якщо я правильно розумію, це принаймні 32 біти (це означає, що це може бути більше? Звучить для мене оманливим). В даний час я використовую uint32_tта друзів у своєму проекті, оскільки я збираю ці дані та надсилаю їх через мережу, і я хочу, щоб відправник та одержувач точно знали, наскільки великі поля. Здається, це може бути не найнадійнішим рішенням, оскільки платформа може не реалізовуватись uint32_t, але всі мої, мабуть, роблять це, тому я добре в тому, що роблю.
yano

5
@yano: Для роботи в мережі вам також слід подбати про порядок байтів / ендіаність - uint32_tне дає вам цього (і шкода, що немає uint32_t_beі uint32_t_le, що було б доречнішим майже для всіх можливих випадків, коли uint32_tна даний момент найкращий варіант).
Brendan

3
@Brendan - щодо _be та _le, чи могли б htonl () та ntohl () надавати ту саму можливість?
mpez0,

2
@Brendan - це досить важкий об’єкт, який можна сховати у стандартному int, усі з яких є примітивними типами. Я в принципі погоджуюсь з вами, що це потрібно десь обробляти у стандарті, але я думаю, що це може бути не те місце
Стів Кокс

Відповіді:


79

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

uint_fast32_t має дуже мало гарантій щодо того, як він поводиться в різних системах порівняно.

Якщо ви перейдете на платформу, де uint_fast32_tінший розмір, весь код, який використовує uint_fast32_t, повинен бути перевірений і перевірений. Усі припущення щодо стабільності будуть невідомими. Вся система буде працювати по-іншому.

Під час написання коду ви можете навіть не мати доступу до uint_fast32_tсистеми розміром не 32 біти.

uint32_t не буде працювати інакше (див. виноску).

Правильність важливіша за швидкість. Таким чином, передчасна коректність є кращим планом, ніж передчасна оптимізація.

Якщо я писав код для систем із uint_fast32_t64 і більше бітами, я міг би протестувати свій код для обох випадків і використовувати його. Якщо заборонити як потребу, так і можливості, це поганий план.

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


1 Як зазначив @chux у коментарі, якщо unsignedзначення більше uint32_t, арифметика on uint32_tпроходить звичайні цілочисельні акції, а якщо ні, то залишається як uint32_t. Це може спричинити помилки. Ніколи ніколи не буває ідеально.


15
"uint32_t гарантовано мати однакові властивості на будь-якій платформі, яка його підтримує." Існує кутова проблема, коли unsignedвона ширша, ніж, uint32_tа потім uint32_tна одній платформі проходить звичайні цілочисельні акції, а на іншій - ні. Проте з uint32_tцілими математичними завданнями значно зменшується.
chux

2
@chux - кутовий випадок, який може спричинити UB при множенні, тому що просування віддає перевагу переповненню підписаним int та цілим числом зі знаком UB.
CodesInChaos

2
Хоча ця відповідь є правильною, наскільки вона є, вона дуже применшує ключові деталі. У двох словах, uint32_tце те, де важливі точні деталі машинного представлення типу, тоді uint_fast32_tяк для найбільш важливих обчислювальних швидкостей, важлива (не) підписаність та мінімальний діапазон, а деталі представлення є несуттєвими. Існує також те, uint_least32_tде (не) підпис і мінімальний діапазон найважливіші, компактність важливіша за швидкість, а точне подання не є важливим.
Джон Боллінгер,

@JohnBollinger Що добре, але без тестування фактичного обладнання, що реалізує більше 1 варіанту, типи змінних розмірів є пасткою. І причина, чому люди використовують, uint32_tа не інші типи, полягає в тому, що у них зазвичай немає такого обладнання, на якому можна проводити тестування . (Те саме стосується int32_tменшої міри, і навіть intі short).
Якк - Адам Неврамонт,

1
Приклад кутового випадку: Нехай unsigned short== uint32_tі int== int48_t. Якщо ви обчислюєте щось на зразок (uint32_t)0xFFFFFFFF * (uint32_t)0xFFFFFFFF, тоді операнди підвищуються signed intі викликають переповнення цілим числом із підписом, що є невизначеною поведінкою. Дивіться це питання.
Наюкі

32

Чому багато людей використовують, uint32_tа не uint32_fast_t?

Примітка: Помилкові імена uint32_fast_tповинні бути uint_fast32_t.

uint32_tмає більш жорсткі характеристики, ніж, uint_fast32_tі тому робить для більш послідовної функціональності.


uint32_t плюси:

  • Різні алгоритми визначають цей тип. IMO - найкраща причина для використання.
  • Точна ширина та діапазон відомих.
  • Масиви цього типу не втрачають відходів.
  • беззнакова ціла математика з її переповненням є більш передбачуваною.
  • Ближче співпадіння за діапазоном та математикою 32-розрядних типів інших мов.
  • Ніколи не прокладено.

uint32_t мінуси:

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

uint_fast32_t плюси:

  • Завжди в наявності.
    Це завжди дозволяє всім платформам, як новим, так і старим, використовувати швидкі / мінімальні типи.
  • "Найшвидший" тип, що підтримує 32-бітний діапазон.

uint_fast32_t мінуси:

  • Діапазон відомий лише мінімально. Наприклад, це може бути 64-розрядний тип.
  • Масиви цього типу можуть бути марними в пам’яті.
  • Усі відповіді (мої теж спочатку), публікація та коментарі використовували неправильну назву 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у переносимості при вибраних математичних операціях.


Додаткова примітка про, int32_tа не int_fast32_t: На рідкісних машинах INT_FAST32_MINможе становити -2 147 483 647, а не -2 147 483 688. Більший момент: (u)intN_tтипи жорстко вказані і ведуть до портативного коду.


2
Найшвидший тип, який підтримує 32-розрядний діапазон => насправді? Це пережиток часу, коли оперативна пам'ять працювала на швидкості процесора, в наш час баланс різко змінився на ПК, тому (1) витягування 32-бітових цілих чисел з пам'яті вдвічі швидше, ніж витягування 64-бітових і (2) векторизовані інструкції на 32-бітових цілих числах подрібнюється вдвічі більше, ніж на 64-бітових. Це все-таки найшвидше?
Matthieu M.

4
Найшвидше для одних речей, повільніше для інших. Немає універсальної відповіді на питання "який найшвидший розмір цілого числа", якщо врахувати масиви проти нульового розширення. У системі x86-64 System V ABI uint32_fast_tє 64-розрядним типом, тому він економить випадкове розширення знака і дозволяє imul rax, [mem]замість окремої інструкції навантаження, що розширюється нулем, використовувати його з 64-розрядними цілими числами або вказівниками. Але це все, що ви отримуєте за ціну подвійного розміру кеш-пам’яті та додаткового розміру коду (REX має префікс на всьому.)
Пітер Кордес,

1
Крім того, 64-бітне розділення набагато повільніше, ніж 32-бітове, на більшості процесорів x86, а деякі (наприклад, сімейство бульдозерів, Atom та Silvermont) мають 64-бітове множення повільніше, ніж 32. Сімейство бульдозерів також має повільніші 64-бітні popcnt. І пам’ятайте, використовувати цей тип безпечно лише для 32-розрядних значень, оскільки він менший для інших архітектур, тож ви платите цю вартість задарма.
Пітер Кордес,

2
Я би очікував, що як середньозважене середнє значення для всіх програм C та C ++, прийняття рішення uint32_fast_tна x86 є жахливим вибором. imul rax, [mem]Швидші операції незначні, і користь, коли вони відбуваються, здебільшого незначна: відмінності у випадку, про які згадує @PeterCordes, дуже і дуже малі: одна зага в сплавленому домені та нуль у несплавленому домені. У найбільш цікавих сценаріях він навіть не додає жодного циклу. Баланс між двократним використанням пам’яті та гіршою векторизацією складно переконатись, що вона дуже часто виграє.
BeeOnRope

2
@PeterCordes - цікаво, але теж жахливо :). Це зробило fast_tб ще гірше int: воно не тільки має різні розміри на різних платформах, але і має різні розміри залежно від рішень щодо оптимізації та різних розмірів у різних файлах! З практичної точки зору, я думаю, це не може працювати навіть при оптимізації цілої програми: розміри в C і C ++ зафіксовані так, sizeof(uint32_fast_t)або що-небудь, що визначає, що воно навіть безпосередньо повинно завжди повертати одне і те ж значення, тому для компілятора буде дуже важко зробити таку трансформацію.
BeeOnRope

25

Чому багато людей використовують, 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, як це продемонстрували коментарі та відповіді, і, мабуть, припускають, що unsigned intвони мають однакову семантику, хоча багато сучасних архітектур все ще мають 16-бітні ints, а деякі рідкісні музейні зразки мають інші дивні розміри int менше 32.

Відповідь UX:

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

Елегантність має значення (очевидно, на основі думок):

  • uint32_tвиглядає досить погано, що багато програмістів вважають за краще визначати свій власний u32або uint32тип ... З цієї точки зору, uint_fast32_tвиглядає незграбно, що не підлягає ремонту. Не дивно, що він сидить на лавці зі своїми друзями тощо uint_least32_t.

+1 для UX. Це краще, ніж std::reference_wrapperя здогадуюсь, але іноді мені цікаво, чи справді стандартний комітет бажає використовувати типи, які він стандартизує ...
Matthieu M.

7

Однією з причин є те, що unsigned intце вже "найшвидше", без необхідності в будь-яких спеціальних typedefs або необхідності щось включати. Отже, якщо вам це потрібно швидко, просто використовуйте основний intабо unsigned intтип.
Хоча стандарт прямо не гарантує, що він є найшвидшим, він опосередковано робить це, зазначаючи "Рівнинні вставки мають природний розмір, запропонований архітектурою середовища виконання" в 3.9.1. Іншими словами, int(або його непідписаний аналог) - це те, що найбільш зручно для процесора.

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

Стандарт С ++ не заважає вказувати, які це <cstdint>типи або що вони гарантують, він просто згадує "те саме, що в С".

uint32_t, згідно стандарту C, гарантує, що ви отримаєте рівно 32 біти. Не щось інше, не менше і ніяких бітів. Іноді це саме те, що вам потрібно, і, отже, це дуже цінно.

uint_least32_tгарантує, що який би не був розмір, він не може бути меншим за 32 біти (але цілком може бути більшим). Іноді, але набагато рідше, ніж точний розум або "байдуже", це те, що ви хочете.

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

Оскільки більшу частину часу ви або не дбаєте про точний розмір, або вам потрібні рівно 32 (або 64, іноді 16) біт, а оскільки unsigned intтип " мені все одно" найшвидший, це пояснює, чому uint_fast32_tце не так часто використовуються.


3
Я здивований, що ви не пам’ятаєте 16-розрядних int8-розрядних процесорів, я не пам’ятаю жодного з тих часів, який використовував щось більше. Якщо пам'ять служить, компілятори для сегментованої архітектури x86 також використовували 16-біт int.
Марк Ренсом

@MarkRansom: Ого, ти маєш рацію. Я був ооооочень переконаний, що intдля 68000 було 32 біти (про що я думав, як приклад). Це не було ...
Деймон

intзадумувався як найшвидший у минулому тип із мінімальною шириною 16 біт (саме тому C має правило цілочисельного просування), але сьогодні з 64-розрядними архітектурами це вже не відповідає дійсності. Наприклад, 8-байтові цілі числа швидші, ніж 4-байтові цілі числа в біті x86_64, тому що з 4-байтними цілими числами компілятор повинен вставити додаткову інструкцію, яка розширює 4-байтове значення до 8-байтового значення, перш ніж порівнювати його з іншими 8-байтовими значеннями.
StaceyGirl

"unsigned int" не обов'язково найшвидший на x64. Бували дивні речі.
Джошуа

Іншим поширеним випадком є ​​те long, що з історичних причин він повинен бути 32-розрядним, і intтепер він повинен бути не ширшим, ніж long, тому, intможливо, доведеться залишатися 32-розрядним, навіть коли 64 біти будуть швидшими.
Девіслор

6

Я не бачив доказів, які uint32_tб використовувались для його діапазону. Натомість більшість часу, який я бачив uint32_t, використовується, це вміщення рівно 4 октетів даних у різних алгоритмах, із гарантованою семантикою обгортання та зсуву!

Існують також інші причини для використання 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


@ikegami: ти змінив мій намір shortредагуванням. intімовірно швидкий, коли він відрізняється від short.
Антті Хаапала,

1
Тоді ваше останнє речення абсолютно неправильне. Стверджуючи, що ви повинні використовувати unsigned intзамість uint16_fast_tзасобів, які, як ви стверджуєте, знаєте краще, ніж компілятор.
ikegami

Також, вибачаюся за зміну наміру вашого тексту. Це був не мій намір.
ikegami

unsigned longне є хорошим вибором, якщо ваша платформа має 64-розрядні longs, і вам потрібні лише номери <2^32.
Руслан

1
@ikegami: Тип "unsigned int" завжди поводитиметься як тип без підпису, навіть при підвищенні. У цьому відношенні він перевершує обидва uint16_tі uint_fast16_t. Якби вони uint_fast16_tбули задані більш вільно, ніж звичайні цілочисельні типи, такі що його діапазон не повинен бути узгодженим для об'єктів, адреси яких не беруться, це могло б запропонувати певні переваги продуктивності на платформах, які виконують 32-бітову арифметику внутрішньо, але мають 16-бітну шину даних . Однак стандарт не передбачає такої гнучкості.
supercat

5

Кілька причин.

  1. Багато людей не знають, що існують "швидкі" типи.
  2. Більш багатослівно друкувати.
  3. Важче міркувати про поведінку своїх програм, коли ви не знаєте фактичного розміру типу.
  4. Стандарт насправді не фіксується найшвидше, і насправді, який тип насправді найшвидший, може бути дуже залежним від контексту.
  5. Я не бачив жодних доказів того, що розробники платформ задумуються над розміром цих типів при визначенні своїх платформ. Наприклад, у x86-64 Linux усі "швидкі" типи є 64-розрядними, хоча x86-64 має апаратну підтримку для швидких операцій над 32-розрядними значеннями.

Таким чином, "швидкі" типи - нікчемне сміття. Якщо вам дійсно потрібно з'ясувати, який тип є найшвидшим для даного додатка, вам потрібно порівняти свій код із компілятором.


Історично існували процесори, які мали 32-бітну та / або 64-бітну інструкції доступу до пам'яті, але не 8- і 16-бітну. Тож int_fast {8,16} _ це було б не зовсім по-дурному 20+ років тому. AFAIK останнім таким основним процесором став оригінальний DEC Alpha 21064 (друге покоління 21164 вдосконалено). Можливо, все ще існують вбудовані DSP або що завгодно, які роблять лише доступ до слів, але портативність, як правило, не викликає великих занепокоєнь у таких речах, тому я не розумію, чому ви б швидко культували це на них. І були власноруч створені машини Cray "все - 64-розрядні".
user1998586

1
Категорія 1b: Багатьом людей байдуже, що існують "швидкі" типи. Це моя категорія.
gnasher729

Категорія 6: Багато людей не вірять, що „швидкі” типи є найшвидшими. Я належу до цієї категорії.
Clearer

5

З точки зору правильності та простоти кодування, 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) - навіть це мені не зрозуміло. Так, на деяких платформах вам знадобляться додаткові інструкції щодо деяких 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`` типи повинні взаємодіяти, ви можете знайти (на додаток до складності розробки) непотрібні розширення знаків або інший код, що не відповідає розміру. У багатьох випадках компілятори роблять нормальну роботу, оптимізуючи це, але все одно не рідко бачити це в оптимізованому виданні при змішуванні типів різних розмірів.

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


1 Щоб бути зрозумілим, домовленість про структуру упаковки, щільно вписану в регістри, не завжди є явним виграшем для менших значень. Це означає, що менші значення, можливо, доведеться «витягти» перед тим, як їх можна буде використовувати. Наприклад, для простої функції, яка повертає суму двох членів структури, потрібно деякий mov rax, rdi; shr rax, 32; add edi, eaxчас для 64-розрядної версії кожен аргумент отримує власний регістр і просто потребує одиничного addабо lea. Однак якщо ви визнаєте, що дизайн "щільно упакованих структур під час проходження" має загальний сенс, тоді менші значення скористаються більшою перевагою цієї функції.


glibc на x86-64 Linux використовує 64-біт uint_fast32_t, що є помилкою IMO. (Очевидно, Windows uint_fast32_t- це 32-розрядна версія для Windows.) Будучи 64-розрядною на x86-64 Linux, тому я ніколи не рекомендую будь-кому користуватися uint_fast32_t: вона оптимізована для низької кількості інструкцій (аргументи функції та значення повернення ніколи не потребують нульового розширення для використання як індекс масиву) не для загальної швидкості або розміру коду на одній з основних важливих платформ.
Пітер Кордес,

2
О, правильно, я прочитав ваш коментар вище про SysV ABI, але, як ви вказуєте пізніше, можливо, це вирішила інша група / документ - але, мабуть, одного разу це трапляється в значній мірі. Я думаю, навіть сумнівно, що чистий підрахунок циклів / підрахунок інструкцій надає перевагу більшим типам, навіть ігноруючи ефекти слідів пам'яті та векторизацію, навіть на платформах без хорошої підтримки 32-бітової роботи - адже все ще існують випадки, коли компілятор може оптимізувати менші типи. Я додав кілька прикладів вище. @PeterCordes
BeeOnRope

Пакування декількох членів структури в один реєстр SysV досить часто коштує більше інструкцій при поверненні файлу pair<int,bool>або pair<int,int>. Якщо обидва члени не є константами часу компіляції, зазвичай є більше, ніж просто АБО, і абонент повинен розпакувати повернені значення. ( bugs.llvm.org/show_bug.cgi?id=34840 LLVM оптимізує повернення значення, що передається для приватних функцій, і повинен розглядати 32-розрядний int як такий, що приймає ціле, raxтож boolє окремим, dlа не потребує 64-розрядної константи для test).
Пітер Кордес,

1
Я думаю, що компілятори зазвичай не ділять функції. Виділення швидкого шляху як окремої функції є корисною оптимізацією рівня джерела (особливо в заголовку, де його можна вставити в рядок). Може бути дуже добре, якщо 90% входів - це "нічого не робити"; виконати цю фільтрацію в циклі абонента є великим виграшем. IIRC, Linux використовує __attribute__((noinline))саме для того, щоб переконатися, що gcc не вбудовує функцію обробки помилок і не додає купу push rbx/ .../ pop rbx/ ... на швидкий шлях деяких важливих функцій ядра, які мають багато абонентів і самі не вбудовані.
Пітер Кордес,

1
У Java це теж дуже важливо, тому що вбудовування є настільки важливим для подальших оптимізацій (особливо де-віртуалізації, яка є поширеною на відміну від C ++), тому часто варто розбити там швидкий шлях, і "оптимізація байт-коду" насправді є справою (незважаючи на загальноприйнята думка, що це не має сенсу, оскільки JIT робить остаточну компіляцію) лише для того, щоб отримати зворотний відлік байт-коду, оскільки рішення про вбудовування базуються на розмірі байт-коду, а не на розмірі вбудованого машинного коду (і співвідношення може змінюватися залежно від порядку).
BeeOnRope

4

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

Теоретично корисним є один варіант uint_least32_t, коли вам потрібно мати можливість зберігати будь-яке 32-бітове значення, але ви хочете бути портативним для машин, яким бракує 32-бітного типу точного розміру. Практично, говорячи, однак, це не те, про що вам потрібно турбуватися.


4

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

Коли 32-розрядні платформи стали більш поширеними, можна сказати, що "розумний" розмір змінився на 32 біти:

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

Але коли 64-розрядна платформа стала нормою, ніхто не розширився intі став 64-розрядним цілим числом через:

  • Переносимість: багато коду залежить від intрозміру 32 біта.
  • Споживання пам'яті: подвоєне використання пам’яті для кожного intможе бути нерозумним для більшості випадків, оскільки в більшості випадків кількість використовуваних набагато менше 2 мільярдів.

Тепер, чому ви б вважали за краще , uint32_tщоб uint_fast32_t? З тієї ж причини мови C # та 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_t8 байт, тому ситуація інша.

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

В основному, незалежно від того, що говорить стандарт, uint_fast32_tу будь-якому випадку реалізація порушена. Якщо ви дбаєте про додаткові інструкції, сформовані в деяких місцях, вам слід визначити свій власний "рідний" цілий тип. Або ви можете використовувати size_tдля цієї мети, оскільки вона, як правило, відповідає nativeрозміру (я не включаю старі та незрозумілі платформи, такі як 8086, лише платформи, які можуть запускати Windows, Linux тощо).


Ще однією ознакою, яка показує, що intмав бути власний цілочисельний тип, є "правило цілочисельного просування". Більшість центральних процесорів можуть виконувати операції лише з власними, тому 32-бітовий центральний процесор зазвичай може виконувати лише 32-розрядні додавання, віднімання тощо (процесори Intel тут є винятком). Цілі цілі інших розмірів підтримуються лише за допомогою інструкцій щодо завантаження та зберігання. Наприклад, 8-бітове значення повинно бути завантажено з відповідною інструкцією "завантажити 8-бітовий підпис" або "завантажити 8-бітове беззнакове" і після завантаження значення буде розширено до 32 біт. Без цілочисельного правила просування компіляторам С потрібно було б додати трохи більше коду для виразів, що використовують типи, менші за рідний тип. На жаль, це більше не стосується 64-розрядних архітектур, оскільки компіляторам тепер доводиться видавати додаткові інструкції в деяких випадках (як було показано вище).


2
Думки про те, що "ніхто не розширював int до 64-бітових цілих чисел, тому що" та "На жаль, це більше не відповідає 64-бітовій архітектурі", є дуже хорошими моментами . Щоб бути справедливим, хоча щодо "найшвидшого" та порівняння коду збірки: у цьому випадку, здається, 2-й фрагмент коду повільніший із додатковими інструкціями, проте довжина та швидкість коду іноді не так добре співвідносяться. Більш сильне порівняння повідомило б про час роботи, але це не так просто зробити.
chux

Мені нелегко буде виміряти повільність другого коду, процесор Intel, можливо, робить справді хорошу роботу, але довший код означає також велике забруднення кеш-пам’яті. Час від часу одна інструкція, мабуть, не зашкодить, але корисність uint_fast32_t стає неоднозначною.
StaceyGirl

Я рішуче погоджуюсь з корисністю того, що uint_fast32_tстає неоднозначним за будь-яких обставин. Я підозрюю, що основна причина uint_fastN_tвзагалі полягає в тому, щоб прийняти "не давайте використовувати unsignedяк 64-розрядний, хоча це найшвидше на новій платформі, тому що занадто багато коду зламається", але "я все ще хочу швидкий принаймні N-бітний тип . " Я б знову вас провів ультрафіолетом, якби міг.
chux

Більшість 64-розрядних архітектур можуть легко працювати з 32-розрядними цілими числами. Навіть у DEC Alpha (яка була новою галузевою 64-розрядною архітектурою, а не розширенням до існуючої 32-розрядної ISA, такої як PowerPC64 або MIPS64) було 32 та 64-розрядні завантаження / сховища. (Але не байт або 16-розрядні завантаження / сховища!). Більшість інструкцій були лише 64-розрядними, але він мав власну підтримку HW для 32-розрядного додавання / доповнення та множення, що скорочує результат до 32 бітів. ( alasir.com/articles/alpha_history/press/alpha_intro.html ) Отже, не буде майже ніякого приросту швидкості від створення int64 біт, і, як правило, втрата швидкості від сліду кешу.
Пітер Кордес,

Крім того, якщо ви зробили int64-розрядну версію, для вашого uint32_ttypedef із фіксованою шириною потрібен буде __attribute__інший хакер або якийсь спеціальний тип, менший за int. (Або short, але тоді у вас така сама проблема uint16_t.) Ніхто цього не хоче. 32-бітний досить широкий майже для всього (на відміну від 16-бітного); використання 32-бітових цілих чисел, коли це все, що вам потрібно, не є "неефективним" будь-яким значущим чином на 64-бітній машині.
Пітер Кордес,

2

У багатьох випадках, коли алгоритм працює з масивом даних, найкращим способом поліпшення продуктивності є мінімізація кількості пропусків кешу. Чим менше кожен елемент, тим більше їх може поміститися в кеш. Ось чому багато коду все ще пишеться для використання 32-розрядних вказівників на 64-розрядних машинах: їм не потрібно нічого, близького до 4 Гб даних, але вартість створення всіх вказівників та зсувів потребує восьми байт замість чотирьох було б істотним.

Є також деякі ABI та протоколи, яким вказано рівно 32 біти, наприклад, адреси IPv4. Ось що uint32_tнасправді означає: використовуйте рівно 32 біти, незалежно від того, чи це ефективно на центральному процесорі чи ні. Раніше їх оголошували як longабо unsigned long, що викликало багато проблем під час 64-розрядного переходу. Якщо вам просто потрібен непідписаний тип, що містить числа щонайменше до 2³²-1, це було визначено з unsigned longчасу появи першого стандарту C. На практиці, однак, досить старий код припускав, що a 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для релігійного обчислення. Або якщо ви перейдете цей міст, коли дійдете до нього і просто захочете чогось простого, є unsigned long.


Але є архітектури, де intне 32 біти, наприклад ILP64. Не те, що вони поширені.
Antti Haapala,

Я не думаю, що ILP64 існує в теперішньому часі? Кілька веб-сторінок стверджують, що "Cray" використовує його, і всі вони посилаються на ту саму сторінку Unix.org 1997 року, але UNICOS в середині 90-х років насправді зробив щось дивніше, і сучасні Crays використовують апаратне забезпечення Intel. На цій самій сторінці стверджується, що суперкомп'ютери ETA використовували ILP64, але вони давно припинили свою діяльність. Вікіпедія стверджує, що порт HAL Solaris до SPARC64 використовував ILP64, але вони також не працюють роками. CppReference каже, що ILP64 використовувався лише у кількох ранніх 64-розрядних Unices. Тож це стосується лише деяких дуже езотеричних ретрообчислень.
Девіслор

Зверніть увагу, що якщо ви сьогодні використовуєте “інтерфейс ILP64” бібліотеки математичного ядра Intel, int ширина складе 32 біти. Тип MKL_INT- це те, що зміниться.
Девіслор,

1

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

Що стосується технічних причин віддати перевагу uint32_t, я не думаю, що їх існує - коли вам абсолютно потрібен точний 32-бітний беззнаковий int, тоді цей тип є вашим єдиним стандартизованим вибором. Майже у всіх інших випадках інші варіанти є технічно кращими, зокрема, uint_fast32_tякщо вас турбує швидкість і uint_least32_tякщо вас турбує простір для зберігання. Використання uint32_tв будь-якому з цих випадків ризикує неможливість скомпілювати, оскільки тип не повинен існувати.

На практиці такі uint32_tта пов'язані з ними типи існують на всіх поточних платформах, за винятком деяких дуже рідкісних (на сьогоднішній день) ЦСП або реалізацій жартів, тому фактичний ризик використання точного типу невеликий. Подібним чином, хоча ви можете натрапити на покарання за швидкість із типами фіксованої ширини, вони (на сучасному процесорі) більше не калічать.

Ось чому, я думаю, коротший тип просто виграє в більшості випадків через лінощі програмістів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.