Чи є хорошою практикою використання менших типів даних для змінних для збереження пам'яті?


32

Коли я вперше вивчив мову C ++, я дізнався, що крім int, float тощо, в мові існують менші або більші версії цих типів даних. Наприклад, я міг би викликати змінну x

int x;
or 
short int x;

Основна відмінність полягає в тому, що короткий int займає 2 байти пам'яті, а int - 4 байти, а короткий int має менше значення, але ми можемо також назвати це, щоб зробити його ще меншим:

int x;
short int x;
unsigned short int x;

що ще більш обмежує.

Моє запитання тут полягає в тому, чи є хорошою практикою використовувати окремі типи даних відповідно до того, які значення приймає ваша змінна в межах програми. Чи є гарною ідеєю завжди оголошувати змінні відповідно до цих типів даних?


3
чи знаєте ви макет дизайну Flyweight ? "об'єкт, який мінімізує використання пам'яті, обмінюючись якомога більше даних з іншими подібними об'єктами; це спосіб використання об'єктів у великій кількості, коли просте повторне подання використовує неприйнятний об'єм пам'яті ..."
gnat

5
За допомогою стандартних параметрів компілятора упаковки / вирівнювання змінні будуть так чи інакше вирівняні до 4-х байтових меж, тому різниця може взагалі не відрізнятися.
nikie

36
Класичний випадок передчасної оптимізації.
шарфридж

1
@nikie - вони можуть бути вирівняні на 4-байтній межі на процесорі x86, але це взагалі не відповідає дійсності. MSP430 розміщує знаки на будь-якій байт-адресі, а все інше - на рівній байтовій адресі. Я думаю, що AVR-32 і ARM Cortex-M - те саме.
uɐɪ

3
Друга частина вашого запитання передбачає, що додавання unsignedякимось чином цілим числом займає менше місця, що, звичайно, помилково. Він буде мати однакову кількість дискретних репрезентативних значень (дати або взяти 1 залежно від того, як представлений знак), але просто зміститься виключно в позитивне.
підкреслюйте_d

Відповіді:


41

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

unsigned int salary;

Ви надаєте корисну інформацію іншому розробнику: зарплата не може бути негативною.

Різниця між короткими, int, long, рідко, спричинить простір у вашій програмі. Ви, швидше за все, випадково зробите хибне припущення, що число завжди міститиметься в якомусь типі даних. Напевно, безпечніше завжди використовувати int, якщо ви не на 100% впевнені, що ваші цифри завжди будуть дуже маленькими. Навіть тоді навряд чи ви заощадите помітну кількість місця.


5
Правда, в наші дні це рідко буде створювати проблеми, але якщо ви проектуєте бібліотеку чи клас, який буде використовувати інший розробник, то це вже інша справа. Можливо, їм знадобиться сховище для мільйона цих об’єктів, і в цьому випадку різниця велика - 4 Мб порівняно з 2 МБ тільки для цього одного поля.
dodgy_coder

30
Використання unsignedв цьому випадку є поганою ідеєю: не тільки зарплата не може бути негативною, але і різниця між двома зарплатами не може бути негативною. (Взагалі, використовувати непідписане для нічого, окрім біт-твітінгу та визначену поведінку при переповненні - це погана ідея.)
zvrba

15
@zvrba: Різниця між двома зарплатами не є самою зарплатою, тому законно використовувати інший тип, який підписується.
JeremyP

12
@JeremyP Так, але якщо ви використовуєте C (і, схоже, це справедливо і в C ++), непідписане ціле віднімання призводить до непідписаного int , що не може бути негативним. Це може перетворитись на правильне значення, якщо ви передасте його до підписаного int, але результатом обчислення є непідписаний int. Дивіться також цю відповідь, щоб отримати більш підписані / неподписані підрахунки дивацтва - ось чому ви ніколи не повинні використовувати непідписані змінні, якщо ви дійсно не змітаєте біти.
Такрой

5
@zvrba: Різниця - це грошова кількість, але не зарплата. Тепер ви можете стверджувати, що заробітна плата - це також грошова кількість (обмежена позитивними цифрами і 0 шляхом перевірки вхідних даних, що робило б більшість людей), але різниця між двома зарплатами не є сама зарплата.
ДжереміP

29

ОП нічого не сказала про тип системи, для якої вони пишуть програми, але я припускаю, що ОП думав про типовий ПК з пам'яттю ГБ, оскільки згадується C ++. Як говориться в одному з коментарів, навіть при такому запам'ятовуванні, якщо у вас є кілька мільйонів елементів одного типу - наприклад, масив - то розмір змінної може змінити значення.

Якщо ви потрапляєте у світ вбудованих систем - що насправді не виходить за рамки питання, оскільки ОП не обмежує його лише ПК, то розмір типів даних дуже важливий. Я щойно закінчив швидкий проект на 8-бітовому мікроконтролері, який містить лише 8 К слів програмної пам’яті та 368 байт оперативної пам’яті. Там, очевидно, кожен байт має значення. Ніколи не використовується змінна, більша, ніж їм потрібно (і з точки зору простору, і з розміром коду - 8-бітні процесори використовують безліч інструкцій для маніпулювання 16 та 32-бітовими даними). Навіщо використовувати ЦП з такими обмеженими ресурсами? У великих кількостях вони можуть коштувати аж чверть.

Наразі я роблю ще один вбудований проект із 32-розрядним мікроконтролером на базі MIPS, який має 512 К байтів флеш-пам’яті та 128 К байт оперативної пам’яті (і коштує близько 6 доларів США). Як і для ПК, "природний" розмір даних - 32-бітний. Тепер стає більш ефективним, кодовим, використовувати ints для більшості змінних замість символів або шортів. Але ще раз, будь-який тип масиву чи структури повинен враховувати, чи є меншими типи даних гарантованими. На відміну від компіляторів для більших систем, швидше за все змінні структури будуть упаковані у вбудовану систему. Я дбаю про те, щоб завжди намагатися спочатку вставити всі 32-бітні змінні, потім 16-бітні, потім 8-бітні, щоб уникнути будь-яких «дірок».


10
+1 за те, що до вбудованих систем застосовуються різні правила. Те, що згадується C ++, не означає, що ціль - це ПК. Один з моїх останніх проектів був написаний на C ++ на процесорі з 32k оперативної пам’яті та 256K Flash.
uɐɪ

13

Відповідь залежить від вашої системи. Як правило, ось переваги та недоліки використання менших типів:

Переваги

  • Менші типи використовують менше пам'яті в більшості систем.
  • Менші типи дають швидші обчислення для деяких систем. Особливо це стосується float vs double у багатьох системах. А менші типи int також дають значно швидший код на 8- або 16-бітних процесорах.

Недоліки

  • Багато процесорів мають вимоги до вирівнювання. Деякі дані вирівнюють дані швидше, ніж несанкціоновані. Деякі повинні вирівняти дані, щоб навіть мати доступ до них. Більші цілі типи дорівнюють одній вирівняній одиниці, тому вони, швидше за все, не вирівнюються. Це означає, що компілятор може бути змушений розміщувати ваші менші цілі числа у більші. І якщо більш дрібні типи є частиною більшої структури, ви можете отримати різні байти відкладки, які мовчки вставляються де-небудь в структурі компілятором, щоб виправити вирівнювання.
  • Небезпечні неявні перетворення. C і C ++ мають декілька незрозумілих, небезпечних правил щодо того, як змінні рекламуються до більших, неявно без набору тексту. Існує два набори неявних правил перетворення, переплетених один з одним, які називаються "цілими правилами просування" та "звичайними арифметичними перетвореннями". Детальніше про них читайте тут . Ці правила є однією з найпоширеніших причин виникнення помилок на C і C ++. Ви можете уникнути безлічі проблем, просто використовуючи один і той же цілий тип у всій програмі.

Моя порада - це сподобається:

system                             int types

small/low level embedded system    stdint.h with smaller types
32-bit embedded system             stdint.h, stick to int32_t and uint32_t.
32-bit desktop system              Only use (unsigned) int and long long.
64-bit system                      Only use (unsigned) int and long long.

Крім того, ви можете використовувати int_leastn_tабо int_fastn_tвід stdint.h, де n - це число 8, 16, 32 або 64. int_leastn_tтип означає "я хочу, щоб це було принаймні n байтів, але мені байдуже, чи компілятор виділяє його як тип більшого розміру для вирівнювання ".

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

Як правило, різні типи stdint.h набагато краща практика, ніж звичайні intтощо, оскільки вони портативні. Намір з - intполягав у тому, щоб не надати йому заданої ширини виключно для того, щоб зробити її портативною. Але насправді важко перенести порт, тому що ніколи не знаєш, наскільки він буде великим у певній системі.


Місце на вирівнюванні. У моєму поточному проекті безкоштовне використання uint8_t на 16-бітному MSP430 таємничим чином розбило MCU (швидше за все, десь відбувся несогласований доступ, можливо, помилка GCC, можливо, ні) - просто заміна всіх uint8_t на "непідписаний" усунула збої. Використання 8-бітних типів на> 8-бітних арках, якщо не є фатальним, принаймні неефективне: компілятор генерує додаткові інструкції 'і reg, 0xff'. Використовуйте "int / unsigned" для портативності та звільніть компілятор від зайвих обмежень.
alexei

11

Залежно від того, як працює конкретна операційна система, ви, як правило, очікуєте, що пам'ять буде виділена неоптимізовано таким чином, що коли ви вимагаєте виділити байт або слово або якийсь інший малий тип даних, значення займає весь регістр, все це дуже власний. Як ваш компілятор чи інтерпретатор працює для тлумачення цього, проте, це щось інше, тому, якщо ви, наприклад, збирали програму в C #, значення може фізично займати реєстр для себе, однак значення буде перевірено межами, щоб гарантувати, що ви цього не зробите спробуйте зберегти значення, яке перевищить межі призначеного типу даних.

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

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

Якщо у вас є дуже суворі вимоги до даних, наприклад, написання протоколу зв’язку або якийсь алгоритм шифрування, то використання перевірених діапазонів даних може бути дуже зручним, особливо якщо ви намагаєтеся уникнути проблем, пов’язаних із перевитратами / недоїданням даних. або недійсні значення даних.

Єдина інша причина, з якої я можу подумати над головою, щоб використовувати конкретні типи даних, - це коли ви намагаєтеся повідомити про наміри у своєму коді. Якщо ви, наприклад, використовуєте короткий коментар, ви повідомляєте іншим розробникам, що ви дозволяєте отримувати додатні та негативні цифри в дуже маленькому діапазоні значень.


6

Як коментує шарфридж , це - а

Класичний випадок передчасної оптимізації .

Намагання оптимізувати використання пам'яті може вплинути на інші сфери продуктивності, а золотими правилами оптимізації є:

Перше правило оптимізації програми: не робіть цього .

Друге правило оптимізації програми (лише для експертів!): Не робіть цього ще ".

- Майкл А. Джексон

Для того, щоб знати, чи зараз настав час для оптимізації, потрібно тестування та тестування. Вам потрібно знати, де ваш код неефективний, щоб ви могли орієнтуватися на свої оптимізації.

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

Також пам’ятайте, що те, що дана реалізація є більш ефективною для поточного покоління процесорів, не означає, що це завжди буде так. Моя відповідь на питання Чи важлива мікрооптимізація при кодуванні? детально описує приклад з особистого досвіду, коли застаріла оптимізація призвела до сповільнення на порядок.

У багатьох процесорах нестандартний доступ до пам'яті значно дорожчий, ніж вирівнювання доступу до пам'яті. Упаковка кількох шортів у вашу структуру може просто означати, що ваша програма повинна виконувати операції пакування / розпакування кожен раз, коли ви торкаєтесь будь-якого значення.

З цієї причини сучасні укладачі ігнорують ваші пропозиції. Як коментарі nikie :

За допомогою стандартних параметрів компілятора упаковки / вирівнювання змінні будуть так чи інакше вирівняні до 4-х байтових меж, тому різниця може взагалі не відрізнятися.

По-друге, здогадайтеся, ваш компілятор загрожує.

Існує місце для таких оптимізацій під час роботи з терабайтними наборами даних або вбудованими мікроконтролерами, але для більшості з нас це насправді не викликає занепокоєння.


3

Основна відмінність полягає в тому, що короткий int займає 2 байти пам'яті, а int - 4 байти, а короткий int має менше значення, але ми можемо також назвати це, щоб зробити його ще меншим:

Це неправильно. Ви не можете робити припущення щодо кількості байтів, що містять кожен тип, окрім charтого, як один байт і принаймні 8 біт на байт, разом з тим, що розмір кожного типу більший або рівний попередньому.

Переваги продуктивності неймовірно незначні для змінних стеків - вони, швидше за все, будуть вирівняні / прокладені в будь-якому випадку.

Через це shortі longпрактично не користуєтесь нині, і вам майже завжди краще користуватися int.


Звичайно, є і те, stdint.hщо цілком чудово використовувати, коли intйого не вирізати. Якщо ви коли-небудь виділяєте величезні масиви цілих чисел / структур, то це intX_tмає сенс, оскільки ви можете бути ефективними та розраховувати на розмір типу. Це зовсім не передчасно, оскільки ви можете заощадити мегабайти пам'яті.


1
Насправді, з появою 64-бітового середовища, вони longможуть відрізнятися від int. Якщо ваш компілятор LP64, intмає 32 біти і longстановить 64 біти, і ви побачите, що ints може все-таки вирівняти 4 байти (наприклад, мій компілятор).
JeremyP

1
@JeremyP Так, я сказав щось інше чи щось?
Паббі

Ваше останнє речення, яке стверджує, коротке і тривале, практично не має користі. Довго, безумовно, є користь, хоча як базовий типint64_t
JeremyP

@JeremyP: Ти можеш жити чудово з int та довго.
gnasher729

@ gnasher729: Що ви використовуєте, якщо вам потрібна змінна, яка може містити значення понад 65 тисяч, але ніколи не більше мільярда? int32_t, int_fast32_tі longвсі хороші варіанти, long longце просто марнотратний і intне портативний.
Ben Voigt

3

Це буде з точки зору ООП та / або підприємства / програми та може не застосовуватися в певних областях / областях, але я хочу виховувати концепцію примітивної одержимості .

Це гарна ідея використовувати різні типи даних для різних видів інформації у вашій програмі. Однак, мабуть, НЕ БЕЗПЕЧНО використовувати для цього вбудовані типи, якщо Ви не маєте серйозних проблем з продуктивністю (які були виміряні та перевірені тощо).

Якщо ми хочемо , щоб модель температури в градусах Кельвіна в нашому додатку, ми можемо використовувати ushortабо uintчи щось подібне , щоб позначити , що «поняття негативних градусів Кельвіна є абсурдним і логічна помилка домену». Ідея, що стоїть за цим, звучить, але ти не йдеш увесь шлях. Ми зрозуміли, що ми не можемо мати негативних значень, тому це зручно, якщо ми можемо отримати компілятор, щоб переконатися, що ніхто не призначає негативне значення температурі Кельвіна. ТАКОЖ правда, що ви не можете робити побітові операції при температурі. І ви не можете додати міру ваги (кг) до температури (К). Але якщо ви будете моделювати як температуру, так і масу як uints, ми можемо зробити саме це.

Використання вбудованих типів для моделювання наших об'єктів DOMAIN неодмінно призведе до безладного коду та деяких пропущених перевірок та зламаних інваріантів. Навіть якщо тип захоплює ДЕЯКУЮ частину сутності (не може бути негативною), вона обов'язково пропускає інші (не може бути використана в довільних арифметичних виразах, не може розглядатися як масив бітів тощо).

Рішення полягає у визначенні нових типів, що інкапсулює інваріанти. Таким чином ви можете переконатися, що гроші - це гроші, а відстані - це відстані, і ви не можете їх скласти разом, і ви не можете створити негативну відстань, але МОЖЕТЕ створити негативну суму грошей (або боргу). Звичайно, ці типи будуть використовувати вбудовані типи всередині, але це приховано від клієнтів. Що стосується Вашого запитання щодо продуктивності / споживання пам’яті, подібні речі можуть дозволити вам змінити, як речі зберігаються внутрішньо, не змінюючи інтерфейс ваших функцій, що працюють на ваших доменних об'єктах, якщо ви дізнаєтесь, що чорт, а shortце занадто чорт великий.


1

Так, звісно. Це добре використовувати uint_least8_tдля словників, масивів величезних констант, буферів тощо. Краще використовуватиuint_fast8_t для цілей обробки.

uint8_least_t(зберігання) -> uint8_fast_t(обробка) ->uint8_least_t (зберігання).

Наприклад, ви берете 8-бітовий символ source, 16-бітний код dictionariesі 32-бітний constants. Чим ви обробляєте з ними 10-15 бітних операцій, і виводить 8 бітdestination .

Давайте уявимо, що ви повинні обробити 2 гігабайти source. Кількість бітових операцій величезна. Ви отримаєте великий бонус за перфоманс, якщо під час обробки перейдете на швидкі типи. Швидкі типи можуть бути різними для кожної сім'ї процесора. Ви можете включати stdint.hі використовувати uint_fast8_t, uint_fast16_t,uint_fast32_t і т.д.

Ви можете використовувати uint_least8_tзамість uint8_tпортативності. Але ніхто насправді не знає, який сучасний процесор використовуватиме цю функцію. VAC машина - музейний предмет. Тож, можливо, це надмірність.


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