Оптимизация кода вызывает нулевую ссылку в синглтоне - PullRequest
4 голосов
/ 01 июня 2019

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

Я использую Visual Studio 2017 с Console app в .NET 4.6.1 для этого примера.Мое основное приложение - приложение WPF, и я вызываю Singleton в конструкторе App ().

class Program
{
    static void Main(string[] args)
    {
        StringHolder.ImportantString = "Howdy";
        var x = Singleton.Current;
    }
}

public static class StringHolder
{
    public static string ImportantString { get; set; }
}

public class Singleton
{
    public static Singleton Current { get; } = new Singleton();
    private Singleton()
    {
        var x = StringHolder.ImportantString.ToLower(); // Null Reference occurs here when Optimize Code is on.
    }
}

Это, вероятно, связано с тем, что Singleton создается до того первая строка в Main называется.Добавление вызовов Console.WriteLine() показывает, что это происходит.

Одним из решений этой проблемы является разбиение функции Main на части: установка ImportantString и вызов функции Part, которая вызывает Singleton.

static void Main(string[] args)
{
    StringHolder.ImportantString = "Howdy";
    Part2();
}

public static void Part2()
{
    var x = Singleton.Current;
}

Однако это решение не работает с примером кода здесь: оно работает только в моем основном проекте.Я не совсем уверен, почему.

Другое решение состоит в том, чтобы изменить способ работы Singleton.Current:

public static Singleton _current;
public static Singleton Current => _current ?? (_current = new Singleton());

(Это, очевидно, исправляет это, поскольку статическое свойство не создается доэто называется.)

Третье решение заключается в добавлении статического конструктора:

static Singleton() { }

Но исправление моего кода не моя задача.Мои опасения заключаются в следующем:

  1. Почему включение функции оптимизации кода внезапно приводит к преждевременному созданию элемента Singleton?
  2. Почему трюк Part2() работает только, но тольков моем главном приложении?
  3. Почему добавление статического конструктора исправляет ошибку?

1 Ответ

1 голос
/ 02 июня 2019

Добавьте трассировку:

class Program
{
    static void Main(string[] args)
    {
        StringHolder.ImportantString = "Howdy";
        var x = Singleton.Current;
        Console.WriteLine("Done");
    }
}

public class Singleton
{
    public static Singleton Current { get; } = new Singleton();

    private Singleton()
    {
        try
        {

            Console.WriteLine("Calling ctor.");
            var x = StringHolder.ImportantString.ToLower(); // Null Reference occurs here when Optimize Code is on.
            Console.WriteLine("ctor called.");
        }
        catch(Exception e)
        {
            Console.WriteLine($"ctor failed with {e.GetType()}");
        }
    }
}

public static class StringHolder
{
    private static string importantString;

    public static string ImportantString
    {
        get
        {
            Console.WriteLine("Getting ImportantString");
            return importantString;
        }
        set
        {
            Console.WriteLine("Setting ImportantString");
            importantString = value;
            Console.WriteLine("ImportantString set");
        }
    }
}

и запустите программу в режиме Debug.

Вывод:

Setting ImportantString
ImportantString set
Calling ctor.
Getting ImportantString
ctor called.
Done

Запустите программу в режиме Release.

Вывод:

Calling ctor.
Getting ImportantString
ctor failed with System.NullReferenceException
Setting ImportantString
ImportantString set
Done

Debug mode IL из Main is:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       30 (0x1e)
  .maxstack  1
  .locals init ([0] class OptimizationIssue.Singleton x)
  IL_0000:  nop
  IL_0001:  ldstr      "Howdy"
  IL_0006:  call       void OptimizationIssue.StringHolder::set_ImportantString(string)
  IL_000b:  nop
  IL_000c:  call       class OptimizationIssue.Singleton OptimizationIssue.Singleton::get_Current()
  IL_0011:  stloc.0
  IL_0012:  ldstr      "Done"
  IL_0017:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_001c:  nop
  IL_001d:  ret
} // end of method Program::Main

Release mode IL of Main is:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       27 (0x1b)
  .maxstack  8
  IL_0000:  ldstr      "Howdy"
  IL_0005:  call       void OptimizationIssue.StringHolder::set_ImportantString(string)
  IL_000a:  call       class OptimizationIssue.Singleton OptimizationIssue.Singleton::get_Current()
  IL_000f:  pop
  IL_0010:  ldstr      "Done"
  IL_0015:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_001a:  ret
} // end of method Program::Main

IL s для обоих выглядят одинаково.

Debug mode disassembly из Main is:

        {
01A3084A  in          al,dx  
01A3084B  push        edi  
01A3084C  push        esi  
01A3084D  push        ebx  
01A3084E  sub         esp,38h  
01A30851  mov         esi,ecx  
01A30853  lea         edi,[ebp-44h]  
01A30856  mov         ecx,0Eh  
01A3085B  xor         eax,eax  
01A3085D  rep stos    dword ptr es:[edi]  
01A3085F  mov         ecx,esi  
01A30861  mov         dword ptr [ebp-3Ch],ecx  
01A30864  cmp         dword ptr ds:[16042E8h],0  
01A3086B  je          01A30872  
01A3086D  call        7247F5A0  
01A30872  xor         edx,edx  
01A30874  mov         dword ptr [ebp-40h],edx  
01A30877  nop  
            StringHolder.ImportantString = "Howdy";
01A30878  mov         ecx,dword ptr ds:[4402334h]  
01A3087E  call        01A30458  
01A30883  nop  
            var x = Singleton.Current;
01A30884  call        01A30468  
01A30889  mov         dword ptr [ebp-44h],eax  
01A3088C  mov         eax,dword ptr [ebp-44h]  
01A3088F  mov         dword ptr [ebp-40h],eax  
            Console.WriteLine("Done");
01A30892  mov         ecx,dword ptr ds:[4402338h]  
01A30898  call        70DD3CD4  
01A3089D  nop  
        }
01A3089E  nop  
01A3089F  lea         esp,[ebp-0Ch]  
01A308A2  pop         ebx  
01A308A3  pop         esi  
01A308A4  pop         edi  
01A308A5  pop         ebp  
01A308A6  ret  

Release mode disassembly из Main is:

            StringHolder.ImportantString = "Howdy";
00D51072  in          al,dx  
00D51073  mov         ecx,dword ptr ds:[3A32344h]  
00D51079  call        dword ptr ds:[0BF4DF4h]  
            Console.WriteLine("Done");
00D5107F  mov         ecx,dword ptr ds:[3A32348h]  
00D51085  call        70DD3CD4  
00D5108A  pop         ebp  
00D5108B  ret  

disassembly с сильно отличаются. JIT-compilation имеет значение. Похоже JIT-compilation удаляет неиспользованную переменную. Но он по-прежнему создает тип OptimizationIssue.Singleton, , вызывающий его статический конструктор перед выполнением метода Main. Статический конструктор создается неявно из-за public static Singleton Current { get; } = new Singleton(); в коде. Когда он называется StringHolder.ImportantString еще не установлен, он равен нулю, поэтому NullReferenceException выбрасывается при попытке вызвать ToLower() для него.

Удалите var x = Singleton.Current; из Main и посмотрите disassembly:

            StringHolder.ImportantString = "Howdy";
00FE084A  in          al,dx  
00FE084B  mov         ecx,dword ptr ds:[3BF2334h]  
00FE0851  call        dword ptr ds:[0CC4DF4h]  
            Console.WriteLine("Done");
00FE0857  mov         ecx,dword ptr ds:[3BF2338h]  
00FE085D  call        70DD3CD4  
00FE0862  pop         ebp  
00FE0863  ret  

Это мало что меняет. Мы удалили вручную то, что компилятор удалил автоматически. Но тип Singleton больше не упоминается, поэтому статический конструктор не вызывается, поэтому нет исключения.

Addig static Singleton() { } изменяется disassembly на:

            StringHolder.ImportantString = "Howdy";
0169084A  in          al,dx  
0169084B  mov         ecx,dword ptr ds:[41F2334h]  
01690851  call        dword ptr ds:[1334DF4h]  
            var x = Singleton.Current;
01690857  call        dword ptr ds:[1334E60h]  
            Console.WriteLine("Done");
0169085D  mov         ecx,dword ptr ds:[41F2338h]  
01690863  call        70DD3CD4  
01690868  pop         ebp  
01690869  ret   

Теперь он по какой-то причине не удаляет var x = Singleton.Current; и вызывает Singleton статический конструктор просто перед выполнением строки после того, как StringHolder.ImportantString было установлено, поэтому исключений нет.

enter image description here

Это оптимизировано JIT-compilation магия. Не надейся на это. Удалите static Singleton() { } из Singleton и лучше добавьте [MethodImpl(MethodImplOptions.NoOptimization)] в Main. (Или намного лучше не создавайте объекты, которые вы никогда не используете.)

Тогда вывод:

Setting ImportantString
ImportantString set
Calling ctor.
Getting ImportantString
ctor called.
Done

disassembly - это:

            StringHolder.ImportantString = "Howdy";
00DA084A  in          al,dx  
00DA084B  mov         ecx,dword ptr ds:[3A42334h]  
00DA0851  call        dword ptr ds:[0B14DF4h]  
            var x = Singleton.Current;
00DA0857  call        dword ptr ds:[0B14E60h]  
            Console.WriteLine("Done");
00DA085D  mov         ecx,dword ptr ds:[3A42338h]  
00DA0863  call        70DD3CD4  
00DA0868  pop         ebp  
00DA0869  ret  

И все отлично работает.

Мораль истории : JIT-compilation с оптимизациями полон встраивания, удаления и многих других вещей, которые трудно предвидеть и которые могут странным образом изменить поведение вашего кода .

...