Почему массивы не могут быть обрезаны?


на сайте документации MSDN говорится следующее о Array.Resize способ:

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

Если newSize меньше длины старого массива, то новый массив будет выделены и элементы копируются из старого массива в новый пока не будет заполнен новый; остальные элементы в старом матрица игнорируются.

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

но зачем создавать новый массив с меньшим размером? Почему массив не может просто удалить свое утверждение из последних блоков памяти? Тогда это будет операция O(1) вместо O(n), как сейчас.

имеет ли это какое-то отношение к тому, как данные организованы на архитектурном или физическом уровне компьютера?

8 70

8 ответов:

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

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

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

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

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

есть также алгоритмические компромиссы производительности. Например, изучая поведение большинства приложений, вы обнаружите, что в 99% случаев приложения выделяют блоки данных размером от 6 до 8 КБ.

Если бы система памяти позволила вам выделить и освободить 4KB, она осталась бы со свободным куском 4KB, который 99% выделений не смогут использовать. Если бы вместо того, чтобы выделять более 8 КБ, даже если бы требовалось только 4 КБ, это было бы много более многоразовые.

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

первый подход более эффективен, но медленнее, второй подход менее эффективен, но быстрее.

Это становится еще более интересным в таких языках, как C# и Java, которые имеют "памяти". В управляемой системе памяти память даже не освобождается; она просто перестает использоваться, что сборщик мусора позже, в некоторых случаях намного позже, обнаруживает и освобождает.

для получения дополнительной информации различные управления памятью и распределения вы можете проверить эту статью на Википедия:

https://en.wikipedia.org/wiki/Memory_management

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

в .NET, система.Объект играет ключевую роль. Все знают, что он делает, что не так очевидно, что он продолжает жить после того, как объект собран. Два дополнительных поля в заголовке объекта (syncblock и введите handle) затем превратитесь в указатель назад и вперед на предыдущий/следующий свободный блок. Он также имеет минимальный размер, 12 байт в 32-разрядном режиме. Гарантирует, что всегда будет достаточно места для хранения свободного размера блока после сбора объекта.

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

Я искал ответ на ваш вопрос, так как я нашел его очень интересным вопросом. Я нашел ответ, который имеет интересную первую строку:

вы не можете освободить часть массива - вы можете только free() указатель, который вы получили от malloc() и когда вы это сделаете, вы освободите все распределение, которое вы просили.

Так что на самом деле проблема заключается в регистре, который хранит, какая память выделяется. Вы не можете просто освободить часть из блока, который вы выделили, вы должны освободить его полностью или вы не освобождаете его вообще. Это означает, что для того, чтобы освободить эту память, вы должны сначала переместить данные. Я не знаю, делает ли .NET memory management что-то особенное в этом отношении, но я думаю, что это правило относится и к среде CLR.

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

пример:

int[] original = new int[] { 1, 2, 3, 4, 5, 6 };
int[] otherReference = original; // currently points to the same object

Array.Resize(ref original, 3);

Console.WriteLine("---- OTHER REFERENCE-----");

for (int i = 0; i < otherReference.Length; i++)
{
    Console.WriteLine(i);
}

Console.WriteLine("---- ORIGINAL -----");

for (int i = 0; i < original.Length; i++)
{
    Console.WriteLine(i);
}

принты:

---- OTHER REFERENCE-----
0
1
2
3
4
5
---- ORIGINAL -----
0
1
2

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

во-вторых, есть реализации, где это абсолютно необходимо сделать. Например, MacOS X имеет реализацию, в которой один большой блок памяти используется для выделения блоков malloc от 1 до 16 байт, другой большой блок памяти для блоков malloc от 17 до 32 байт, один для блоков malloc от 33 до 48 байт и т. д. Это очень естественно, что любое изменение размера, которое остается в диапазоне, скажем, от 33 до 48 байт, возвращает тот же блок, но меняется на 32 или 49 байт должны перераспределить блока.

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

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

рассмотрим простой пример:

var fun = 0;
for (var i = 0; i < array.Length; i++)
{
  fun ^= array[i];
}

для поддержания безопасности памяти, каждый array доступ должен быть проверен границами, гарантируя, что проверка границ не является нарушается другими потоками (среда выполнения .NET имеет гораздо более строгие гарантии, чем, скажем, компилятор C).

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

var fun = 0;
for (var i = 0; i < array.Length; i++)
{
  lock (array)
  {
    if (i >= array.Length) throw new IndexOutOfBoundsException(...);

    fun ^= array[i];
  }
}

Излишне говорить, что это ужасно дорого. Создание неизменной длины массива дает вам два массивных прирост производительности:

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

на самом деле, то, что на самом деле делает среда выполнения, оказывается чем-то вроде этого:

var fun = 0;
var len = array.Length; // Provably safe

for (var i = 0; i < len; i++)
{
  // Provably safe, no bounds checking needed
  fun ^= array[i];
}

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

теперь давайте посмотрим плюсы и минусы добавления массива сокращается так, как вы хотите:

плюсы:

  • в очень редком случае, когда вы хотите сделать массив меньше, это будет означать, что массив не нужно копировать, чтобы изменить его длину. Тем не менее, это все равно потребует уплотнения кучи в будущем, что включает в себя много копирования.
  • если вы храните ссылки на объект в массив, вы можете получить некоторые преимущества от локальности кэша, если выделение массива и элементов происходит colocated. Излишне говорить, что это еще реже, чем Pro #1.

плюсы:

  • любой доступ к массиву стал бы ужасно дорогим, даже в узких петлях. Так что все будут использовать unsafe код вместо этого, и там идет ваша безопасность памяти.
  • каждый отдельный фрагмент кода, имеющий дело с массивами, должен был бы ожидать, что длина массив может измениться в любое время. Для каждого отдельного доступа к массиву потребуется try ... catch (IndexOutOfRangeException), и все итерации по массиву должны быть в состоянии справиться с изменением размера-когда-либо задавались вопросом, почему вы не можете добавлять или удалять элементы из List<T> вы перебираете?
  • огромный объем работы для команды CLR, который не может быть использован на другой, более важной функции.

есть некоторые детали реализации, которые делают это еще менее выгодным. Важнее, Куча .NET не имеет ничего общего с malloc/free шаблоны. Если исключить LOH, то текущий MS.NET куча ведет себя совершенно по-другому:

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

кроме того, как Ханс очень хорошо объяснил в своем ответе, только потому, что массив меньше, не обязательно означает, что в том же объеме памяти достаточно места для меньшего массива из-за заголовков объектов (помните, как .NET предназначен для безопасности памяти? Знание правильного типа объекта является обязательным для выполнения). Но на что он не указывает выходит, что даже если у вас достаточно памяти,вам все равно нужно переместить массив. Рассмотрим простой массив:

ObjectHeader,1,2,3,4,5

теперь мы удаляем последние два элемента:

OldObjectHeader;NewObjectHeader,1,2,3

Упс. Нам нужен старый заголовок объекта, чтобы сохранить список свободного места,иначе мы не смогли бы правильно сжать кучу. Теперь, это может быть сделано, что старый заголовок объекта будет перенесен дальше массив, чтобы избежать копирования, но это еще одно усложнение. Это оказывается, довольно дорогая функция для чего-то, что noöne никогда не будет использовать, на самом деле.

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

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

как объяснили другие, машинный код не намного лучше. Хотя вам не нужно поддерживать те же гарантии безопасности (которые я бы не стал использовать в качестве преимущества, но хорошо), все еще есть осложнения, связанные с тем, как вы выделение и управление памятью. Называется realloc чтобы сделать массив 10-item 5-item? Ну, он либо будет скопирован, либо все равно будет размером с массив из 10 элементов, потому что нет никакого способа вернуть оставшуюся память любым разумным способом.

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

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

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

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

чтобы ответить на ваш вопрос, пространство, выделенное массиву, рассматривается как один блок и хранится в stack в случае локальных переменных или bss/data segments когда он является глобальным. AFAIK, когда вы получаете доступ к массиву, как array[3], на низком уровне, ОС получит вам указатель на первый элемент и прыгает/пропускает до тех пор, пока он не достигнет (трижды в случае приведенного выше примера) требуемого блок. так это может быть архитектурное решение, что размер массива не может быть изменен после его объявления.

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