Компілятор повинен створити асемблер (і в кінцевому підсумку машинний код) для деякої машини, і загалом 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
це означає:
- то
int num
параметр був прийнятий в регістрі EDI, а це означає , що саме розмір і макет Intel очікувати рідної регістр. Функція не повинна нічого перетворювати
- множення - це одна інструкція (
imul
), яка дуже швидко
- повернення результату - це просто питання його копіювання до іншого реєстру (абонент очікує, що результат буде поміщений в 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 ++ забезпечує неявний доступ до нативного, природного набору даних платформи, і дозволяє самостійно кодувати будь-яке інше (неноземне) представлення.
unsinged
значення, яке можна представити 1 байтом, є255
. 2) Розглянемо накладні витрати на обчислення оптимального розміру зберігання та зменшення / розширення області зберігання змінної, оскільки значення змінюється.