Страницы

Поиск по вопросам

среда, 20 февраля 2019 г.

Когда уничтожается ValueType и семантика работы GC с ValueType

Ходят мифы и легенды,мол ValueType удаляется посредством GC(то бишь GC деаллоцирует как ReferenceType,так и ValueType). Но на самом то деле это не так. К примеру у нас есть код:
class A {} class B { void TestMethod() { A a = new A(); int x = 100; } }
в контексте(Scope) метода TestMethod(), создается объект(ReferenceType) типа А,а так же переменная X(ValueType).
По завершению работы метода, переменная X уничтожается,а объект типа А теряет ссылку на объект,и становится претендентом для удаления от GC.
Иными словами,ValueTypе существует в контексте до тех пор,пока выполняется,и соответственно Stack, по типу метода Pop() сам удалит эти данные из памяти, и никакого участия в этом не принимает GC, поэтому ValueType и работает быстрее (хотя все зависит от задачи).
И сам вопрос,всегда ли это так работает? (читал разные статьи,иногда пишут,что это происходит только тогда,когда стек забивается, т.е. доходит до заполнения)
Что делает CLR,когда стек уже почти переполнен,а все данные в нем к примеру являются ссылками на объекты в куче? Как и когда удаляются пользовательские структуры? Как именно CLR решает удалять ли данные из стека или оставить их еще существовать N-ое кол-во времени!??


Ответ

Локальные переменные типов ValueType [на которых нет замыканий из анонимных методов и лябмд] лежат прямо в стеке или в регистрах процессора (как захочется оптимизатору).
Вы можете прямо посмотреть, как выполняется ваш код, нажав правой кнопкой по нему в отладке, и выбрав Go To Disassembly, может быть это прояснит картину. Вот как это выглядит в отладочном режиме (что выключает оптимизации). Я добавил комментарии в важных местах:
{ 025B2E48 push ebp // это так называемый пролог функции 025B2E49 mov ebp,esp // https://en.wikipedia.org/wiki/Function_prologue 025B2E4B push edi // суть его - сохранить текущее положение 025B2E4C push esi // стека в "базовый указатель" - [e]bp 025B2E4D push ebx
в стек запихнули значения 3-х регистров, так что его указатель теперь отличается уже на 12 от того, который был в начале функции
025B2E4E sub esp,3Ch
esp - это указатель на начало стека. Уменьшить его на 3Ch - это выделить в стеке 3Сh (60) байт под локальные переменные (или другие накладные расходы) к этому моменту он уже отличался на 12 от значения, которое лежит в ebp, так что локальные переменные находятся в диапазоне по адресам от [ebp-13] до [ebp-72]. Он же [ebp-0Dh] до [ebp-48h].
Потом делаем кучу проверок и долго и мучительно создаем объект (это все из-за отладочного режима). Я пропущу большую часть кода, она не имеет отношения к вопросу:
025B2E51 mov esi,ecx 025B2E7C nop A a = new A(); 025B2E7D mov ecx,700F98h 025B2E82 call 024130F4 025B2E87 mov dword ptr [ebp-48h],eax 025B2E8A mov ecx,dword ptr [ebp-48h] 025B2E8D call 025B0D18 025B2E92 mov eax,dword ptr [ebp-48h]
и вот наконец ложим указатель на созданный объект в стек (в ebp лежит положение стека на момент начала вызова функции) 025B2E95 mov dword ptr [ebp-40h],eax
С целым числом попроще - просто запихиваем нужное значение в относительно epb - т.е. относительно начала стека на момент функции.
int x = 100; 025B2E98 mov dword ptr [ebp-44h],64h } 025B2E9F nop
А вот теперь фокус. Берем и загружаем в указатель стека значение, которое в нем было сразу после 025B2E4D push ebx. По сути это esp = ebp-0Ch
025B2EA0 lea esp,[ebp-0Ch] 025B2EA3 pop ebx 025B2EA4 pop esi 025B2EA5 pop edi
и после следующей строчки получаем значение esp равное тому, которое было в начале функции.
025B2EA6 pop ebp 025B2EA7 ret
За счет чего при этом выделалась и освобождалась память в стеке?
Выделалась за счет уменьшения указателя стека на нужное значение. Освобождалась - за счет восстановления старого значения указателя. Расходов на разрушение или "сброрку мусора" локальных переменных при этом не было.
Это стандартный механизм на x86, так что можно считать что так происходит почти всегда. По возврату из функции значение Stack Pointer восстанавливается в то, что было до ее вызова.

Комментариев нет:

Отправить комментарий