Обновление: ответ ПОЛНОСТЬЮ переписан. Оригинальный ответ содержал методы, чтобы найти максимально возможный адресуемый массив в любой системе путем деления и завоевания, смотрите историю этого ответа, если вам интересно. Новый ответ пытается объяснить разрыв в 56 байт.
В своем собственном ответе А.З. пояснил, что максимальный размер массива ограничен менее 2 ГБ, и с некоторыми пробами и ошибками (или другим методом?) Находит следующее (краткое изложение):
- Если размер типа составляет 1, 2, 4 или 8 байт, максимальный размер для заполнения составляет 2 ГБ - 56 байт;
- Если размер типа составляет 16 байт, максимальный размер составляет 2 ГБ - 48 байт;
- Если размер типа составляет 32 байта, максимальный размер составляет 2 ГБ - 32 байта.
Я не совсем уверен насчет ситуации с 16 байтами и 32 байтами. Общий доступный размер для массива может отличаться, если это массив структур или встроенный тип. Я подчеркну размер шрифта 1-8 байт (в этом я тоже не уверен, см. Заключение).
Макет данных массива
Чтобы понять, почему CLR не допускает точно 2GB / IntPtr.Size
элементов, нам нужно знать, как устроен массив. Хорошей отправной точкой является эта SO статья , но, к сожалению, некоторая информация кажется ложной или, по крайней мере, неполной. Эта углубленная статья о том, как .NET CLR создает объекты среды выполнения оказалась бесценной, а также недокументированных массивов статья о CodeProject.
Взяв всю информацию из этих статей, мы получаем следующую компоновку массива в 32-битных системах:
Single dimension, built-in type
SSSSTTTTLLLL[...data...]0000
^ sync block
^ type handle
^ length array
^ NULL
Каждая часть - одна система размером DWORD
. В 64-битных окнах это выглядит следующим образом:
Single dimension, built-in type
SSSSSSSSTTTTTTTTLLLLLLLL[...data...]00000000
^ sync block
^ type handle
^ length array
^ NULL
Макет выглядит немного иначе, когда он представляет собой массив объектов (то есть строк, экземпляров классов). Как видите, добавлен дескриптор типа для объекта в массиве.
Single dimension, built-in type
SSSSSSSSTTTTTTTTLLLLLLLLtttttttt[...data...]00000000
^ sync block
^ type handle
^ length array
^ type handle array element type
^ NULL
Глядя дальше, мы обнаруживаем, что встроенный тип, или фактически любой тип структуры, получает собственный обработчик определенного типа (все uint
имеют одинаковый общий доступ, но int
имеет обработчик другого типа для массива затем uint
или byte
). Все массивы объектов имеют один и тот же обработчик типов, но имеют дополнительное поле, которое указывает на обработчик типов объектов.
Замечание о типах структуры: не всегда может быть применено заполнение, что может затруднить прогнозирование фактического размера структуры.
Все еще не 56 байтов ...
Чтобы сосчитать 56 байтов ответа AZ, я должен сделать несколько предположений. Я предполагаю, что:
- дескриптор syncblock и type учитывает размер объекта;
- переменная, содержащая ссылку на массив (указатель объекта), учитывает размер объекта;
- нулевой терминатор массива учитывается в размере объекта.
Синхронный блок помещается перед адресом, на который указывает переменная, поэтому выглядит так, как будто он не является частью объекта . Но на самом деле, я верю, что это так и имеет значение для внутреннего ограничения в 2 ГБ. Добавляя все это, мы получаем, для 64-битных систем:
ObjectRef +
Syncblock +
Typehandle +
Length +
Null pointer +
--------------
40 (5 * 8 bytes)
Не 56 пока. Возможно, кто-то может взглянуть на Memory View во время отладки, чтобы проверить, как выглядит компоновка массива под 64-битными окнами.
Полагаю, что-то вроде этого (выбирайте, смешивайте и сочетайте):
- 2ГБ никогда не будут возможны, так как это один байт в следующем сегменте. Самый большой блок должен быть
2GB - sizeof(int)
. Но это глупо, так как индексы mem должны начинаться с нуля, а не с одного;
- Любой объект, размер которого превышает 85016 байт, будет помещен в LOH (куча больших объектов). Это может включать в себя дополнительный указатель или даже 16-байтовую структуру, содержащую информацию LOH. Возможно, это относится к пределу;
Выравнивание: при условии, что objectref не считается (в любом случае он находится в другом сегменте mem), общий разрыв составляет 32 байта. Вполне возможно, что система предпочитает 32-байтовые границы. Взгляните по-новому на макет памяти. Если начальная точка должна находиться на границе 32 байта, и ей нужно место для синхронизирующего блока перед ней, синхронизирующий блок окажется в конце первого 32 байтового блока. Примерно так:
XXXXXXXXXXXXXXXXXXXXXXXXSSSSSSSSTTTTTTTTLLLLLLLLtttttttt[...data...]00000000
, где XXX..
обозначает пропущенные байты.
- многомерных массивов: если вы динамически создаете массивы с помощью
Array.CreateInstance
с 1 или более измерениями, будет создан один массив dim с двумя дополнительными DWORDS, содержащими размер и нижнюю границу измерения (даже если у вас есть только один размерность, но только если нижняя граница указана как ненулевая). Я считаю это крайне маловероятным, поскольку вы, вероятно, упомянули бы об этом, если бы это было так в вашем коде. Но это принесло бы общее количество до 56 байт;).
Заключение
Из всего, что я собрал во время этого небольшого исследования, я думаю, что Overhead + Aligning - Objectref
является наиболее вероятным и наиболее подходящим выводом. Однако «настоящий» гуру CLR мог бы пролить некоторый дополнительный свет на этот специфический предмет.
Ни один из этих выводов не объясняет, почему 16 или 32-байтовые типы данных имеют разрыв 48 и 32 байта соответственно.
Спасибо за сложную тему, кое-что узнал на моем пути. Возможно, некоторые люди могут снять отрицательный голос, когда обнаружат, что этот новый ответ более связан с вопросом (который я изначально неправильно понял, и извинениями за беспорядок, который это могло вызвать).