Как CLR (.NET) внутренне выделяет и передает пользовательские типы значений (структуры)? - PullRequest
6 голосов
/ 15 мая 2010

Вопрос:

Все ли типы значений CLR, включая определяемые пользователем struct s, живут исключительно в стеке оценки, а это означает, что их никогда не потребуется возвращать сборщику мусора, или есть случаи, когда они собираются мусором?

Справочная информация:

Ранее я задавал вопрос для SO о влиянии свободного интерфейса на производительность среды выполнения приложения .NET . Я был особенно обеспокоен тем, что создание большого количества очень коротких временных объектов отрицательно скажется на производительности во время выполнения за счет более частой сборки мусора.

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

(Это произошло со мной в основном потому, что я думал о способе обработки локальных переменных в C ++. Обычно это автоматические (auto) переменные, они размещаются в стеке и поэтому освобождаются, когда выполнение программы возвращается к вызывающей программе & mdash ; вообще не задействовано динамическое управление памятью через new / delete. Я думал, что CLR просто может обрабатывать struct с аналогично.)

Что я узнал до сих пор:

Я провел небольшой эксперимент, чтобы увидеть, каковы различия в CIL, сгенерированном для пользовательских типов значений и ссылочных типов. Это мой код C #:

struct SomeValueType     {  public int X;  }
class SomeReferenceType  {  public int X;  }
.
.
static void TryValueType(SomeValueType vt) { ... }
static void TryReferenceType(SomeReferenceType rt) { ... }
.
.
var vt = new SomeValueType { X = 1 };
var rt = new SomeReferenceType { X = 2 };
TryValueType(vt);
TryReferenceType(rt);

И это CIL, сгенерированный для последних четырех строк кода:

.locals init
(
    [0] valuetype SomeValueType vt,
    [1] class SomeReferenceType rt,
    [2] valuetype SomeValueType <>g__initLocal0,  //
    [3] class SomeReferenceType <>g__initLocal1,  // why are these generated?
    [4] valuetype SomeValueType CS$0$0000         //
)

L_0000: ldloca.s CS$0$0000
L_0002: initobj SomeValueType  // no newobj required, instance already allocated
L_0008: ldloc.s CS$0$0000
L_000a: stloc.2
L_000b: ldloca.s <>g__initLocal0
L_000d: ldc.i4.1 
L_000e: stfld int32 SomeValueType::X
L_0013: ldloc.2 
L_0014: stloc.0 
L_0015: newobj instance void SomeReferenceType::.ctor()
L_001a: stloc.3
L_001b: ldloc.3 
L_001c: ldc.i4.2 
L_001d: stfld int32 SomeReferenceType::X
L_0022: ldloc.3 
L_0023: stloc.1 
L_0024: ldloc.0 
L_0025: call void Program::TryValueType(valuetype SomeValueType)
L_002a: ldloc.1 
L_002b: call void Program::TryReferenceType(class SomeReferenceType)

Что я не могу понять из этого кода, так это:

  • Где расположены все эти локальные переменные, упомянутые в блоке .locals? Как они распределяются? Как они освобождены?

  • (Не по теме: почему так много анонимных локальных переменных требуется и копируется туда-сюда только для инициализации моих двух локальных переменных rt и vt?)

Ответы [ 4 ]

11 голосов
/ 21 мая 2012

Ваш принятый ответ неверен.

Разница между типами значений и ссылочными типами в основном заключается в семантике присваивания. Типы значений копируются при присваивании - для структуры, что означает копирование содержимого всех полей. Типы ссылок только копируют ссылку, а не данные. Стек - это деталь реализации. В спецификации CLI ничего не говорится о том, где расположен объект, и плохая идея полагаться на поведение, которого нет в спецификации.

Типы значений характеризуются семантикой передачи по значению, но это не означает, что они фактически копируются сгенерированным машинным кодом.

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

Несколько человек объяснили, почему этот ответ был неправильным в комментариях под ним, но какой-то модератор удалил их все.

Временные объекты (местные жители) будут жить в поколении GC 0. GC уже достаточно умен, чтобы освободить их, как только они выйдут из области видимости. Для этого вам не нужно переключаться на экземпляры структуры.

Это полная чушь. GC видит только информацию, доступную во время выполнения, и к этому моменту все понятия области исчезли. GC не будет собирать ничего «как только выйдет за рамки». GC заберет его в какой-то момент после того, как он станет недоступным.

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

Опять же, это полная чушь. Нет ничего плохого в наличии ссылок внутри типа значения.

Теперь, чтобы ответить на ваш вопрос:

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

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

Например, если вы напишите функцию, которая зацикливается на элементах массива, то есть большая вероятность, что переменная цикла int (тип значения) будет полностью жить в регистре и никогда не будет пролита в стек или записано в кучу. Вот что имел в виду Эрик Липперт (из команды Microsoft C #, который написал о себе «Я не знаю всех подробностей» относительно GC .NET), когда писал, что типы значений могут передаваться в стек когда "джиттер выбирает не регистрировать значение" . Это также верно для типов больших значений (например, System.Numerics.Complex), но существует большая вероятность того, что типы больших значений не поместятся в регистры.

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

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

CIL - это промежуточный язык высокого уровня, и, следовательно, он не даст вам никакой информации о распределении регистров и разливе в стек и даже не даст вам точную картину бокса. Однако, взглянув на CIL, вы сможете увидеть, как внешний компилятор C # или F # блокирует некоторые типы значений, так как он переводит даже конструкции более высокого уровня, такие как async и comp понять, в CIL.

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

5 голосов
/ 15 мая 2010

Пожалуйста, примите во внимание следующее:

  1. Разница между типами значений и ссылочными типами в основном одна из семантики присваивания. Типы значений копируются при присваивании - для struct, что означает копирование содержимого всех полей. Типы ссылок только копируют ссылку, а не данные. Стек - это деталь реализации . В спецификации CLI ничего не говорится о том, где расположен объект, и обычно опасно полагаться на поведение, которого нет в спецификации.

  2. Временные объекты (местные жители) будут жить в поколении GC 0. GC уже достаточно умен, чтобы освобождать их (почти), как только они выходят из области видимости - или когда это наиболее эффективно для этого. , Gen0 запускается достаточно часто, поэтому вам не нужно переключаться на struct экземпляры для эффективного управления временными объектами.

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

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

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

  4. g_initLocal0 и связанные с ними поля существуют, потому что вы используете инициализаторы объектов. Переключитесь на параметризованные конструкторы, и вы увидите, что они исчезают.

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

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

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

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

4 голосов
/ 15 мая 2010

Это деталь реализации JIT-компилятора, в которой он будет размещать .locals. В данный момент я не знаю ни одного, который бы не размещал их в кадре стека. Они «распределяются» путем настройки указателя стека и «освобождаются» путем сброса его обратно. Очень быстро, трудно улучшить. Но кто знает, через 20 лет мы все можем работать на машинах с ядрами ЦП, которые оптимизированы для запуска только управляемого кода с совершенно другой внутренней реализацией. Вероятно, ядра с тонной регистров, оптимизатор JIT уже использует регистры для хранения локальных данных.

Временные значения испускаются компилятором C #, чтобы обеспечить некоторые минимальные гарантии согласованности в случае, если инициализаторы объекта генерируют исключения. Он не позволяет вашему коду видеть частично инициализированный объект в блоке catch или finally. Также используемый в операторах using и lock, он предотвращает удаление или разблокировку неправильного объекта, если вы замените ссылку на объект в своем коде.

1 голос
/ 15 мая 2010

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

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

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

...