Предполагается, что компилятор создает ассемблер (и, в конечном счете, машинный код) для некоторой машины, и, как правило, 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) Рассмотрите накладные расходы на вычисление оптимального размера хранилища и уменьшение / расширение области хранения переменной при изменении значения.