Чому типи завжди певного розміру не залежать від його значення?


149

Реалізація може відрізнятися між фактичними розмірами типів, але для більшості типів, таких як безпідписаний int та float, завжди є 4 байти. Але чому тип завжди займає певний об'єм пам'яті незалежно від його значення? Наприклад, якщо я створив таке ціле число зі значенням 255

int myInt = 255;

Тоді myIntб мій компілятор займав 4 байти. Однак фактичне значення 255може бути представлене лише 1 байтом, то чому б myIntне просто займати 1 байт пам'яті? Або більш узагальнений спосіб запитання: Чому тип має лише один розмір, пов'язаний з ним, коли простір, необхідний для представлення значення, може бути меншим за цей розмір?


15
1) " Однак фактичне значення 256 може бути представлене лише 1 байтом " Неправильно, найбільше unsingedзначення, яке можна представити 1 байтом, є 255. 2) Розглянемо накладні витрати на обчислення оптимального розміру зберігання та зменшення / розширення області зберігання змінної, оскільки значення змінюється.
Альгерд Преіджіус

99
Що ж, коли настане час прочитати значення з пам'яті, як ви пропонуєте, щоб машина визначила, скільки байтів читати? Як машина дізнається, де зупинити читання значення? Для цього знадобляться додаткові можливості. І в цілому видатки на пам'ять та продуктивність для цих додаткових засобів будуть значно вищими, ніж у випадку простого використання фіксованих 4 байтів для unsigned intзначення.
1818 року

74
Мені дуже подобається це питання. Незважаючи на те, що на це може здатися простим, я думаю, що для точного пояснення потрібно добре зрозуміти, як працюють комп’ютери та комп'ютерна архітектура. Більшість людей, ймовірно, просто сприймуть це як належне, не маючи для цього всебічного пояснення.
andreee

37
Поміркуйте, що буде, якби ви додали 1 до значення змінної, зробивши її 256, тому її потрібно було б розширити. Куди вона розширюється? Ви переміщуєте решту пам’яті, щоб зробити місце? Чи змінюється сама змінна? Якщо це так, куди він рухається і як ви знайдете вказівники, які потрібно оновити?
molbdnilo

13
@someidiot nope, ти помилився. std::vector<X>завжди має однаковий розмір, тобто sizeof(std::vector<X>)константа часу компіляції.
СергійА

Відповіді:


131

Компілятор повинен створити асемблер (і в кінцевому підсумку машинний код) для деякої машини, і загалом C ++ намагається співчувати цій машині.

Симпатизувати базовій машині означає приблизно: полегшити запис C ++ коду, який буде ефективно відображати операції, які машина може швидко виконувати. Отже, ми хочемо забезпечити швидкий та "природний" доступ до типів даних та операцій на нашій апаратній платформі.

Конкретно розглянемо конкретну архітектуру машини. Візьмемо теперішнє сімейство Intel x86.

Посібник для розробників програмного забезпечення для архітектури Intel® 64 та IA-32, том 1 ( посилання ), розділ 3.4.1, говорить:

32-бітні регістри загального призначення EAX, EBX, ECX, EDX, ESI, EDI, EBP та ESP передбачені для зберігання таких елементів:

• операнди для логічних та арифметичних операцій

• Операнди для обчислення адреси

• Показники пам’яті

Отже, ми хочемо, щоб компілятор використовував ці регістри EAX, EBX і т.д., коли він компілює просту арифметику з цілим числом C ++. Це означає, що коли я оголошуюint , це має бути щось сумісне з цими регістрами, щоб я міг їх ефективно використовувати.

Регістри завжди однакового розміру (тут, 32 біти), тому мій int змінні завжди будуть також 32 біти. Я буду використовувати один і той же макет (little-endian), щоб мені не довелося робити перетворення кожного разу, коли я завантажую змінне значення в регістр або зберігаю реєстр назад у змінну.

Використовуючи godbolt, ми можемо точно побачити, що робить компілятор для якогось тривіального коду:

int square(int num) {
    return num * num;
}

компілює (з GCC 8.1 і -fomit-frame-pointer -O3для простоти):

square(int):
  imul edi, edi
  mov eax, edi
  ret

це означає:

  1. то int numпараметр був прийнятий в регістрі EDI, а це означає , що саме розмір і макет Intel очікувати рідної регістр. Функція не повинна нічого перетворювати
  2. множення - це одна інструкція (imul ), яка дуже швидко
  3. повернення результату - це просто питання його копіювання до іншого реєстру (абонент очікує, що результат буде поміщений в EAX)

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

Знову використовуючи Godbolt , ми можемо порівняти просте нативне множення

unsigned mult (unsigned x, unsigned y)
{
    return x*y;
}

mult(unsigned int, unsigned int):
  mov eax, edi
  imul eax, esi
  ret

з еквівалентним кодом нестандартної ширини

struct pair {
    unsigned x : 31;
    unsigned y : 31;
};

unsigned mult (pair p)
{
    return p.x*p.y;
}

mult(pair):
  mov eax, edi
  shr rdi, 32
  and eax, 2147483647
  and edi, 2147483647
  imul eax, edi
  ret

Усі додаткові вказівки стосуються перетворення вхідного формату (два 31-бітні цілі числа) у формат, з яким процесор може керувати власним чином. Якби ми хотіли зберегти результат назад у 31-бітове значення, було б ще одна-дві вказівки для цього.

Ця додаткова складність означає, що ви будете турбуватися з цим лише тоді, коли економія місця дуже важлива. У цьому випадку ми зберігаємо лише два біти порівняно з використанням нативного unsignedабо uint32_tтипу, що створило б набагато простіший код.


Примітка про динамічні розміри:

Наведений вище приклад - це все-таки значення фіксованої ширини, а не змінної ширини, але ширина (і вирівнювання) більше не відповідають власним регістрам.

Платформа x86 має кілька нативних розмірів, включаючи 8-бітний та 16-бітний на додаток до основного 32-бітного (я глянцюю над 64-бітовим режимом та різними іншими речами для простоти).

Ці типи (char, int8_t, uint8_t, int16_t тощо) також безпосередньо підтримуються архітектурою - частково для зворотної сумісності зі старими 8086/286/386 / тощо. і т.д. інструкції.

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

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


Подальша примітка щодо ефективності

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

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

Штраф за швидкість, який ви платите за це, означає, що це варто лише тоді, коли вам потрібно мінімізувати пропускну здатність або довготривале зберігання, а в таких випадках зазвичай простіше використовувати простий і природний формат - а потім просто стиснути його загальною системою загального призначення (наприклад, zip, gzip, bzip2, xy чи будь-що інше).


тл; д-р

Кожна платформа має одну архітектуру, але ви можете розробити по суті необмежену кількість різних способів представлення даних. Для будь-якої мови недоцільно надавати необмежену кількість вбудованих типів даних. Таким чином, C ++ забезпечує неявний доступ до нативного, природного набору даних платформи, і дозволяє самостійно кодувати будь-яке інше (неноземне) представлення.


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

7
@asd, але скільки реєстрів ви будете використовувати в коді, який визначає, скільки змінних зараз зберігається в реєстрі?
користувач253751

1
FWIW прийнято пакувати декілька значень у найменший доступний простір, де ви вирішите, що економія місця важливіша, ніж швидкість витрат на упаковку та розпакування. Ви просто не можете нормально працювати з ними в упакованому вигляді, оскільки процесор не знає, як правильно робити арифметику нічим іншим, крім вбудованих регістрів. Шукайте частковий виняток BCD з підтримкою процесора
Марно

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

1
+1. Примітка про те, що "простий і природний формат, а потім стиснення", як правило, краще: Це, звичайно, правда , але : для деяких даних VLQ-кожне значення-потім-стиснення-все-цілое спрацьовує помітно краще, ніж просто стиснення - - Що-небудь, а для деяких додатків ваші дані не можуть стискатися разом , оскільки вони або розрізнені (як у gitметаданих 's'), або ви насправді зберігаєте їх у пам'яті, час від часу потрібно випадково отримувати доступ або змінювати декілька, але не більшість значення (як у двигунах візуалізації HTML + CSS), і таким чином можна збивати лише за допомогою чогось на зразок VLQ на місці.
mtraceur

139

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

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

Однак, навіть якщо одна людина живе в будинку, в якому може розміститися 10 осіб, розмір будинку не вплине на нинішню кількість мешканців.


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

5
"Оскільки типи принципово представляють сховище", це стосується не всіх мов (наприклад,
typecript

56
@ corvus_192 теги мають значення. Це питання позначене
темою

4
@ ahouse101 Дійсно, існує декілька мов, які мають цілі числа з необмеженою точністю, вони зростають у міру необхідності. Ці мови не вимагають виділяти фіксовану пам'ять для змінних, вони внутрішньо реалізовані як посилання на об'єкти. Приклади: Lisp, Python.
Бармар

2
@jamesqf Мабуть, це не випадковість того, що арифметика MP була вперше прийнята в Ліспі, що також зробило автоматичне управління пам'яттю. Дизайнери вважали, що ефективність роботи є другорядним порівняно з простотою програмування. І були розроблені методи оптимізації для мінімізації впливу.
Бармар

44

Це оптимізація та спрощення.

Ви можете мати об'єкти фіксованого розміру. Таким чином зберігаючи значення.
Або ви можете мати об'єкти змінного розміру. Але зберігання вартості та розміру.

об'єкти фіксованого розміру

Код, який маніпулює числом, не повинен турбуватися про розмір. Ви припускаєте, що завжди використовуєте 4 байти, а код дуже простий.

Об'єкти динамічного розміру

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

Якщо поверніть це значення в пам'ять, якщо значення не перевищило його поточного розміру, просто поверніть це значення в пам'ять. Але якщо значення зменшилося або зросло, вам потрібно перенести місце зберігання об’єкта в інше місце в пам'яті, щоб переконатися, що воно не переповнюється. Тепер вам слід відстежити позицію цього числа (оскільки воно може рухатися, якщо зростає занадто великим для свого розміру). Вам також потрібно відслідковувати всі невикористані мінливі місця, щоб вони могли бути потенційно використані.

Підсумок

Код, створений для об'єктів фіксованого розміру, набагато простіше.

Примітка

Стиснення використовує той факт, що 255 поміститься в один байт. Існують схеми стиснення для зберігання великих наборів даних, які активно використовуватимуть різні значення розмірів для різних чисел. Але оскільки це не живі дані, ви не маєте описаних вище складностей. Ви використовуєте менше місця для зберігання даних за рахунок стиснення / видалення даних для зберігання.


4
Це найкраща відповідь для мене: як ви відстежуєте розмір? З більшою кількістю пам'яті?
онлайн Thomas

@ThomasMoors Так, саме: з більшою кількістю пам'яті. Якщо у вас, наприклад, є динамічний масив, то деякі intзберігатимуть кількість елементів у цьому масиві. Саме intвоно знову матиме фіксований розмір.
Альфе

1
@ThomasMoors є два варіанти, які зазвичай використовуються, обидва з яких потребують додаткової пам'яті - або у вас є поле (фіксованого розміру), яке повідомляє вам, скільки даних є (наприклад, int для розміру масиву, або рядки "в стилі pascal", де перший елемент містить кількість символів), або ви можете мати ланцюжок (або більш складну структуру), де кожен елемент якось зазначає, чи є останнім, наприклад, рядки з нульовим завершенням або більшість форм пов'язаних списків.
Петріс

27

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

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

Що стосується того, чому основне обладнання комп'ютера лежить саме так: це тому, що це простіше та ефективніше для багатьох випадків (але не для всіх).

Уявіть комп’ютер як шматок стрічки:

| xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | ...

Якщо ви просто скажете комп’ютеру подивитися перший байт на стрічці, то xxяк воно знає, чи зупиняється тип там, чи переходить до наступного байта? Якщо у вас є число на кшталт 255(шістнадцятковий FF) або число на зразок 65535(шістнадцяткове FFFF), перший байт - це завжди FF.

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

Це відображають типи мов фіксованої ширини, такі як C і C ++.

Це не повинно бути таким, і більш абстрактні мови, які менше стосуються відображення до максимально ефективного коду, можуть використовувати кодування змінної ширини (також відомі як "Кількість змінної довжини" або VLQ) для числових типів.

Подальше читання: Якщо ви шукаєте «змінну величину довжиною» ви можете знайти деякі приклади того, що тип кодування є фактично ефективним і стоять додаткової логіки. Зазвичай, коли потрібно зберігати величезну кількість значень, які можуть бути десь у великому діапазоні, але більшість значень прагнуть до деякого невеликого піддіапазону.


Зауважте, що якщо компілятор може довести, що він може піти зі збереженням значення в меншій кількості простору, не порушуючи жодного коду (наприклад, це змінна, видима лише всередині однієї одиниці перекладу), і її евристика оптимізації дозволяє припустити, що це ' Будемо більш ефективними щодо цільового обладнання, цілком дозволено відповідним чином його оптимізувати та зберігати у меншій кількості простору, поки решта коду працює "так, ніби" це робило стандартна річ.

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

Тому що, якщо це не узгоджено, є таке ускладнення: що робити, якщо у мене є, int x = 255;але пізніше в коді, який я роблю x = y? Якщо intможе бути змінної ширини, компілятор повинен знати заздалегідь, щоб заздалегідь виділити максимальний обсяг простору, який йому знадобиться. Це не завжди можливо, адже що робити, якщо yаргумент передається з іншого фрагмента коду, який складається окремо?


26

Java використовує класи під назвою "BigInteger" і "BigDecimal", щоб зробити саме це, як і інтерфейс класу GM + C ++ C ++, очевидно (завдяки Digital Trauma). Ви можете легко зробити це самостійно майже будь-якою мовою, якщо хочете.

Процесори завжди мали можливість використовувати BCD (Binary Coded Decimal), який призначений для підтримки операцій будь-якої довжини (але ви, як правило, керуєте одним байтом в той час, що було б низьким за сьогоднішніми стандартами GPU.)

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

У ситуаціях масового зберігання та транспортування упаковані значення часто є ТІЛЬКИМ типом значення, яке ви використовували б. Наприклад, пакет музики / відео, який передається на ваш комп'ютер, може витратити трохи, щоб визначити, чи наступне значення - 2 байти або 4 байти як оптимізація розміру.

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


4
Радий бачити, як хтось згадує BigInteger. Справа не в тому, що це дурна думка, це просто сенс робити це для надзвичайно великої кількості.
Макс Барраклу

1
Щоб бути педантичним, ви насправді маєте на увазі дуже точні цифри :) Ну принаймні у випадку з BigDecimal ...
Білл К

2
А оскільки це позначено c ++ , його, мабуть, варто згадати інтерфейс класу GMP C ++ , який є тією ж ідеєю, що і великий Java *.
Цифрова травма

20

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

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

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

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

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


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


Я дуже сумніваюся, що таке робиться під час компіляції. Мало сенсу зберігати пам’ять компілятора таким чином, і це єдина перевага.
Bartek Banachewicz

1
Я швидше думав про такі операції, як множення змінної constexpr на звичайну змінну. Наприклад, ми маємо (теоретично) 8-байтну змінну constexpr зі значенням 56і множимо її на деяку 2-байтну змінну. У деяких архітектурах 64-розрядна операція була б важкою для обчислень, тому компілятор міг оптимізувати виконання 16-бітного множення.
NO_NAME

Деякі реалізації APL та деякі мови сімейства SNOBOL (я думаю, SPITBOL? Можливо, Icon) зробили саме це (із деталізацією): динамічно змінювали формат представлення залежно від фактичних значень. APL піде від булевого цілого до плаваючого і назад. SPITBOL пішов би від представлення стовпців булевих (8 окремих булевих масивів, що зберігаються в байтовому масиві), до цілих чисел (IIRC).
davidbak

16

Тоді myIntб мій компілятор займав 4 байти. Однак фактичне значення 255може бути представлене лише 1 байтом, то чому б myIntне просто займати 1 байт пам'яті?

Це відоме як кодування змінної довжини , визначені різні кодування, наприклад VLQ . Одним із найвідоміших, проте, мабуть, є UTF-8 : UTF-8 кодує кодові точки на змінну кількість байтів, від 1 до 4.

Або більш узагальнений спосіб запитання: Чому тип має лише один розмір, пов'язаний з ним, коли простір, необхідний для представлення значення, може бути меншим за цей розмір?

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

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

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

Який індекс байта, в якому починається 4-а кодова точка в рядку UTF-8?

Це залежить від значень попередніх кодових точок, потрібно лінійне сканування.

Напевно, існують схеми кодування змінної довжини, які кращі при випадковій адресації?

Так, але вони також складніші. Якщо є ідеальний, я його ще ніколи не бачив.

Чи дійсно має значення випадкове звернення?

О, так!

Вся справа в тому, що будь-який тип агрегату / масиву покладається на типи фіксованого розміру:

  • Доступ до 3-го поля a struct? Випадкова адресація!
  • Доступ до 3-го елемента масиву? Випадкова адресація!

Що означає, що у вас є такі компроміси:

Типи фіксованого розміру АБО Сканування лінійної пам'яті


Це не стільки проблема, скільки звучання. Ви завжди можете використовувати векторні таблиці. Є накладні витрати на пам'ять і додаткові вибори, але лінійні сканування не потрібні.
Артелій

2
@Artelius: Як кодувати векторну таблицю, коли цілі числа мають змінну ширину? Крім того, яка об'єм пам'яті векторної таблиці при кодуванні цілих чисел, які використовують в пам'яті від 1 до 4 байт?
Матьє М.

Подивіться, ви праві, у конкретному прикладі ОП, який дає вектор, використовує векторні таблиці має нульову перевагу. Замість того, щоб будувати векторну таблицю, ви також можете помістити дані в масив елементів фіксованого розміру. Однак ОП також вимагала більш загальної відповіді. У Python масив цілих чисел - це векторна таблиця цілих чисел змінного розміру! Це не тому, що вона вирішує цю проблему, а тому, що Python не знає під час компіляції, чи будуть елементами списку "Цілі", "Поплавці", "Дікти", "Строки" чи "Списки", які, звичайно, мають різний розмір.
Артелій

@Artelius: Зауважте, що в Python масив містить покажчики фіксованого розміру на елементи; це змушує O (1) дістатися до елемента ціною непрямості.
Матьє М.

16

Пам'ять комп'ютера підрозділяється на послідовно адресовані фрагменти певного розміру (часто 8 біт і називають байтами), а більшість комп'ютерів розроблені для ефективного доступу до послідовностей байтів, які мають послідовні адреси.

Якщо адреса об'єкта ніколи не змінюється протягом життя об'єкта, то код, що задається його адресою, може швидко отримати доступ до об'єкта, про який йде мова. Однак, істотним обмеженням цього підходу є те, що якщо адреса призначена для адреси X, а потім призначена інша адреса для адреси Y, яка знаходиться на відстані N байтів, то X не зможе вирости більше N байтів протягом життя Y, якщо не переміщено ні X, ні Y. Для того, щоб X рухався, необхідно було б все у Всесвіті, що містить адресу X, оновлено так, щоб відображати нову, і так само, щоб рухався Y. Хоча можна створити систему для полегшення таких оновлень (як Java, так і .NET досить добре керують нею), але набагато ефективніше працювати з об'єктами, які залишатимуться в одному місці протягом усього життя,


"X не зможе вирости більше N байтів протягом життя Y, якщо не перемістити або X, або Y. Для того, щоб X перемістився, потрібно було б оновити все у Всесвіті, що містить адресу X, щоб відображати новий, і так само, щоб Y рухався ". Це важливий момент IMO: об’єкти, які використовують лише стільки розміру, скільки їх поточне значення, потрібно буде додати тонни накладних витрат для розмірів / дозорних, переміщення пам’яті, довідкові графіки тощо. І цілком очевидно, коли хтось замислюється про те, як це коли-небудь може працювати ... але все-таки дуже варто викласти так чітко, тим більше, що так мало хто робив.
підкреслюй

@underscore_d: Такі мови, як Javascript, які створені з самого початку, щоб мати справу з об'єктами змінного розміру, можуть бути надзвичайно ефективними. З іншого боку, хоча об'єктні системи змінного розміру можна зробити простими, і це можливо зробити їх швидкими, прості реалізації є повільними, а швидкі реалізації - надзвичайно складними.
supercat

13

Коротка відповідь: Тому що стандарт C ++ так говорить.

Відповідь тривала: те, що ви можете зробити на комп’ютері, зрештою обмежується апаратним забезпеченням. Звичайно, можна кодувати ціле число у змінну кількість байтів для зберігання, але для його читання потрібні спеціальні інструкції щодо процесора, або ви можете його реалізувати в програмному забезпеченні, але тоді це буде жахливо повільно. Операції з фіксованим розміром доступні в ЦП для завантаження значень заздалегідь визначених ширин, для змінних ширин немає жодних.

Ще один момент, який слід врахувати - це те, як працює пам'ять комп’ютера. Скажімо, ваш цілочисельний тип може займати десь від 1 до 4 байтів пам’яті. Припустимо, ви зберігаєте значення 42 у ціле число: воно займає 1 байт, і ви розміщуєте його за адресою пам'яті X. Потім ви зберігаєте свою наступну змінну у місці X + 1 (я не розглядаю вирівнювання в цій точці) тощо . Пізніше ви вирішите змінити своє значення на 6424.

Але це не вкладається в один байт! Так, що ти робиш? Куди ви кладете решту? У вас уже є щось на X + 1, тому не можете розмістити його там. Десь в іншому місці? Як пізніше ви дізнаєтесь де? Пам'ять комп’ютера не підтримує семантику вставок: ви не можете просто розмістити щось у місці і відсунути все після цього, щоб звільнити місце!

Убік: те, про що ви говорите, насправді область стиснення даних. Алгоритми стиснення існують для того, щоб упакувати все чіткіше, тому принаймні деякі з них вважають, що не потрібно використовувати більше місця для вашого цілого числа, ніж потрібно. Однак стислі дані неможливо легко змінити (якщо це можливо взагалі), і вони просто стискаються кожного разу, коли ви вносите будь-які зміни до них.


11

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

Це не завжди так робиться. Розглянемо протокол Protobuf від Google. Протобуфи призначені для передачі даних дуже ефективно. Зменшення кількості переданих байтів коштує вартості додаткових інструкцій при роботі з даними. Відповідно, протобуфи використовують кодування, яке кодує цілі числа в 1, 2, 3, 4 або 5 байт, а менші цілі числа займають менше байтів. Однак, як тільки повідомлення надійде, воно розпаковується в більш традиційний цілочисельний формат фіксованого розміру, з яким простіше працювати. Лише під час передачі в мережі вони використовують таке цілочисельне ціле число змінної довжини.


11

Мені подобається аналогія будинку Сергія , але я думаю, що аналогія автомобіля була б кращою.

Уявіть різні дані як типи автомобілів, а люди як дані. Коли ми шукаємо новий автомобіль, ми вибираємо той, який найкраще відповідає нашому призначенню. Ми хочемо маленький розумний автомобіль, який може вмістити лише одну чи двох людей? Або лімузин, щоб перевозити більше людей? Обидва мають свої переваги та недоліки, такі як швидкість та пробіг газу (думаю, швидкість та використання пам'яті).

Якщо у вас є лімузин і ви їдете в самоті, він не збирається зменшуватися, щоб підходити тільки вам. Для цього вам доведеться продати машину (читайте: угоду) та придбати для себе новий менший.

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

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


10

Причин декілька. Одне - це додаткова складність для обробки чисел довільних розмірів, і це враховує ефективність, оскільки компілятор не може більше оптимізувати, виходячи з припущення, що кожен int має рівно X байт.

По-друге, такий спосіб зберігання простих типів означає, що їм потрібен додатковий байт, щоб утримувати довжину. Отже, значення 255 або менше насправді потребує двох байтів у цій новій системі, а не в одному, а в гіршому випадку зараз вам потрібно 5 байт замість 4. Це означає, що виграш у продуктивності у використанні пам'яті менше, ніж ви могли б Подумайте, а в деяких крайніх випадках насправді це може бути чиста втрата.

Третя причина полягає в тому, що пам'ять комп’ютера зазвичай адресована словами , а не байтами. (Але див. Виноску). Слова є декількома байтами, як правило, 4 в 32-бітових системах і 8 в 64-бітових системах. Зазвичай ви не можете прочитати окремий байт, ви прочитаєте слово та витягнете n-й байт із цього слова. Це означає і те, що вилучення окремих байтів із слова вимагає трохи більше зусиль, ніж просто читання всього слова, і це дуже ефективно, якщо вся пам’ять рівномірно розділена на шматки розміру слова (тобто, 4-байтні). Тому що, якщо навколо вас плавають цілі довільні розміри довільного розміру, то, можливо, одна частина цілого числа знаходиться в одному слові, а інша - у наступному слові, для отримання повного цілого числа потрібно два читання.

Виноска: Якщо точніше, поки ви зверталися до байтів, більшість систем ігнорували "нерівні" байти. Тобто, адреси 0, 1, 2 і 3 всі читають одне і те ж слово, 4, 5, 6 і 7 читають наступне слово тощо.

На безперервній ноті, це також, чому 32-бітні системи мали максимум 4 Гб пам'яті. Регістри, які використовуються для адреси місць у пам'яті, зазвичай є достатньо великими, щоб вмістити слово, тобто 4 байти, максимальне значення (2 ^ 32) -1 = 4294967295. 4294967296 байт становить 4 ГБ.


8

Є об'єкти, які в деякому сенсі мають змінний розмір, у стандартній бібліотеці C ++, наприклад std::vector. Однак усі вони динамічно виділяють додаткову пам'ять, яка їм знадобиться. Якщо ви візьмете sizeof(std::vector<int>), ви отримаєте константу, яка не має нічого спільного з пам'яттю, якою керує об'єкт, і якщо ви виділите масив або структуру, що міститьstd::vector<int> , він буде резервувати цей базовий розмір, а не ставити додатковий сховище в той самий масив або структуру . Є кілька фрагментів синтаксису С, які підтримують щось подібне, зокрема масиви та структури змінної довжини, але C ++ не вирішив їх підтримувати.

Стандарт мови визначає розмір об'єкта таким чином, щоб компілятори могли генерувати ефективний код. Наприклад, якщо intу деякій реалізації виходить довжиною 4 байти, і ви оголошуєте aвказівником на значення чи масив intзначень, а потім a[i]переводите в псевдокод, "перенаправлення адреси a + 4 × i". Це можна робити в постійний час, і це така поширена і важлива операція, що багато архітектури набору інструкцій, включаючи x86 і DEC PDP-машини, на яких C був спочатку розроблений, можуть це робити в одній машинній інструкції.

Один поширений приклад реального світу даних, що зберігаються послідовно як одиниці змінної довжини, - це рядки, кодовані як UTF-8. (Однак базовий тип рядка UTF-8 до компілятора все ще є charі має ширину 1. Це дозволяє рядки ASCII інтерпретувати як дійсні UTF-8, і багато бібліотечного коду, такого як strlen()і strncpy()продовжувати працювати.) Кодування будь-якої кодової точки UTF-8 може бути довжиною від одного до чотирьох байтів, і тому, якщо ви хочете, щоб п'ята кодова точка UTF-8 у рядку, вона може починатися десь від п'ятого байта до сімнадцятого байту даних. Єдиний спосіб знайти його - це сканувати з початку рядка і перевірити розмір кожної кодової точки. Якщо ви хочете знайти п’яту графему, вам також потрібно перевірити класи символів. Якщо ви хочете знайти мільйонний символ UTF-8 в рядку, вам доведеться запустити цю петлю мільйон разів! Якщо ви знаєте, що вам доведеться часто працювати з індексами, ви можете пройти рядок один раз і створити його індекс - або перетворити на кодування фіксованої ширини, наприклад UCS-4. Пошук мільйонного символу UCS-4 у рядку - лише питання додавання чотирьох мільйонів до адреси масиву.

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

Таким чином, можна мати змінну довжину bignums замість фіксованої ширину short int, int, long intі long long int, але це було б неефективно виділяти і використовувати їх. Крім того, всі основні процесори призначені для арифметики на регістрих фіксованої ширини, і жоден не має інструкцій, які безпосередньо працюють на якомусь bignum змінної довжини. Їх потрібно впровадити в програмне забезпечення, набагато повільніше.

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


7

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

Передусім через вимоги до вирівнювання.

Відповідно до basic.align / 1 :

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

Подумайте про будівлю, яка має багато поверхів, і кожен поверх має багато кімнат.
Кожна кімната - ваш розмір (фіксований простір), здатний вмістити N кількість людей або предметів.
Маючи заздалегідь відомий розмір приміщення, це робить структурну складову будівлі добре структурованою .

Якщо кімнати не вирівняні, каркас будівлі не буде добре структурований.


7

Це може бути менше. Розглянемо функцію:

int foo()
{
    int bar = 1;
    int baz = 42;
    return bar+baz;
}

він компілюється в код складання (g ++, x64, деталі позбавлені)

$43, %eax
ret

Тут barі в bazкінцевому підсумку використовують нульові байти для представлення.


5

то чому б мій не просто займав 1 байт пам'яті?

Тому що ти сказав йому, щоб так багато використовувати. Під час використання an unsigned intдеякі стандарти наказують, що буде використано 4 байти і що доступний діапазон для нього буде від 0 до 4 294 967 295. Якби ви користувалисяunsigned char замість цього, ви, ймовірно, використовуєте лише 1 байт, який ви шукаєте, (залежно від стандарту і C ++ зазвичай використовує ці стандарти).

Якби не ці стандарти, вам слід було б пам’ятати про це: як компілятор чи процесор повинен знати лише 1 байт замість 4? Згодом у програмі ви можете додати або помножити це значення, що вимагатиме більше місця. Щоразу, коли ви здійснюєте розподіл пам’яті, ОС повинна знаходити, відображати та надавати вам цей простір (можливо, замінюючи пам'ять і на віртуальну ОЗУ); це може зайняти багато часу. Якщо ви виділите пам'ять перед рукою, вам не доведеться чекати завершення іншого розподілу.

Щодо причини, чому ми використовуємо 8 біт на байт, ви можете поглянути на це: Яка історія, чому байти - це вісім біт?

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


5

Щось просте, на що, як видається, пропадає більшість відповідей:

тому що він відповідає дизайнерським цілям C ++.

Можливість розробити розмір типу під час компіляції дозволяє зробити компілятор та програміст величезну кількість спрощених припущень, які приносять багато користі, особливо щодо продуктивності. Звичайно, типи фіксованого розміру мають супутні підводні камені, як цілий перелив. Ось чому різні мови приймають різні дизайнерські рішення. (Наприклад, цілі числа Python по суті є змінними розмірами.)

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

То чому C вирішив в першу чергу типів фіксованого розміру? Простий. Він був розроблений для написання операційних систем 70-х років, серверного програмного забезпечення та утиліт; речі, які забезпечили інфраструктуру (наприклад, управління пам'яттю) для іншого програмного забезпечення. На такому низькому рівні продуктивність є критичною, тому компілятор робить саме те, про що ви йому говорите.


5

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

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

Ще один спосіб ви можете це зробити, зробивши кожну змінну вказівником на кучу місця, але ви витратите ще більше циклів процесора та пам'яті. Покажчики - це 4 байти (32-бітова адресація) або 8 байт (64-бітова адресація), тому ви вже використовуєте 4 або 8 для вказівника, то фактичний розмір даних у купі. У цьому випадку кошти на перерозподіл ще є витратами. Якщо вам потрібно перерозподілити кучу даних, ви можете пощастити і мати можливість розширити їх вбудовано, але іноді вам доведеться перемістити їх кудись ще в купі, щоб мати суміжний блок пам'яті потрібного вам розміру.

Завжди швидше вирішити, скільки пам'яті використовувати заздалегідь. Якщо ви можете уникнути динамічного розміру, ви отримуєте продуктивність. Витрата пам'яті, як правило, вартує збільшення продуктивності. Ось чому комп'ютери мають тонни пам’яті. :)


3

Компілятору дозволено вносити багато змін у ваш код, якщо все ще працює (правило "як є").

Можна було б використовувати 8-бітну інструкцію прямого переміщення замість довшої (32/64 біт), необхідної для переміщення повної int. Однак вам знадобиться дві інструкції для завершення завантаження, оскільки вам доведеться спочатку встановити регістр на нуль, перш ніж виконувати навантаження.

Просто ефективніше (принаймні, згідно з основними компіляторами) обробляти значення як 32-бітове. Насправді я ще не бачив компілятор x86 / x86_64, який би виконував 8-бітове завантаження без вбудованої збірки.

Однак, якщо мова йде про 64 біт, то все по-іншому. Розробляючи попередні розширення (від 16 до 32 біт) своїх процесорів, Intel допустила помилку. Ось гарне уявлення про те, як вони виглядають. Основний висновок тут полягає в тому, що коли ви пишете на AL або AH, на інше це не впливає (досить справедливо, в цьому і було сенс). Але це стає цікавим, коли вони розширили його до 32 біт. Якщо ви пишете нижній біт (AL, AH або AX), з верхніми 16 бітами EAX нічого не відбувається, це означає, що якщо ви хочете просунути charвint , вам потрібно спочатку очистити цю пам'ять, але у вас немає способу фактично використовуючи лише ці 16 кращих бітів, що робить цю "особливість" більше болем, ніж будь-чим.

Тепер з 64 бітами AMD зробив набагато кращу роботу. Якщо ви торкаєтесь чого-небудь у нижньому 32 біті, верхні 32 біти просто встановлюються на 0. Це призводить до деяких фактичних оптимізацій, які ви можете побачити на цьому бобовому болті . Ви можете бачити, що завантаження чогось з 8 біт або 32 біт виконується однаково, але коли ви використовуєте 64 бітні змінні, компілятор використовує іншу інструкцію залежно від фактичного розміру вашого буквала.

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


виправлення: неначе . Крім того, я не бачу, як, якщо можна скористатися коротшим завантаженням / зберіганням, це звільнить інші байти для використання - що, здається, те, про що задається ОП: не просто уникати торкання пам'яті, не потрібної поточному значенню, але вміючи сказати, скільки байтів читати, і магічно зміщувати всю оперативну пам’ять під час виконання, так що дотримується якась дивна філософська ідея просторової ефективності (не маючи на увазі гігантської вартості продуктивності!) ... Просто вигравши інструкції із нижньою площею 't' вирішити 'це. Що потрібно зробити процесору / ОС, це було б настільки складно, що це чітко відповідає на питання IMO.
підкреслюй_16

1
Ти не можеш реально "зберегти пам'ять" в регістрах. Якщо ви не намагаєтесь зробити щось дивне, зловживаючи AH та AL, у жодному разі ви не можете мати декілька різних значень в одному реєстрі загального призначення. Локальні змінні часто залишаються в регістрах і ніколи не переходять на оперативну пам’ять, якщо в цьому немає потреби.
meneldal
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.