Как стать автором
Обновить

Пара историй про отличия Release от Debug

Время на прочтение 5 мин
Количество просмотров 26K
Все разработчики знают, что исполнение релизной версии может отличаться от отладочной. В этой статье я расскажу пару случаев из жизни, когда такие отличия приводили к ошибочному исполнению программы. Примеры не отличаются большой сложностью, но вполне могут уберечь от наступления на грабли.

История 1



Собственно, началось все с того, что пришел баг о том что при некоторых операциях приложение вылетает. Это бывает часто. Баг не захотел воспроизводиться в Debug-версии. Это порой бывает. Поскольку в приложении часть библиотек была написано на C++, то первой мыслью было что-то вроде «где-то забыли переменную проинициализировать или что-то в этом духе». Но на деле суть бага крылась в управляемом коде, хотя без неуправляемого тоже не обошлось.

А код оказался примерно следующим:

class Wrapper : IDisposable
  {
    public IntPtr Obj {get; private set;};

    public Wrapper()
    {
      this.Obj = CreateUnmObject();
    }

    ~Wrapper()
    {
      this.Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
      if (disposing)
      {
      }

      this.ReleaseUnmObject(this.Obj);
      this.Obj = IntPtr.Zero;
    }

    public void Dispose()
    {
      this.Dispose(true);
      GC.SuppressFinalize(this);
    }
  }


* This source code was highlighted with Source Code Highlighter.

В принципе, практически каноническая реализация шаблона IDisposable («практически» — потому, что нет переменной disposed, вместо нее обнуление указателя), вполне стандартный класс-обертка неуправляемого ресурса.

Использовался же класс примерно следующим образом:
{
  Wrapper wr = new Wrapper();
  calc.DoCalculations(wr.Obj);
}


* This source code was highlighted with Source Code Highlighter.

Естественно, что внимательный читатель сразу обратит внимание, что объекта wr надо вызвать Dispose, то есть обернуть все конструкцией using. Но на первый взгляд, на причину падения это не должно повлиять, так как разница будет в том детерминировано ли очистится ресурс или нет.

Но на самом деле разница есть и именно в релизной сборке. Дело в том, что объект wr становится доступным сборщику мусора сразу после начала выполнения метода DoCalculations, ведь больше нет ни одного «живого» объекта, кто на него ссылался бы. А значит wr вполне может(а так оно и происходило) быть уничтожен во время выполнения DoCalculations и указатель, переданный в этот метод становится невалидным.

Если обернуть вызов DoCalculations в using (Wrapper wr = new Wrapper()){...}, то это решит проблему, поскольку вызов Dispose в блоке finally, не даст жадному сборщику мусора «съесть» объект раньше времени. Если же по какой-то причине мы не можем или не хотим вызывать Dispose (к примеру WPF этот шаблон совсем не жалует), то придется вставлять GC.KeepAlive(wr) после вызова DoCalculations.

Реальный код, безусловно, был сложнее и разглядеть в нем ошибку было не так просто, как в примере.

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

История 2



Жил-был проект, где для доступа к ресурсам использовался менеджер, который по строковому ключу доставал из заданной сборки различного вида ресурсы. С целью облегчения написания кода был написан следующего вида метод:
public string GetResource(string key)
    {
      Assembly assembly = Assembly.GetCallingAssembly();
      return this.GetResource(assembly, key);
    }


* This source code was highlighted with Source Code Highlighter.


После миграции на .Net 4 некоторые ресурсы внезапно перестали находиться. И дело тут опять же в оптимизации релизной версии. Дело в том, что в 4 версии дотнета компилятор может встраивать вызовы в код методов других сборок.

Чтобы «почувствовать разницу» предлагается следующий пример:

  dll1:
  public class Class1
  {
    public void Method1()
    {
      Console.WriteLine(new StackTrace());
    }
  }

  dll2:
  public class Class2
  {
    public void Method21()
    {
      this.Method22();
    }

    public void Method22()
    {
      (new Class1()).Method1();
    }
  }

  dll3:
  class Program
  {
    static void Main(string[] args)
    {
      (new Class3()).Method3();
    }
  }
  class Class3
  {
    public void Method3()
    {
      (new Class2()).Method21();
    }
  }


* This source code was highlighted with Source Code Highlighter.


Если скомпилировать в дебажной конфигурации(или если запускать процесс из-под студии) то получим честный стек вызовов:
в ClassLibrary1.Class1.Method1()
в ClassLibrary2.Class2.Method22()
в ClassLibrary2.Class2.Method21()
в ConsoleApplication1.Class3.Method3()
в ConsoleApplication1.Program.Main(String[] args)

Если собрать под .Net версии до 3.5 включительно в релизе:
в ClassLibrary1.Class1.Method1()
в ClassLibrary2.Class2.Method21()
в ConsoleApplication1.Program.Main(String[] args)

А под .Net 4 в релизной конфигурации то и вовсе получим:
в ConsoleApplication1.Program.Main(String[] args)

Мораль здесь проста — не стоит привязывать логику к стеку вызовов, равно как и удивляться необычному стеку в исключениях в логе релизной версии. В частности, если вы пытаетесь найти причину исключения исключительно по его стеку вызовов, то стоит учитывать, что если стек заканчивается на методе Method1, то в коде оно(исключение) могло быть сгенерировано в одном из небольших методов, которые вызываются в теле Method1.

Так же на всякий случай стоит помнить, что можно запретить компилятору встраивать метод пометив его атрибутом [MethodImpl(MethodImplOptions.NoInlining)], эдакий аналог __declspec(noinline) в VC++.

Вместо заключения



Мир проявляемых только в релизе багов воистину безграничен, и цели сделать полный его обзор я не ставил. Просто хотелось поделиться собственным опытом, точнее более интересной его частью. Ну и остается только пожелать читателям поменьше сталкиваться с подобными ошибками в работе.
Теги:
Хабы:
+57
Комментарии 27
Комментарии Комментарии 27

Публикации

Истории

Работа

.NET разработчик
74 вакансии

Ближайшие события