Почему максимальный размер байта [] 2 ГБ - 57 В?

На моей 64-битной машине этот код C# работает:

new byte[2L * 1024 * 1024 * 1024 - 57]

но этот бросает OutOfMemoryException:

new byte[2L * 1024 * 1024 * 1024 - 56]

Зачем?

Я понимаю, что максимальный размер управляемого объекта составляет 2 ГБ и что создаваемый объект массива содержит больше байтов, чем я хочу. А именно, есть 4 байта (или 8?) Для номера синхронизирующего блока, 8 байтов для ссылки на MethodTable и 4 байта для размера массива. Это 24 байта, включая заполнение, так почему я не могу выделить массив с 2G - 24 байта? Действительно ли максимальный размер действительно равен 2 ГБ? Если это так, для чего используются оставшиеся 2 ГБ?

Примечание. На самом деле мне не нужно выделять массив с 2 миллионами байтов. И даже если бы я сделал, 56 байтов незначительно накладные расходы. И я мог легко обойти предел, используя пользовательские BigArray<T>,

3 ответа

Решение

Вам нужно 56 байтов накладных расходов. Это на самом деле 2,147,483,649-1 минус 56 для максимального размера. Вот почему ваш минус 57 работает, а минус 56 нет.

Как сказал здесь Джон Скит:

Однако на практике я не верю, что какая-либо реализация поддерживает такие огромные массивы. CLR имеет ограничение для каждого объекта чуть более 2 ГБ, поэтому даже байтовый массив не может иметь 2147483648 элементов. Немного экспериментов показывает, что на моем компьютере самый большой массив, который вы можете создать, это новый байт [2147483591]. (Это на 64-битной.NET CLR; версия Mono, на которой у меня установлены дроссели.)

Смотрите также эту статью InformIT на ту же тему. Он предоставляет следующий код для демонстрации максимальных размеров и накладных расходов:

class Program
{
  static void Main(string[] args)
  {
    AllocateMaxSize<byte>();
    AllocateMaxSize<short>();
    AllocateMaxSize<int>();
    AllocateMaxSize<long>();
    AllocateMaxSize<object>();
  }

  const long twogigLimit = ((long)2 * 1024 * 1024 * 1024) - 1;
  static void AllocateMaxSize<T>()
  {
    int twogig = (int)twogigLimit;
    int num;
    Type tt = typeof(T);
    if (tt.IsValueType)
    {
      num = twogig / Marshal.SizeOf(typeof(T));
    }
    else
    {
      num = twogig / IntPtr.Size;
    }

    T[] buff;
    bool success = false;
    do
    {
      try
      {
        buff = new T[num];
        success = true;
      }
      catch (OutOfMemoryException)
      {
        --num;
      }
    } while (!success);
    Console.WriteLine("Maximum size of {0}[] is {1:N0} items.", typeof(T).ToString(), num);
  }
}

Наконец, в статье есть что сказать:

Если вы посчитаете, вы увидите, что накладные расходы на выделение массива составляют 56 байтов. В конце осталось несколько байтов из-за размеров объекта. Например, 268 435 448 64-разрядных чисел занимают 2 147 483 584 байта. Добавление заголовка массива 56 байтов дает вам 2 147 483 640, оставляя вам 7 байтов меньше 2 гигабайт.

Редактировать:

Но подождите, это еще не все!

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

Type            x86 size            x64 size
object          12                  24
object[]        16 + length * 4     32 + length * 8
int[]           12 + length * 4     28 + length * 4
byte[]          12 + length         24 + length
string          14 + length * 2     26 + length * 2

Мистер Скит продолжает:

Вам может быть прощено смотреть на числа выше и думать, что "накладные расходы" объекта составляют 12 байтов в x86 и 24 в x64... но это не совсем верно.

и это:

  • "Базовые" накладные расходы составляют 8 байтов на объект в x86 и 16 на объект в x64... учитывая, что мы можем хранить Int32 "реальных" данных в x86 и при этом иметь размер объекта 12, а также мы можем хранить два Int32 реальных данных в x64 и все еще имеют объект x64.

  • Существует "минимальный" размер 12 байтов и 24 байта соответственно. Другими словами, вы не можете иметь тип, который является просто накладными расходами. Обратите внимание, что класс "Пустой" принимает тот же размер, что и при создании экземпляров Object… фактически есть некоторая свободная комната, потому что CLR не любит работать с объектом без данных. (Обратите внимание, что структура без полей также занимает место, даже для локальных переменных.)

  • Объекты x86 дополняются до 4-байтовых границ; на x64 это 8 байт (как и раньше)

и наконец Джон Скит ответил на вопрос, который я ему задал, в другом вопросе, где он заявляет (в ответ на статью InformIT, которую я ему показал):

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

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

Одно можно сказать наверняка: у вас не может быть нечетного количества байтов, оно обычно кратно собственному размеру слова, который составляет 8 байтов в 64-битном процессе. Таким образом, вы можете добавить еще 7 байтов в массив.

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

https://github.com/dotnet/runtime/blob/596ee7cc7fef74d40223bccacdee3e1e7f21bbef/src/coreclr/vm/gchelpers.cpp

      inline SIZE_T MaxArrayLength(SIZE_T componentSize)
{
    // Impose limits on maximum array length in each dimension to allow efficient
    // implementation of advanced range check elimination in future. We have to allow
    // higher limit for array of bytes (or one byte structs) for backward compatibility.
    // Keep in sync with Array.MaxArrayLength in BCL.
    return (componentSize == 1) ? 0X7FFFFFC7 : 0X7FEFFFFF;
}
...

SIZE_T componentSize = pArrayMT->GetComponentSize();
if ((SIZE_T)cElements > MaxArrayLength(componentSize))
    ThrowOutOfMemoryDimensionsExceeded();
Другие вопросы по тегам