Странное поведение C# GC для слабых ссылок и нуль-условного оператора
При создании модульного теста для моего кода C#, который работает с WeakReferences
Я столкнулся с некоторым странным поведением GC - странным, потому что я не смог придумать объяснение этому.
Проблема связана с использованием ?.
нулевой условный оператор на объекте, который был получен из моей слабой ссылки после того, как GC собирал его.
Вот минимальный код, который копирует это:
public class XYZClass
{
public string Name { get; set; }
}
public class Tests
{
public void NormalBehavior()
{
var @ref = new WeakReference<XYZClass>(new XYZClass { Name = "bleh" });
GC.Collect();
GC.WaitForPendingFinalizers();
XYZClass t;
@ref.TryGetTarget(out t);
Console.WriteLine(t == null); //outputs true
}
public void WeirdBehavior()
{
var @ref = new WeakReference<XYZClass>(new XYZClass { Name = "bleh" });
GC.Collect();
GC.WaitForPendingFinalizers();
XYZClass t;
@ref.TryGetTarget(out t);
Console.WriteLine(t == null); //outputs false
Console.WriteLine(t?.Name == null); //outputs false
}
}
Поведение не проявлялось, когда этот код запускался с использованием linqpad. Я также проверил скомпилированный код IL (используя linqpad) и все еще не мог распознать ничего плохого.
1 ответ
Это не имеет ничего общего с нулевым условным оператором. Вы можете легко увидеть это, заменив его обычным доступом членов:
Console.WriteLine(t == null); //outputs false
Console.WriteLine(t.Name == null); //outputs false
Оригинальная ссылка на новый XYZClass
объект никогда не выходит "из области видимости" в сборке отладки (и работает под отладчиком). Отключите оптимизацию в LINQPad, и вы также увидите, что t
не является нулевым Но обратите внимание, что все это - деталь реализации - в зависимости от особенностей вашей системы, вы можете получить любой результат (например, я получаю то, что вы получаете в 32-битных сборках Debug, но не в 64-битных сборках Debug).
Единственная гарантия того, что вы получите время жизни управляемого объекта в.NET, состоит в том, что сильная ссылка вне финализатора предотвратит сбор объекта. Забудьте все детерминистическое управление памятью - его просто нет. Реализация.NET, которая вообще не имеет сборщика мусора, будет вполне допустимой.
Итак, давайте посмотрим на код, сгенерированный на моей машине, в частности. В 64-битной сборке t.Name == null
а также t?.Name == null
имеют точно такие же результаты (хотя, конечно, t.Name == null
вызовет NullReferenceException
вместо возвращения истины). А как насчет 32-битной сборки?
t.Name == null
часть существенно короче:
00533111 mov ecx,dword ptr [ebp-44h] ; t
00533114 cmp dword ptr [ecx],ecx ; null check
00533116 call 00530D28 ; t.get_Name
0053311B mov dword ptr [ebp-54h],eax ; Name string
0053311E cmp dword ptr [ebp-54h],0 ; is null?
00533122 sete cl
00533125 movzx ecx,cl
00533128 call 708B09F4
Вы можете видеть, что мы используем два регистра (ecx и eax) и два слота стека (-44h и -54h). Что насчет t?.Name == null
один?
001F3111 cmp dword ptr [ebp-44h],0 ; is t null?
001F3115 jne 001F311F
001F3117 nop
001F3118 xor edx,edx
001F311A mov dword ptr [ebp-54h],edx ; result is false
001F311D jmp 001F312A
001F311F mov ecx,dword ptr [ebp-44h] ; t
001F3122 call 001F0D28 ; t.get_Name
001F3127 mov dword ptr [ebp-54h],eax
001F312A cmp dword ptr [ebp-54h],0 ; is name null?
001F312E sete cl
001F3131 movzx ecx,cl
001F3134 call 708B09F4
001F3139 nop
Мы все еще используем те же два слота стека, но требуется еще один регистр - edx. Может ли это быть тем, что мы ищем? Еще бы! Если мы посмотрим, как изначально был создан объект:
001F30A0 mov ecx,2C0814h
001F30A5 call 001330F4 ; new XYZClass
001F30AA mov dword ptr [ebp-48h],eax ; tmp
001F30AD mov ecx,dword ptr [ebp-48h]
001F30B0 call 001F0D38 ; tmp.XYZClass()
001F30B5 mov edx,dword ptr ds:[36B230Ch] ; "bleh"
001F30BB mov ecx,dword ptr [ebp-48h]
001F30BE cmp dword ptr [ecx],ecx
001F30C0 call 001F0D30 ; tmp.set_Name("bleh")
001F30C5 nop
001F30C6 mov ecx,2C0858h
001F30CB call 710F9ECF ; new WeakReference
001F30D0 mov dword ptr [ebp-4Ch],eax
001F30D3 mov ecx,dword ptr [ebp-4Ch]
001F30D6 mov edx,dword ptr [ebp-48h] ; EDX references tmp!
001F30D9 call 709090B0
001F30DE mov eax,dword ptr [ebp-4Ch]
001F30E1 mov dword ptr [ebp-40h],eax
Вы можете видеть, что случается так, что в условно-нулевой версии используется тот же регистр, который использовался для хранения временной ссылки на XYZClass
, И именно в этом заключается разница - среда выполнения не может исключать, что edx
доступ - это использование временной ссылки, поэтому он защищает ее и сохраняет объект в корне, что предотвращает его сбор.
64-битная версия (и запущенная без отладчика) не видит разницы, потому что она использует другой регистр - на моей конкретной машине 64-битная версия повторно использует rcx
(который содержит ссылку на WeakReference
не XYZClass
), и 32-разрядная версия без отладчика повторно используется eax
(который содержит ссылку на "bleh"
). поскольку edx
(а также rdx
) никогда не используются в методе, временная ссылка больше не укоренена и может быть бесплатно собрана.
Почему используется версия отладчика edx
особенно? Скорее всего, он пытается быть полезным. В середине нулевого условного оператора вы хотите увидеть значение обоих t
а также t?.Name
, поэтому они должны быть доступны (вы можете увидеть это в Locals как "XYZClass.Name.get вернул"bleh" string").
Опять же, обратите внимание, что это полностью зависит от реализации. В контракте указывается только, когда объект не должен быть возвращен - в нем не указано, когда он будет возвращен.