Почему типы всегда имеют определенный размер независимо от его значения?


149

Реализации могут отличаться между фактическими размерами типов, но на большинстве типов, таких как unsigned 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значения.
AnT

74
Мне очень нравится этот вопрос. Даже если на это может показаться простым, я думаю, что для точного объяснения требуется хорошее понимание того, как на самом деле работают компьютеры и компьютерные архитектуры. Большинство людей, вероятно, просто примут это как должное, не имея исчерпывающего объяснения этому.
Андре

37
Подумайте, что произойдет, если вы добавите 1 к значению переменной, сделав ее 256, поэтому ее нужно будет расширить. Куда это расширяется? Вы перемещаете остальную часть памяти, чтобы освободить место? Перемещается ли сама переменная? Если да, куда он перемещается и как вы находите указатели, которые вам нужно обновить?
molbdnilo

13
@ Someidiot Нет, вы не правы. std::vector<X>всегда имеет одинаковый размер, то sizeof(std::vector<X>)есть является константой времени компиляции.
SergeyA

Ответы:


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 бит для целого числа, а не только для большего количества переменных в регистре ? Если порядок байтов одинаков, почему это не будет оптимальным?
Николас Уден

7
@asd, но сколько регистров вы будете использовать в коде, который определяет, сколько переменных в настоящий момент хранится в регистре?
user253751

1
FWIW обычно упаковывают несколько значений в наименьшее доступное пространство, где вы решаете, что экономия места важнее, чем скорость упаковки и распаковки. Вы просто не можете работать с ними естественным образом в упакованном виде, потому что процессор не знает, как правильно делать арифметику на чем-либо, кроме встроенных регистров. Ищите BCD для частичного исключения с поддержкой процессора
Бесполезно

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

1
+1. Примечание о том, что «простой и естественный формат и затем сжатие», как правило, лучше: это, безусловно, в общем случае верно , но : для некоторых данных VLQ-каждое-значение-затем-сжатие-целая вещь работает заметно лучше, чем просто сжатие -в целом, и для некоторых приложений ваши данные не могут быть сжаты вместе , потому что они либо несопоставимы (как в gitметаданных), либо вы фактически храните их в памяти, иногда возникает необходимость случайного доступа или изменения нескольких, но не большей части значения (как в механизмах рендеринга HTML + CSS) и, таким образом, могут быть удалены только с помощью чего-то вроде VLQ на месте.
mtraceur

139

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

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

Тем не менее, даже если в доме, который может вместить 10 человек, проживает один человек, размер дома не будет зависеть от текущего количества жильцов.


31
Мне нравится аналогия. Если бы мы немного расширили его, мы могли бы представить себе использование языка программирования, который не использует фиксированные размеры памяти для типов, и это было бы похоже на разрушение комнат в нашем доме, когда они не использовались, и восстановление их при необходимости. (т. е. тонны накладных расходов, когда мы могли бы просто построить кучу домов и оставить их там, где нам нужно).
ahouse101

5
«Поскольку типы в основном представляют хранилище», это не так для всех языков (например, для машинописи)
corvus_192

56
Теги @ corvus_192 имеют значение. Этот вопрос помечен C ++, а не «машинопись»
SergeyA

4
@ ahouse101 Действительно, есть ряд языков с целочисленными значениями неограниченной точности, они растут по мере необходимости. Эти языки не требуют выделения фиксированной памяти для переменных, они внутренне реализованы как ссылки на объекты. Примеры: Лисп, Питон.
Бармар

2
@jamesqf Вероятно, это не совпадение, что арифметика MP впервые была применена в Лиспе, который также осуществлял автоматическое управление памятью. Дизайнеры чувствовали, что влияние на производительность было вторичным по отношению к простоте программирования. И методы оптимизации были разработаны, чтобы минимизировать влияние.
Бармар

44

Это оптимизация и упрощение.

Вы можете иметь объекты фиксированного размера. Таким образом, сохраняя стоимость.
Или вы можете иметь объекты переменного размера. Но сохраняя стоимость и размер.

объекты фиксированного размера

Код, который манипулирует числом, не должен беспокоиться о размере. Вы предполагаете, что всегда используете 4 байта и делаете код очень простым.

Объекты динамического размера

Код, которым манипулирует число, должен понимать при чтении переменной, что он должен читать значение и размер. Используйте размер, чтобы убедиться, что все старшие биты обнуляются в регистре.

Когда поместите значение обратно в память, если значение не превысило его текущий размер, просто поместите значение обратно в память. Но если значение уменьшилось или увеличилось, вам нужно переместить место хранения объекта в другое место в памяти, чтобы убедиться, что оно не переполняется. Теперь вы должны отслеживать положение этого числа (как оно может двигаться, если оно становится слишком большим для своего размера). Вам также необходимо отслеживать все неиспользуемые переменные, чтобы их можно было повторно использовать.

Резюме

Код, сгенерированный для объектов фиксированного размера, намного проще.

Заметка

Сжатие использует тот факт, что 255 уместится в один байт. Существуют схемы сжатия для хранения больших наборов данных, которые будут активно использовать разные значения размера для разных чисел. Но поскольку это не живые данные, у вас нет сложностей, описанных выше. Вы используете меньше места для хранения данных за счет сжатия / распаковки данных для хранения.


4
Это лучший ответ для меня: как вы отслеживаете размер? С большей памятью?
онлайн Томас

@ThomasMoors Да, именно так: с большей памятью. Если у вас, например, есть динамический массив, то некоторые intбудут хранить количество элементов в этом массиве. Это intсамо будет иметь фиксированный размер снова.
Alfe

1
@ThomasMoors обычно используются две опции, каждая из которых требует дополнительной памяти - либо у вас есть поле (фиксированного размера), сообщающее вам, сколько данных есть (например, int для размера массива, либо строки в стиле «паскаль», где первая элемент содержит количество символов) или, альтернативно, вы можете иметь цепочку (или более сложную структуру), где каждый элемент каким-то образом отмечает, является ли он последним - например, строки с нулевым символом в конце или большинство форм связанных списков.
Петерис

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, чтобы сделать именно это, как, очевидно, и интерфейс класса C ++ GMP C ++ (спасибо Digital Trauma). Вы можете легко сделать это сами практически на любом языке, если хотите.

Процессоры всегда имели возможность использовать BCD (Binary Coded Decimal), который предназначен для поддержки операций любой длины (но вы склонны вручную обрабатывать один байт за раз, что было бы МЕДЛЕННО по сегодняшним стандартам GPU.)

Почему мы не используем эти или другие подобные решения? Производительность. Ваши наиболее высокопроизводительные языки не могут позволить себе расширять переменную в середине некоторой операции с жестким циклом - это было бы очень недетерминированным.

В ситуациях массового хранения и транспортировки упакованные значения часто являются ЕДИНСТВЕННЫМ типом значения, которое вы бы использовали. Например, музыкальный / видео пакет, передаваемый на ваш компьютер, может потратить немного времени, чтобы указать, будет ли следующее значение 2 байта или 4 байта в качестве оптимизации размера.

Как только это на вашем компьютере, где это может использоваться, хотя, память дешева, но скорость и сложность переменных с изменяемым размером не ... это действительно единственная причина.


4
Рад, что кто-то упомянул BigInteger. Дело не в том, что это глупая идея, просто в том, что имеет смысл делать это только для очень больших чисел.
Макс Барракло

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

2
А поскольку он помечен как c ++ , вероятно, стоит упомянуть интерфейс класса GMP C ++ , который является той же идеей, что и Java Big *.
Цифровая травма

20

Потому что было бы очень сложно и сложно вычислить простые типы с динамическими размерами. Я не уверен, что это было бы возможно.
Компьютер должен будет проверять, сколько бит принимает число после каждого изменения его значения. Было бы довольно много дополнительных операций. И было бы намного сложнее выполнять вычисления, когда вы не знаете размеров переменных во время компиляции.

Чтобы поддерживать динамические размеры переменных, компьютер должен был бы на самом деле помнить, сколько байтов у переменной есть сейчас, что ... потребовало бы дополнительной памяти для хранения этой информации. И эта информация должна быть проанализирована перед каждой операцией над переменной, чтобы выбрать правильную инструкцию процессора.

Чтобы лучше понять, как работает компьютер и почему переменные имеют постоянные размеры, изучите основы языка ассемблера.

Хотя, я полагаю, было бы возможно достичь чего-то подобного со значениями constexpr. Однако это сделает код менее предсказуемым для программиста. Я предполагаю, что некоторые оптимизации компилятора могут сделать что-то подобное, но они скрывают это от программиста, чтобы упростить задачу.

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


В заключение, использование переменных меньшего размера, чем объявленные, имеет смысл, только если их значения известны во время компиляции. Вполне вероятно, что современные компиляторы делают это. В других случаях это вызовет слишком много трудных или даже неразрешимых проблем.


Я очень сомневаюсь, что такая вещь делается во время компиляции. Нет особого смысла сохранять память компилятора таким образом, и это единственное преимущество.
Бартек Банахевич

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

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

16

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

Это известно как кодирование переменной длины , определены различные кодировки, например, VLQ . Однако одним из самых известных является, вероятно, UTF-8 : UTF-8 кодирует кодовые точки на переменном количестве байтов, от 1 до 4.

Или более обобщенный способ задать вопрос: почему с типом связан только один размер, когда пространство, необходимое для представления значения, может быть меньше этого размера?

Как всегда в инженерии, это все о компромиссах. Не существует решения, которое имеет только преимущества, поэтому вы должны сбалансировать преимущества и компромиссы при разработке вашего решения.

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

Итак, в чем заключается фундаментальный недостаток кодирования переменных , из-за которого он был отвергнут в пользу более требовательных к памяти схем? Нет случайной адресации .

Каков индекс байта, с которого начинается четвертая кодовая точка в строке UTF-8?

Это зависит от значений предыдущих кодовых точек, требуется линейное сканирование.

Конечно, есть схемы кодирования переменной длины, которые лучше при случайной адресации?

Да, но они также более сложны. Если есть идеальный, я еще никогда его не видел.

Случайная адресация действительно имеет значение в любом случае?

О да!

Дело в том, что любой тип агрегата / массива зависит от типов фиксированного размера:

  • Доступ к 3-му полю struct? Случайная адресация!
  • Доступ к 3-му элементу массива? Случайная адресация!

Это означает, что у вас по существу есть следующий компромисс:

Типы фиксированного размера ИЛИ сканирование линейной памяти


Это не такая большая проблема, как кажется. Вы всегда можете использовать векторные таблицы. Это накладные расходы памяти и дополнительная выборка, но линейное сканирование не требуется.
Артелий

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

Послушайте, вы правы, в конкретном примере, который дал ОП, использование векторных таблиц имеет нулевое преимущество. Вместо построения таблицы векторов вы можете также поместить данные в массив элементов фиксированного размера. Однако ФП также запросил более общий ответ. В Python массив целых чисел является векторной таблицей целых чисел переменного размера! Это не потому, что это решает эту проблему, а потому, что Python не знает во время компиляции, будут ли элементы списка целыми числами, числами с плавающей точкой, Dicts, Strings или Lists, которые, конечно, имеют разные размеры.
Артелиус

@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

@underscore_d: языки наподобие Javascript, разработанные с нуля для работы с объектами переменного размера, могут быть удивительно эффективными. С другой стороны, хотя возможно сделать объектные системы переменного размера простыми и сделать их быстрыми, простые реализации медленные, а быстрые реализации чрезвычайно сложны.
суперкат

13

Короткий ответ: потому что так говорит стандарт C ++.

Длинный ответ таков: то, что вы можете сделать на компьютере, в конечном счете ограничено аппаратным обеспечением. Конечно, можно закодировать целое число в переменное число байтов для хранения, но для его чтения потребуется либо выполнение специальных инструкций ЦП, либо вы можете реализовать это в программном обеспечении, но тогда это будет ужасно медленным. Операции фиксированного размера доступны в ЦПУ для загрузки значений предопределенной ширины, для переменных ширины нет.

Еще один момент, который стоит рассмотреть, - это как работает память компьютера. Допустим, ваш целочисленный тип может занимать от 1 до 4 байт памяти. Предположим, что вы храните значение 42 в вашем целом числе: оно занимает 1 байт, и вы помещаете его в адрес памяти X. Затем вы сохраняете следующую переменную в местоположении X + 1 (я не рассматриваю выравнивание в этой точке) и так далее. , Позже вы решите изменить свое значение на 6424.

Но это не вписывается ни в один байт! Ну так что ты делаешь? Куда вы кладете отдых? У вас уже есть что-то в X + 1, поэтому вы не можете его там разместить. Где-нибудь еще? Как вы узнаете позже, где? Память компьютера не поддерживает семантику вставки: вы не можете просто поместить что-то в какое-то место и отодвинуть все после него, чтобы освободить место!

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


11

Это дает существенные преимущества производительности во время выполнения. Если бы вы работали с типами переменных размеров, вам пришлось бы декодировать каждое число перед выполнением операции (инструкции машинного кода обычно имеют фиксированную ширину), выполнить операцию, а затем найти пространство в памяти, достаточно большое для хранения результата. Это очень сложные операции. Гораздо проще просто хранить все данные немного неэффективно.

Это не всегда так, как это делается. Рассмотрим протокол Protobuf от Google. Протобуфы предназначены для очень эффективной передачи данных. Уменьшение количества передаваемых байтов стоит затрат на дополнительные инструкции при работе с данными. Соответственно, protobufs используют кодировку, которая кодирует целые числа в 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, которые поддерживают что-то вроде этого, особенно массивы и структуры переменной длины, но 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, но это было бы неэффективно выделять и использовать их. Кроме того, все основные процессоры предназначены для выполнения арифметических операций с регистрами фиксированной ширины, и ни один из них не имеет инструкций, которые непосредственно работают с каким-либо типом переменной длины. Это должно быть реализовано в программном обеспечении, гораздо медленнее.

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

так почему бы myInt не просто занять 1 байт памяти?

Потому что вы сказали, чтобы использовать это много. При использовании 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преобразовать a в a int, вам нужно сначала очистить эту память, но у вас нет никакого способа фактически используя только эти верхние 16 бит, делая эту «особенность» более болезненной, чем что-либо еще.

Теперь с 64 битами AMD сделала намного лучшую работу. Если вы касаетесь чего-либо в нижних 32 битах, верхние 32 бита просто устанавливаются в 0. Это приводит к некоторым фактическим оптимизациям, которые вы можете увидеть в этом крестовом болте . Вы можете видеть, что загрузка чего-то из 8 или 32 бит выполняется аналогичным образом, но когда вы используете 64-битные переменные, компилятор использует другую инструкцию в зависимости от фактического размера вашего литерала.

Здесь вы можете видеть, что компиляторы могут полностью изменить фактический размер вашей переменной внутри ЦП, если это даст тот же результат, но это не имеет смысла делать это для меньших типов.


исправление: как будто . Кроме того, я не вижу, как, если бы можно было использовать более короткую загрузку / сохранение, это освободило бы другие байты для использования - что, по-видимому, и вызывает интерес у OP: не просто избежать касания памяти, не необходимой для текущего значения, но возможность сказать, сколько байтов нужно прочитать, и волшебным образом сдвинуть всю оперативную память во время выполнения, чтобы была достигнута какая-то странная философская идея эффективности использования пространства (не говоря уже о гигантских затратах на производительность!) ... Просто победив инструкции меньшего размера 't' решить 'это. То, что ЦП / ОС потребуется для этого, будет настолько сложным, что он наиболее четко отвечает на вопрос IMO.
underscore_d

1
Вы не можете действительно "сохранить память" в регистрах, хотя. Если вы не пытаетесь сделать что-то странное, используя AH и AL, у вас все равно не будет нескольких разных значений в одном и том же регистре общего назначения. Локальные переменные часто остаются в регистрах и никогда не попадают в ОЗУ, если в этом нет необходимости.
Менелдал
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.