Реализации могут отличаться между фактическими размерами типов, но на большинстве типов, таких как unsigned int и float, всегда 4 байта. Но почему тип всегда занимает определенный объем памяти независимо от его значения? Например, если я создал следующее целое число со значением 255
int myInt = 255;
Тогда myInt
бы занимал 4 байта моим компилятором. Однако фактическое значение 255
может быть представлено только 1 байтом, так почему бы myInt
просто не занимать 1 байт памяти? Или более обобщенный способ задать вопрос: почему с типом связан только один размер, когда пространство, необходимое для представления значения, может быть меньше этого размера?
unsinged
значение, которое может быть представлено 1 байтом255
. 2) Рассмотрите накладные расходы на вычисление оптимального размера хранилища и уменьшение / расширение области хранения переменной при изменении значения.unsigned int
значения.std::vector<X>
всегда имеет одинаковый размер, тоsizeof(std::vector<X>)
есть является константой времени компиляции.Ответы:
Предполагается, что компилятор создает ассемблер (и, в конечном счете, машинный код) для некоторой машины, и, как правило, C ++ пытается сочувствовать этой машине.
Быть сочувствующим основной машине означает грубо: облегчить написание кода на C ++, который будет эффективно отображать операции, которые машина может выполнить быстро. Итак, мы хотим предоставить доступ к типам данных и операциям, которые являются быстрыми и «естественными» на нашей аппаратной платформе.
Конкретно рассмотрим конкретную архитектуру машины. Давайте возьмем текущее семейство Intel x86.
В Руководстве разработчика программного обеспечения Intel® 64 и IA-32, том 1 ( ссылка ), раздел 3.4.1 говорится:
Итак, мы хотим, чтобы компилятор использовал эти регистры EAX, EBX и т. Д., Когда он компилирует простую целочисленную арифметику C ++. Это означает, что когда я объявляю
int
, это должно быть что-то совместимое с этими регистрами, чтобы я мог использовать их эффективно.Регистры всегда имеют одинаковый размер (здесь 32 бита), поэтому мои
int
переменные также всегда будут 32 битами. Я буду использовать ту же компоновку (little-endian), чтобы мне не приходилось выполнять преобразование каждый раз, когда я загружаю значение переменной в регистр или сохраняю регистр обратно в переменную.Используя godbolt, мы можем точно увидеть, что делает компилятор для некоторого тривиального кода:
компилирует (с GCC 8.1 и
-fomit-frame-pointer -O3
для простоты) в:это означает:
int num
параметр был принят в регистре EDI, а это означает , что именно размер и макет Intel ожидать родной регистр. Функция не должна ничего преобразовыватьimul
), которая очень быстроИзменить: мы можем добавить соответствующее сравнение, чтобы показать разницу, используя не родной макет. В простейшем случае значения хранятся в чем-то отличном от собственной ширины.
Используя Godbolt снова, мы можем сравнить простое умножение
с эквивалентным кодом для нестандартной ширины
Все дополнительные инструкции касаются преобразования входного формата (двух 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 ++ обеспечивает неявный доступ к естественному, естественному набору типов данных платформы и позволяет самостоятельно кодировать любое другое (не нативное) представление.
источник
git
метаданных), либо вы фактически храните их в памяти, иногда возникает необходимость случайного доступа или изменения нескольких, но не большей части значения (как в механизмах рендеринга HTML + CSS) и, таким образом, могут быть удалены только с помощью чего-то вроде VLQ на месте.Поскольку типы в основном представляют хранилище, и они определяются с точки зрения максимального значения, которое они могут содержать, а не текущего значения.
Очень простой аналогией может быть дом - дом имеет фиксированный размер, независимо от того, сколько людей в нем проживает, и есть также строительный кодекс, который определяет максимальное количество людей, которые могут жить в доме определенного размера.
Тем не менее, даже если в доме, который может вместить 10 человек, проживает один человек, размер дома не будет зависеть от текущего количества жильцов.
источник
Это оптимизация и упрощение.
Вы можете иметь объекты фиксированного размера. Таким образом, сохраняя стоимость.
Или вы можете иметь объекты переменного размера. Но сохраняя стоимость и размер.
объекты фиксированного размера
Код, который манипулирует числом, не должен беспокоиться о размере. Вы предполагаете, что всегда используете 4 байта и делаете код очень простым.
Объекты динамического размера
Код, которым манипулирует число, должен понимать при чтении переменной, что он должен читать значение и размер. Используйте размер, чтобы убедиться, что все старшие биты обнуляются в регистре.
Когда поместите значение обратно в память, если значение не превысило его текущий размер, просто поместите значение обратно в память. Но если значение уменьшилось или увеличилось, вам нужно переместить место хранения объекта в другое место в памяти, чтобы убедиться, что оно не переполняется. Теперь вы должны отслеживать положение этого числа (как оно может двигаться, если оно становится слишком большим для своего размера). Вам также необходимо отслеживать все неиспользуемые переменные, чтобы их можно было повторно использовать.
Резюме
Код, сгенерированный для объектов фиксированного размера, намного проще.
Заметка
Сжатие использует тот факт, что 255 уместится в один байт. Существуют схемы сжатия для хранения больших наборов данных, которые будут активно использовать разные значения размера для разных чисел. Но поскольку это не живые данные, у вас нет сложностей, описанных выше. Вы используете меньше места для хранения данных за счет сжатия / распаковки данных для хранения.
источник
int
будут хранить количество элементов в этом массиве. Этоint
само будет иметь фиксированный размер снова.Потому что в таком языке, как C ++, цель разработки состоит в том, чтобы простые операции компилировались в простые машинные инструкции.
Все основные наборы команд ЦП работают с типами фиксированной ширины , и если вы хотите использовать типы переменной ширины , вы должны выполнить несколько машинных инструкций для их обработки.
Что касается того, почему базовое компьютерное оборудование так: это потому, что оно проще и эффективнее во многих случаях (но не во всех).
Представьте компьютер как кусок ленты:
Если вы просто скажете компьютеру взглянуть на первый байт на ленте,
xx
как он узнает, останавливается ли на этом тип или нет, или переходит к следующему байту? Если у вас есть число, подобное255
(шестнадцатеричноеFF
) или число, подобное65535
(шестнадцатеричноеFFFF
), первый байт всегдаFF
.Так откуда ты знаешь? Вы должны добавить дополнительную логику и «перегрузить» значение по крайней мере одного бита или байтового значения, чтобы указать, что значение продолжается до следующего байта. Эта логика никогда не бывает «свободной», либо вы эмулируете ее в программном обеспечении, либо добавляете в процессор несколько дополнительных транзисторов для этого.
Типы языков фиксированной ширины, такие как C и C ++, отражают это.
Это не обязательно должно быть так, и более абстрактные языки, которые менее заинтересованы в отображении в максимально эффективный код, могут свободно использовать кодировки переменной ширины (также известные как «величины переменной длины» или VLQ) для числовых типов.
Дальнейшее чтение: Если вы ищете «переменную величину длиной» вы можете найти некоторые примеры того, что тип кодирования является фактически эффективным и стоят дополнительной логики. Обычно требуется хранить огромное количество значений, которые могут находиться где угодно в большом диапазоне, но большинство значений имеют тенденцию к небольшому поддиапазону.
Обратите внимание, что если компилятор может доказать, что он может избежать сохранения значения в меньшем объеме пространства, не нарушая какой-либо код (например, это переменная, видимая только внутри одного модуля перевода), и его эвристика оптимизации предполагает, что он ' Он будет более эффективным на целевом оборудовании, поэтому ему будет полностью разрешено соответствующим образом оптимизировать его и хранить его в меньшем объеме пространства, при условии, что остальная часть кода работает «как если бы» это делало стандартно.
Но когда код должен взаимодействовать с другим кодом, который может быть скомпилирован отдельно, размеры должны оставаться согласованными или гарантировать, что каждый фрагмент кода будет следовать одному и тому же соглашению.
Потому что, если это не согласуется, возникает такая сложность: что если у меня есть,
int x = 255;
но потом в коде, который я делаюx = y
? Еслиint
бы он мог иметь переменную ширину, компилятор должен был бы знать заранее, чтобы заранее выделить максимальное количество места, которое ему потребуется. Это не всегда возможно, потому что, еслиy
аргумент передается из другого фрагмента кода, который скомпилирован отдельно?источник
Java использует классы, называемые BigInteger и BigDecimal, чтобы сделать именно это, как, очевидно, и интерфейс класса C ++ GMP C ++ (спасибо Digital Trauma). Вы можете легко сделать это сами практически на любом языке, если хотите.
Процессоры всегда имели возможность использовать BCD (Binary Coded Decimal), который предназначен для поддержки операций любой длины (но вы склонны вручную обрабатывать один байт за раз, что было бы МЕДЛЕННО по сегодняшним стандартам GPU.)
Почему мы не используем эти или другие подобные решения? Производительность. Ваши наиболее высокопроизводительные языки не могут позволить себе расширять переменную в середине некоторой операции с жестким циклом - это было бы очень недетерминированным.
В ситуациях массового хранения и транспортировки упакованные значения часто являются ЕДИНСТВЕННЫМ типом значения, которое вы бы использовали. Например, музыкальный / видео пакет, передаваемый на ваш компьютер, может потратить немного времени, чтобы указать, будет ли следующее значение 2 байта или 4 байта в качестве оптимизации размера.
Как только это на вашем компьютере, где это может использоваться, хотя, память дешева, но скорость и сложность переменных с изменяемым размером не ... это действительно единственная причина.
источник
Потому что было бы очень сложно и сложно вычислить простые типы с динамическими размерами. Я не уверен, что это было бы возможно.
Компьютер должен будет проверять, сколько бит принимает число после каждого изменения его значения. Было бы довольно много дополнительных операций. И было бы намного сложнее выполнять вычисления, когда вы не знаете размеров переменных во время компиляции.
Чтобы поддерживать динамические размеры переменных, компьютер должен был бы на самом деле помнить, сколько байтов у переменной есть сейчас, что ... потребовало бы дополнительной памяти для хранения этой информации. И эта информация должна быть проанализирована перед каждой операцией над переменной, чтобы выбрать правильную инструкцию процессора.
Чтобы лучше понять, как работает компьютер и почему переменные имеют постоянные размеры, изучите основы языка ассемблера.
Хотя, я полагаю, было бы возможно достичь чего-то подобного со значениями constexpr. Однако это сделает код менее предсказуемым для программиста. Я предполагаю, что некоторые оптимизации компилятора могут сделать что-то подобное, но они скрывают это от программиста, чтобы упростить задачу.
Я описал здесь только проблемы, связанные с производительностью программы. Я опустил все проблемы, которые нужно было бы решить, чтобы сэкономить память за счет уменьшения размеров переменных. Честно говоря, я не думаю, что это даже возможно.
В заключение, использование переменных меньшего размера, чем объявленные, имеет смысл, только если их значения известны во время компиляции. Вполне вероятно, что современные компиляторы делают это. В других случаях это вызовет слишком много трудных или даже неразрешимых проблем.
источник
56
и мы умножаем ее на некоторую 2-байтовую переменную. На некоторых архитектурах 64-битная операция была бы более сложной для вычислений, поэтому компилятор мог бы оптимизировать ее для выполнения только 16-битного умножения.Это известно как кодирование переменной длины , определены различные кодировки, например, VLQ . Однако одним из самых известных является, вероятно, UTF-8 : UTF-8 кодирует кодовые точки на переменном количестве байтов, от 1 до 4.
Как всегда в инженерии, это все о компромиссах. Не существует решения, которое имеет только преимущества, поэтому вы должны сбалансировать преимущества и компромиссы при разработке вашего решения.
Дизайн, который был выбран, заключался в использовании фундаментальных типов фиксированного размера, а аппаратное обеспечение / языки просто откатились оттуда.
Итак, в чем заключается фундаментальный недостаток кодирования переменных , из-за которого он был отвергнут в пользу более требовательных к памяти схем? Нет случайной адресации .
Каков индекс байта, с которого начинается четвертая кодовая точка в строке UTF-8?
Это зависит от значений предыдущих кодовых точек, требуется линейное сканирование.
Конечно, есть схемы кодирования переменной длины, которые лучше при случайной адресации?
Да, но они также более сложны. Если есть идеальный, я еще никогда его не видел.
Случайная адресация действительно имеет значение в любом случае?
О да!
Дело в том, что любой тип агрегата / массива зависит от типов фиксированного размера:
struct
? Случайная адресация!Это означает, что у вас по существу есть следующий компромисс:
Типы фиксированного размера ИЛИ сканирование линейной памяти
источник
Память компьютера подразделяется на последовательно адресуемые фрагменты определенного размера (часто 8 бит и именуемые байтами), и большинство компьютеров предназначены для эффективного доступа к последовательностям байтов, которые имеют последовательные адреса.
Если адрес объекта никогда не изменяется в течение времени жизни объекта, то код с учетом его адреса может быстро получить доступ к рассматриваемому объекту. Однако существенным ограничением этого подхода является то, что если для адреса X назначен адрес, а затем для адреса Y назначен другой адрес, который находится на расстоянии N байтов, то X не сможет расти больше, чем N байтов в течение времени жизни. Y, если только X или Y не перемещены. Чтобы X мог двигаться, необходимо, чтобы все во вселенной, содержащей адрес X, было обновлено, чтобы отразить новый, а также для перемещения Y. Хотя можно спроектировать систему для облегчения таких обновлений (и Java, и .NET управляют ею довольно хорошо), гораздо эффективнее работать с объектами, которые будут оставаться в одном и том же месте на протяжении всей своей жизни,
источник
Короткий ответ: потому что так говорит стандарт C ++.
Длинный ответ таков: то, что вы можете сделать на компьютере, в конечном счете ограничено аппаратным обеспечением. Конечно, можно закодировать целое число в переменное число байтов для хранения, но для его чтения потребуется либо выполнение специальных инструкций ЦП, либо вы можете реализовать это в программном обеспечении, но тогда это будет ужасно медленным. Операции фиксированного размера доступны в ЦПУ для загрузки значений предопределенной ширины, для переменных ширины нет.
Еще один момент, который стоит рассмотреть, - это как работает память компьютера. Допустим, ваш целочисленный тип может занимать от 1 до 4 байт памяти. Предположим, что вы храните значение 42 в вашем целом числе: оно занимает 1 байт, и вы помещаете его в адрес памяти X. Затем вы сохраняете следующую переменную в местоположении X + 1 (я не рассматриваю выравнивание в этой точке) и так далее. , Позже вы решите изменить свое значение на 6424.
Но это не вписывается ни в один байт! Ну так что ты делаешь? Куда вы кладете отдых? У вас уже есть что-то в X + 1, поэтому вы не можете его там разместить. Где-нибудь еще? Как вы узнаете позже, где? Память компьютера не поддерживает семантику вставки: вы не можете просто поместить что-то в какое-то место и отодвинуть все после него, чтобы освободить место!
В стороне: То, о чем вы говорите, - это область сжатия данных. Существуют алгоритмы сжатия, позволяющие сжать все, поэтому, по крайней мере, некоторые из них решат не использовать больше места для целого числа, чем нужно. Однако сжатые данные нелегко изменить (если вообще возможно), и они просто сжимаются каждый раз, когда вы вносите в них какие-либо изменения.
источник
Это дает существенные преимущества производительности во время выполнения. Если бы вы работали с типами переменных размеров, вам пришлось бы декодировать каждое число перед выполнением операции (инструкции машинного кода обычно имеют фиксированную ширину), выполнить операцию, а затем найти пространство в памяти, достаточно большое для хранения результата. Это очень сложные операции. Гораздо проще просто хранить все данные немного неэффективно.
Это не всегда так, как это делается. Рассмотрим протокол Protobuf от Google. Протобуфы предназначены для очень эффективной передачи данных. Уменьшение количества передаваемых байтов стоит затрат на дополнительные инструкции при работе с данными. Соответственно, protobufs используют кодировку, которая кодирует целые числа в 1, 2, 3, 4 или 5 байтов, а меньшие целые числа занимают меньше байтов. Однако, как только сообщение получено, оно распаковывается в более традиционный целочисленный формат фиксированного размера, с которым легче работать. Только во время передачи по сети они используют такое целое число переменной длины с эффективным использованием пространства.
источник
Мне нравится домашняя аналогия Сергея , но я думаю, что автомобильная аналогия была бы лучше.
Представьте переменные типы как типы автомобилей и людей как данные. Когда мы ищем новую машину, мы выбираем ту, которая лучше всего соответствует нашим целям. Мы хотим маленькую умную машину, которая может соответствовать только одному или двум людям? Или лимузин для перевозки большего количества людей? Оба имеют свои преимущества и недостатки, такие как скорость и расход топлива (подумайте о скорости и использовании памяти).
Если у вас есть лимузин, и вы едете в одиночку, он не будет уменьшаться, чтобы соответствовать только вам. Чтобы сделать это, вам придется продать машину (читай: освободить) и купить новую поменьше для себя.
Продолжая аналогию, вы можете думать о памяти как об огромной парковке, заполненной автомобилями, и когда вы отправляетесь читать, специализированный шофер, обученный исключительно для вашего типа автомобиля, отправляется за вами. Если ваша машина может менять типы в зависимости от находящихся в ней людей, вам нужно будет брать с собой целую группу шоферов каждый раз, когда вы захотите получить свой автомобиль, поскольку они никогда не узнают, какой автомобиль будет сидеть на месте.
Другими словами, попытка определить, сколько памяти вам нужно прочитать во время выполнения, была бы крайне неэффективной и перевесила бы тот факт, что на вашей парковке вы могли бы разместить еще несколько автомобилей.
источник
Есть несколько причин. Одним из них является дополнительная сложность обработки чисел произвольного размера и снижение производительности, которое это дает, потому что компилятор больше не может оптимизировать, исходя из предположения, что каждое 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 ГБ.
источник
В стандартной библиотеке 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, для других целей. Но мы очень редко это делаем, и их нет в стандартной библиотеке.
источник
Прежде всего из-за требований выравнивания.
Согласно basic.align / 1 :
Представьте себе здание, в котором много этажей, а на каждом этаже много комнат.
Каждая комната - ваш размер (фиксированное пространство), способная вместить N людей или предметов.
Благодаря заранее известному размеру помещения структурная составляющая здания хорошо структурирована .
Если комнаты не выровнены, то каркас здания не будет хорошо структурирован.
источник
Это может быть меньше. Рассмотрим функцию:
компилируется в ассемблерный код (g ++, x64, подробности удалены)
Здесь
bar
и вbaz
конечном итоге использовать нулевые байты для представления.источник
Потому что вы сказали, чтобы использовать это много. При использовании
unsigned int
некоторых стандартов предписывается, что будут использоваться 4 байта и что доступный диапазон для него будет от 0 до 4 294 967 295. Если бы вы использовалиunsigned char
вместо этого, вы, вероятно, использовали бы только 1 байт, который вы ищете (в зависимости от стандарта, и C ++ обычно использует эти стандарты).Если бы не эти стандарты, вы должны помнить об этом: как должен знать компилятор или процессор использовать только 1 байт вместо 4? Позже в вашей программе вы можете добавить или умножить это значение, что потребует больше места. Всякий раз, когда вы делаете выделение памяти, ОС должна находить, отображать и выделять вам это пространство (возможно, также подкачку памяти в виртуальную память); это может занять много времени. Если вы выделите память заранее, вам не придется ждать, пока еще не будет выполнено другое выделение.
Что касается причины, по которой мы используем 8 бит на байт, вы можете взглянуть на это: какова история того, почему байты являются восемью битами?
Кстати, вы можете допустить переполнение целого числа; но если вы используете целое число со знаком, стандарты C \ C ++ утверждают, что переполнение целочисленного значения приводит к неопределенному поведению. Целочисленное переполнение
источник
Что-то простое, что большинство ответов, кажется, не хватает:
потому что это соответствует целям проектирования C ++.
Возможность определить размер шрифта во время компиляции позволяет компилятору и программисту сделать огромное количество упрощающих допущений, которые дают множество преимуществ, особенно в отношении производительности. Конечно, у типов фиксированного размера есть сопутствующие ловушки, такие как целочисленное переполнение. Вот почему разные языки принимают разные дизайнерские решения. (Например, целые числа Python имеют переменный размер.)
Вероятно, главная причина, по которой C ++ так сильно склоняется к типам с фиксированным размером, - это цель C-совместимости. Однако, поскольку C ++ является языком статической типизации, который пытается генерировать очень эффективный код и избегает добавления вещей, явно не указанных программистом, типы фиксированного размера все еще имеют большой смысл.
Так почему же C в первую очередь выбрал тип с фиксированным размером? Просто. Он был разработан для написания операционных систем 70-х годов, серверного программного обеспечения и утилит; вещи, которые обеспечивали инфраструктуру (такую как управление памятью) для другого программного обеспечения. На таком низком уровне производительность имеет решающее значение, и компилятор делает именно то, что вы говорите.
источник
Для изменения размера переменной потребуется перераспределение, и это обычно не стоит дополнительных циклов ЦП по сравнению с потерей еще нескольких байтов памяти.
Локальные переменные помещаются в стек, который очень быстро обрабатывается, когда эти переменные не меняются в размере. Если вы решили увеличить размер переменной с 1 байта до 2 байтов, вам нужно переместить все в стеке на один байт, чтобы освободить для этого место. Это может потенциально стоить много циклов ЦП в зависимости от того, сколько вещей нужно переместить.
Другой способ сделать это - сделать каждую переменную указателем на расположение кучи, но на самом деле вы потратите еще больше циклов ЦП и памяти. Указатели имеют длину 4 байта (32-битная адресация) или 8 байтов (64-битная адресация), поэтому вы уже используете 4 или 8 для указателя, а затем фактический размер данных в куче. В этом случае все еще стоит перераспределение. Если вам нужно перераспределить данные кучи, вам может повезти, и у вас будет место для их расширения, но иногда вам нужно переместить их куда-нибудь в кучу, чтобы получить непрерывный блок памяти нужного вам размера.
Всегда быстрее решить, сколько памяти использовать заранее. Если вы можете избежать динамических размеров, вы получите производительность. Потеря памяти обычно стоит увеличения производительности. Вот почему компьютеры имеют тонны памяти. :)
источник
Компилятору разрешено вносить много изменений в ваш код, пока все работает (правило «как есть»).
Можно было бы использовать 8-битную буквальную инструкцию перемещения вместо более длинной (32/64 бита), необходимой для перемещения целого
int
. Однако для завершения загрузки вам потребуются две инструкции, поскольку перед загрузкой необходимо сначала установить регистр в ноль.Просто более эффективно (по крайней мере, согласно основным компиляторам) обрабатывать значение как 32-битное. На самом деле, я еще не видел компилятор x86 / x86_64, который бы делал 8-битную загрузку без встроенной сборки.
Тем не менее, все обстоит иначе, когда дело доходит до 64 бит. Разрабатывая предыдущие расширения (от 16 до 32 бит) своих процессоров, Intel допустила ошибку. Вот хорошее представление о том, как они выглядят. Основной вывод здесь заключается в том, что когда вы пишете в AL или AH, другой не затрагивается (достаточно справедливо, в этом была точка, и тогда это имело смысл). Но становится интересно, когда они расширили его до 32 бит. Если вы записываете нижние биты (AL, AH или AX), с верхними 16 битами EAX ничего не происходит, что означает, что если вы хотите
char
преобразовать a в aint
, вам нужно сначала очистить эту память, но у вас нет никакого способа фактически используя только эти верхние 16 бит, делая эту «особенность» более болезненной, чем что-либо еще.Теперь с 64 битами AMD сделала намного лучшую работу. Если вы касаетесь чего-либо в нижних 32 битах, верхние 32 бита просто устанавливаются в 0. Это приводит к некоторым фактическим оптимизациям, которые вы можете увидеть в этом крестовом болте . Вы можете видеть, что загрузка чего-то из 8 или 32 бит выполняется аналогичным образом, но когда вы используете 64-битные переменные, компилятор использует другую инструкцию в зависимости от фактического размера вашего литерала.
Здесь вы можете видеть, что компиляторы могут полностью изменить фактический размер вашей переменной внутри ЦП, если это даст тот же результат, но это не имеет смысла делать это для меньших типов.
источник