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

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

int myInt = 255;

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

Ответ 1

Предполагается, что компилятор должен создать ассемблер (и, в конечном счете, машинный код) для какой-либо машины, и, как правило, C++ пытается проявить симпатию к этой машине.

Сочувствие базовой машине означает грубо: упростить запись кода C++, который будет эффективно отображаться на операциях, которые машина может выполнить быстро. Таким образом, мы хотим обеспечить доступ к типам данных и операциям, которые бывают быстрыми и "естественными" на нашей аппаратной платформе.

Конкретно рассмотрим конкретную архитектуру машины. Возьмем текущее семейство Intel x86.

Руководство разработчика программного обеспечения Intel® 64 и IA-32 vol 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++ обеспечивает неявный доступ к родному, естественному набору типов данных платформы и позволяет вам закодировать любое другое (неместное) представление самостоятельно.

Ответ 2

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

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

Однако, даже если один человек живет в доме, который вмещает 10 человек, размер дома не будет затронут нынешним числом жителей.

Ответ 3

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

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

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

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

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

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

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

Резюме

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

Заметка

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

Ответ 4

Потому что на языке, подобном C++, целью дизайна является то, что простые операции сводятся к простым машинным инструкциям.

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

Что касается того, почему базовое компьютерное оборудование - это так: это потому, что оно проще и эффективнее для многих случаев (но не для всех).

Представьте себе компьютер как кусок ленты:

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

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

Так откуда вы знаете? Вы должны добавить дополнительную логику и "перегрузить" значение по меньшей мере одного битового или байтового значения, чтобы указать, что значение продолжается до следующего байта. Эта логика никогда не бывает "свободной", либо вы эмулируете ее в программном обеспечении, либо добавляете кучу дополнительных транзисторов в CPU для этого.

Это отражают типы фиксированной ширины таких языков, как C и C++.

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

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


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

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

Потому что если это не согласовано, то это осложнение: что, если у меня есть int x = 255; но потом в коде я делаю x = y? Если int может быть переменной шириной, компилятор должен заранее знать, чтобы предварительно выделить максимальный объем пространства, в котором он понадобится. Это не всегда возможно, потому что, если y является аргументом, переданным из другого фрагмента кода, который компилируется отдельно?

Ответ 5

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

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

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

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

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

Ответ 6

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

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

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

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

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


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

Ответ 7

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

Если адрес объекта никогда не изменяется в течение жизни объекта, то код, заданный его адресом, может быстро получить доступ к рассматриваемому объекту. Существенное ограничение в этом подходе, однако, состоит в том, что если для адреса X назначен адрес, а затем для адреса Y, который равен N байтам, назначается другой адрес, тогда X не сможет расти больше, чем N байтов в течение жизни от Y, если не перемещены ни X, ни Y. Для того, чтобы X двигался, необходимо, чтобы все в юниверсе, содержащем X-адрес, обновлялось, чтобы отразить новое, а также для того, чтобы Y двигался. Хотя возможно разработать систему для облегчения таких обновлений (как для Java, так и для.NET), гораздо эффективнее работать с объектами, которые будут оставаться в одном месте на протяжении всей своей жизни, что, в свою очередь, обычно требует, чтобы их размер Остаются неизменными.

Ответ 8

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

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

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

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

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

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

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

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

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

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

Действительно ли случайное обращение действительно имеет значение?

О да!

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

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

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

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

Ответ 9

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

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

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

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

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

Ответ 10

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

Это не всегда так, как это делается. Рассмотрите протокол Google Protobuf. Protobufs предназначены для передачи данных очень эффективно. Уменьшение количества переданных байтов стоит затрат дополнительных инструкций при работе с данными. Соответственно, protobuf используют кодировку, которая кодирует целые числа в 1, 2, 3, 4 или 5 байтах, а меньшие целые числа занимают меньше байтов. Однако, как только сообщение получено, оно распаковывается в более традиционный формат целочисленного размера, который легче работать. Только во время сетевой передачи они используют такое пространственно эффективное целое число переменной длины.

Ответ 11

Мне нравится аналогия с Сергеем, но я думаю, что аналогий с автомобилем было бы лучше.

Представьте переменные типы как типы автомобилей и людей как данные. Когда мы ищем новый автомобиль, мы выбираем тот, который лучше всего подходит для нашей цели. Мы хотим небольшой умный автомобиль, который может поместиться только одному или двум людям? Или лимузин для перевозки большего количества людей? Оба имеют свои преимущества и недостатки, такие как скорость и расход газа (думаю, скорость и использование памяти).

Если у вас есть лимузин, и вы едете один, это не будет сокращаться, чтобы соответствовать только вам. Для этого вам нужно будет продать автомобиль (прочитайте: deallocate) и купите новый маленький для себя.

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

Другими словами, пытаясь определить, сколько памяти вам нужно прочитать во время выполнения, было бы крайне неэффективно и перевешивало бы тот факт, что вы могли бы поместиться еще несколько автомобилей на своей стоянке.

Ответ 12

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

Второй заключается в том, что хранение простых типов таким образом означает, что для хранения длины требуется дополнительный байт. Таким образом, значение 255 или меньше фактически нуждается в двух байтах в этой новой системе, а не в одном, а в худшем случае вам нужно 5 байт вместо 4. Это означает, что выигрыш в производительности по используемой памяти меньше, чем вы могли бы думаю, и в некоторых случаях край может фактически быть чистым убытком.

Третья причина заключается в том, что компьютерная память обычно адресуется словами, а не байтами. (Но см. Сноску). Слова кратны байтам, обычно 4 на 32-битных системах и 8 на 64-битных системах. Обычно вы не можете прочитать отдельный байт, вы читаете слово и извлекаете n-й байт из этого слова. Это означает, что извлечение отдельных байтов из слова требует немного больше усилий, чем просто чтение всего слова, и что он очень эффективен, если вся память разделена на части размером (размером 4 байта). Поскольку, если у вас есть произвольные размерные числа, плавающие вокруг, у вас может получиться одна часть целого числа, находящаяся в одном слове, а другое в следующем слове, что требует двух чтений, чтобы получить полное целое число.

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

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

Ответ 13

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

В реальном мире большинство (но не все) программисты решили, что преимущества кодирования UTF-8, особенно совместимость, важны, и что мы так редко заботимся о чем-либо, кроме сканирования строки спереди назад и копирования блоков Память, что недостатки переменной ширины приемлемы. Мы могли бы использовать упакованные элементы переменной ширины, аналогичные UTF-8, для других целей. Но мы очень редко делаем, и их нет в стандартной библиотеке.

Ответ 14

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

В первую очередь из-за требований к выравниванию.

Согласно basic.align/1:

Типы объектов имеют требования к выравниванию, которые ограничивают адреса, по которым может быть выделен объект этого типа.

Подумайте о здании, которое имеет много этажей, и на каждом этаже есть много комнат.
Каждая комната - это ваш размер (фиксированное пространство), способный удерживать N количество людей или объектов.
Имея размер комнаты, известный заранее, он делает структурный компонент здания хорошо структурированным.

Если номера не выровнены, то скелет здания не будет хорошо структурирован.

Ответ 15

Это может быть меньше. Рассмотрим функцию:

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

он компилирует код сборки (g++, x64, детали лишены)

$43, %eax
ret

Здесь bar и baz заканчиваются использованием нулевых байтов для представления.

Ответ 16

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

Потому что вы сказали, что так много использовать. При использовании unsigned int некоторые стандарты определяют, что будут использоваться 4 байта и что доступный диапазон для него будет от 0 до 4 294 967 295. Если бы вы использовали вместо unsigned char, вы, вероятно, использовали бы только 1 байт, который вы ищете (в зависимости от стандарта и C++ обычно используют эти стандарты).

Если бы не эти стандарты, вам нужно было помнить об этом: как компилятор или процессор должны знать, чтобы использовать только 1 байт вместо 4? Позже в вашей программе вы можете добавить или умножить это значение, для чего потребуется больше места. Всякий раз, когда вы производите выделение памяти, ОС должна находить, сопоставлять и предоставлять вам это пространство (возможно, также заменяя память на виртуальную RAM); это может занять много времени. Если вы выделите память перед рукой, вам не придется ждать, пока будет выполнено другое распределение.

Что касается причины, по которой мы используем 8 бит на байт, вы можете посмотреть на это: Какова история того, почему байты составляют восемь бит?

На боковой ноте вы можете разрешить переполнение целого числа; но если вы используете целое число со знаком, стандарты C\C++ указывают, что целочисленные переполнения приводят к неопределенному поведению. Целочисленное переполнение

Ответ 17

Что-то простое, что большинство ответов, похоже, не хватает:

потому что это соответствует целям дизайна C++.

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

Вероятно, основная причина C++ настолько сильно зависит от типа фиксированного размера, что его цель - совместимость с C. Однако, поскольку C++ является статически типизированным языком, который пытается создать очень эффективный код и избегает добавлять вещи, явно не указанные программистом, типы фиксированного размера все еще имеют большой смысл.

Итак, почему C в первую очередь выбрал типы фиксированного размера? Просто. Он был разработан для написания операционных систем эпохи 70-х годов, серверного программного обеспечения и утилит; что обеспечивало инфраструктуру (например, управление памятью) для другого программного обеспечения. На таком низком уровне производительность очень важна, и компилятор делает именно то, что вы ему рассказываете.

Ответ 18

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

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

Другой способ, которым вы могли бы это сделать, - сделать каждую переменную указателем на место кучи, но на самом деле вы бы потратили еще больше циклов процессора и памяти. Указатели - 4 байта (32-разрядная адресация) или 8 байтов (64-разрядная адресация), поэтому вы уже используете 4 или 8 для указателя, а затем фактический размер данных в куче. В этом случае все еще стоит перераспределение. Если вам нужно перераспределить данные кучи, вам может повезти, и у вас будет достаточно места для его расширения, но иногда вам нужно переместить его в другое место в кучу, чтобы иметь непрерывный блок памяти того размера, который вы хотите.

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

Ответ 19

Компилятор может внести много изменений в ваш код, если все еще работает (правило "как есть").

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

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