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

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

int myInt = 255;

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

1) "Однако фактическое значение 256 может быть представлено только 1 байтом." Неправильно, наибольшее значение unsinged, которое может быть представлено 1 байтом, - это 255. 2) Учитывайте накладные расходы на вычисление оптимального размера хранилища и сжатие / расширение области хранения переменной при изменении значения.

Algirdas Preidžius 12.06.2018 16:19

Что ж, когда приходит время читать значения из памяти, как вы предлагаете машине определять, сколько байтов нужно прочитать? Как машина узнает, где прекратить считывание значения? Для этого потребуются дополнительные помещения. И в общем случае накладные расходы на память и производительность для этих дополнительных возможностей будут намного выше, чем в случае простого использования фиксированных 4 байтов для значения unsigned int.

AnT 12.06.2018 16:19
Почему с типом связан только один размер, когда пространство, необходимое для представления значения, может быть меньше этого размера? Потому что не всегда может быть меньше.
Mike Harris 12.06.2018 16:20

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

andreee 12.06.2018 16:26

К вашему сведению - в Ubuntu 17.10 sizeof (std :: string) сообщает о 32 байтах, используемых в автоматической памяти, независимо от того, сколько в ней символов. (Все символы данных отсутствуют в динамической памяти!) Но это деталь реализации. Подобные детали существуют для std :: vector и многих других контейнеров.

2785528 12.06.2018 16:30

@ AlgirdasPreidžius 1) Ах да, я имел в виду, что 1 байт может представлять 256 различных значений. Позвольте мне отредактировать вопрос, чтобы быть более точным. 2) Понятно, но вы также можете сэкономить немного памяти, поэтому минусы и плюсы динамических размеров могут быть эквивалентны плюсам и минусам статических размеров. Таким образом, тип хранилища будет зависеть от ситуации, в которой одно предпочтительнее другого.

Nichlas Uden 12.06.2018 16:54

@asd 1) Хранение - это только одна сторона уравнения. Другое дело - скорость вычислений. В типичном случае скорость вычислений важнее места для хранения. Так зачем платить за то, что ему не нужно? 2) Типы char, short и т. д. Существуют по одной причине: если вы знаете, что числа вы оперируете с достаточно маленькими числами, вы можете использовать меньший тип данных. 3) Рассмотрите возможность прочтения других комментариев / ответов. В типичном случае: это просто не стоит хлопот.

Algirdas Preidžius 12.06.2018 17:00

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

molbdnilo 12.06.2018 18:55

Типы вообще не имеют постоянного размера. int, float и т. д. имеют. многие другие имеют постоянный размер в C++, в отличие от некоторых других языков, по соображениям производительности. Другие типы имеют переменный размер, даже в C++, потому что он им нужен, например: std :: vector

some idiot 12.06.2018 20:13

@someidiot нет, ты ошибаешься. std::vector<X> всегда имеет одинаковый размер, то есть sizeof(std::vector<X>) - это постоянная времени компиляции.

SergeyA 12.06.2018 20:32

Я пропустил объяснение: он хранится как 4 байта, потому что "int" дал явный приказ сделать это.

Droidum 12.06.2018 21:39
Варианты буфера протокола - это пример реализации количество переменной длины, где "Меньшие числа занимают меньшее количество байтов.", как вы его описываете.
null 13.06.2018 00:26

Если вы покупаете восьмизначный калькулятор, станет ли он трехзначным, если вы введете значение 255? Я сомневаюсь в этом.

Jamie Hanrahan 13.06.2018 03:36

@SergeyA Я не согласен. Очевидно, что sizeof(std::vector<X>)является - это постоянная времени компиляции, но это потому, что sizeof неточно сообщает вам объем памяти, который занимает тип. Что больше похож на sizeof(vec) + vec.capacity()*(sizeof(vec.front())) + vec.capacity() ? dynamic_memory_overhead : 0

Martin Bonner supports Monica 13.06.2018 10:47

@MartinBonner вы можете не соглашаться, как хотите, но в терминах C++ размер типа - это значение, возвращаемое оператором sizeof. Это определение из Стандарта.

SergeyA 13.06.2018 15:34

255, и вы хотите использовать для него 2 полубайта. Хорошо, я это вижу. Сколько вы хотите использовать для 9? Сколько за ноль?

jmoreno 14.06.2018 12:12

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

Wayne Conrad 14.06.2018 23:51

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

EJoshuaS - Reinstate Monica 15.06.2018 00:11

Просто чтобы подавить любые возможные споры ... и SergeyA, и Мартин Боннер правы. std:vector<T> инкапсулирует динамически распределенный массив, такой как сгенерированный new T[N], обычно путем сохранения дескриптора указанного массива. Таким образом, размер std::vector постоянен и точно измеряется с помощью sizeof. Однако, поскольку фактическое хранилище данных, которым управляет vector, не находится внутри самого vector, это не отразится на результате sizeof.

Justin Time - Reinstate Monica 15.06.2018 07:16

Даже если вы можете хранить данные в 8 битах, в большинстве систем у вас нет возможности читать данные по 8 бит за раз, поскольку процессоры обычно имеют шину данных фиксированной ширины (например, 32 бита). В итоге вы прочитаете 32 бита из хранилища и просто «проигнорируете» 24 бита в своей схеме, что сделает всю «оптимизацию» бессмысленной.

Masked Man 17.06.2018 08:17
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
150
20
11 729
18

Ответы 18

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

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

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

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

ahouse101 12.06.2018 19:00
«Потому что типы в основном представляют собой хранилище» это не верно для всех языков (например, машинописного текста)
corvus_192 12.06.2018 19:59

Теги @ corvus_192 имеют значение. Этот вопрос помечен тегом C++, а не машинописным текстом.

SergeyA 12.06.2018 20:01

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

Barmar 12.06.2018 22:40

@Barmar: Но, по крайней мере, по моему опыту, арифметика с неограниченной точностью гораздо менее эффективна с точки зрения вычислений, чем использование собственных типов машины. Поэтому, если вам нужна арифметика произвольной точности для конкретной задачи, вы вызываете подходящую библиотеку: en.wikipedia.org/wiki/…

jamesqf 13.06.2018 05:56

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

Barmar 13.06.2018 17:31

@Barmar: «Легкость программирования» и «Лисп»? Из многих языков, с которыми мне приходилось сталкиваться за свою карьеру, это единственный язык, на котором мне не удавалось написать настоящую рабочую программу.

jamesqf 13.06.2018 20:14

@jamesqf Некоторые люди просто не могут выйти за рамки необычного синтаксиса. Единственный язык, который меня действительно так поразил, был APL.

Barmar 13.06.2018 20:28

@Barmar: Дело не только в синтаксисе (множество идиотически глупых скобок :-)), хотя это, конечно, не помогает. Дело в том, что мой ум работает процедурно, так что C кажется почти идеальным вариантом для моего мышления.

jamesqf 14.06.2018 20:32

@jamesqf Lisp является процедурным. За исключением синтаксиса, большинство современных языков сценариев приняли многие из тех же концепций. Если вы не понимаете Lisp, у вас, вероятно, будут аналогичные проблемы с Javascript и Python.

Barmar 14.06.2018 20:50

@Barmar: Не то, что я припоминаю, хотя прошло несколько десятилетий с тех пор, как я познакомился с этим в бакалавриате. Я даже не припоминаю, как делать если или лучше, только CAR, CDR и эти скобки. Javascript у меня не было возможности использовать, в то время как моя единственная (хотя и серьезная) проблема с Python - это идея языка, в котором исходный код самоуничтожается, если у вас есть вкладки набора, отличные от того, что сделал автор кода.

jamesqf 15.06.2018 22:52

@jamesqf (if (= a 1) (format t "It matches~&") (format t "It doesn't match~&"))

Barmar 15.06.2018 23:01

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

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

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

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

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


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

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

Bartek Banachewicz 12.06.2018 16:31

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

NO_NAME 12.06.2018 16:35

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

davidbak 12.06.2018 19:14

Why does a type have only one size associated with it when the space required to represent the value might be smaller than that size?

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

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

Object types have alignment requirements which place restrictions on the addresses at which an object of that type may be allocated.

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

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

Потому что в таком языке, как 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 - это аргумент, переданный из другого фрагмента кода, который компилируется отдельно?

Компьютерная память подразделяется на блоки с последовательной адресацией определенного размера (часто 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 16.06.2018 15:05

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

supercat 16.06.2018 19:49

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

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

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

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

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

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

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

Резюме

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

Примечание

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

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

online Thomas 13.06.2018 17:00

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

Alfe 14.06.2018 11:54

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

Peteris 14.06.2018 12:30

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

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

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

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

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

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

Max Barraclough 13.06.2018 11:56

Если быть педантичным, то на самом деле вы имеете в виду очень точные числа :) Ну, по крайней мере, в случае BigDecimal ...

Bill K 14.06.2018 00:25

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

Digital Trauma 14.06.2018 02:01

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

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

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

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

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

В стандартной библиотеке 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 в строке, достаточно добавить четыре миллиона к адресу массива.

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

Таким образом, можно использовать бигнумы переменной длины вместо short int, int, long int и long long int с фиксированной шириной, но было бы неэффективно их выделять и использовать. Кроме того, все основные процессоры предназначены для выполнения арифметических операций с регистрами фиксированной ширины, и ни один из них не имеет инструкций, которые напрямую работают с каким-либо типом bignum переменной длины. Их нужно будет реализовать программно, гораздо медленнее.

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

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

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

Then myInt would occupy 4 bytes with my compiler. However, the actual value, 255 can be represented with only 1 byte, so why would myInt not just occupy 1 byte of memory?

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

Or the more generalized way of asking: Why does a type have only one size associated with it when the space required to represent the value might be smaller than that size?

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

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

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

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

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

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

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

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

О да!

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

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

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

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

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

Artelius 14.06.2018 07:03

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

Matthieu M. 14.06.2018 08:35

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

Artelius 10.11.2018 03:21

@Artelius: Обратите внимание, что в Python массив содержит указатели фиксированного размера элементам; это делает доступ к элементу за O (1) за счет косвенного обращения.

Matthieu M. 10.11.2018 12:54

Есть несколько причин. Одним из них является дополнительная сложность обработки чисел произвольного размера и снижение производительности, которое это дает, потому что компилятор больше не может оптимизировать, исходя из предположения, что каждый 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 ГБ.

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

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

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

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

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

so why would myInt not just occupy 1 byte of memory?

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

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

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

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

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

потому что это соответствует целям разработки C++.

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

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

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

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

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

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

$43, %eax
ret

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

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

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

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

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

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

underscore_d 16.06.2018 15:16

Впрочем, "экономить память" в регистрах нельзя. Если вы не пытаетесь сделать что-то странное, злоупотребляя AH и AL, вы все равно не сможете иметь несколько разных значений в одном и том же регистре общего назначения. Локальные переменные часто остаются в регистрах и никогда не попадают в оперативную память, если в этом нет необходимости.

meneldal 18.06.2018 01:07

Другие вопросы по теме