Подтвердить что ты не робот

Почему структура Entity Framework значительно медленнее при работе в другом AppDomain?

У нас есть служба Windows, которая загружает кучу плагинов (сборок) в свой собственный AppDomain. Каждый плагин привязан к "границе службы" в смысле SOA и поэтому отвечает за доступ к своей собственной базе данных. Мы заметили, что EF в 3 - 5 раз медленнее, когда в отдельном AppDomain.

Я знаю, что в первый раз, когда EF создает DbContext и попадает в базу данных, он должен выполнять некоторую работу по настройке, которая должна повторяться в AppDomain (т.е. не кэшироваться через AppDomains). Учитывая, что EF-код полностью автономный для плагина (и, следовательно, автономный для AppDomain), я ожидал, что тайминги будут сопоставимы с таймингами из родительского AppDomain. Почему они разные?

Попробовали настроить как .NET 4/EF 4.4, так и .NET 4.5/EF 5.

Пример кода

EF.csproj

Program.cs

class Program
{
    static void Main(string[] args)
    {
        var watch = Stopwatch.StartNew();
        var context = new Plugin.MyContext();
        watch.Stop();
        Console.WriteLine("outside plugin - new MyContext() : " + watch.ElapsedMilliseconds);

        watch = Stopwatch.StartNew();
        var posts = context.Posts.FirstOrDefault();
        watch.Stop();
        Console.WriteLine("outside plugin - FirstOrDefault(): " + watch.ElapsedMilliseconds);

        var pluginDll = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF.Plugin\bin\Debug\EF.Plugin.dll");
        var domain = AppDomain.CreateDomain("other");
        var plugin = (IPlugin) domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");

        plugin.FirstPost();

        Console.ReadLine();
    }
}

EF.Interfaces.csproj

IPlugin.cs

public interface IPlugin
{
    void FirstPost();
}

EF.Plugin.csproj

MyContext.cs

public class MyContext : DbContext
{
    public IDbSet<Post> Posts { get; set; }
}

Post.cs

public class Post
{
    public int Id { get; set; }
}

SamplePlugin.cs

public class SamplePlugin : MarshalByRefObject, IPlugin
{
    public void FirstPost()
    {
        var watch = Stopwatch.StartNew();
        var context = new MyContext();
        watch.Stop();
        Console.WriteLine(" inside plugin - new MyContext() : " + watch.ElapsedMilliseconds);

        watch = Stopwatch.StartNew();
        var posts = context.Posts.FirstOrDefault();
        watch.Stop();
        Console.WriteLine(" inside plugin - FirstOrDefault(): " + watch.ElapsedMilliseconds);
    }
}

Пример времени

Примечания:

  • Это запрос к пустой таблице базы данных - 0 строк.
  • Сроки преднамеренно смотрят только на первые вызовы. Последующие вызовы выполняются намного быстрее, но все же относительно 3 - 5 раз медленнее в дочернем AppDomain и родительском AppDomain.

Выполнить 1

    outside plugin - new MyContext() : 55
    outside plugin - FirstOrDefault(): 783
     inside plugin - new MyContext() : 352
     inside plugin - FirstOrDefault(): 2675

Выполнить 2

    outside plugin - new MyContext() : 53
    outside plugin - FirstOrDefault(): 798
     inside plugin - new MyContext() : 355
     inside plugin - FirstOrDefault(): 2687

Выполнить 3

    outside plugin - new MyContext() : 45
    outside plugin - FirstOrDefault(): 778
     inside plugin - new MyContext() : 355
     inside plugin - FirstOrDefault(): 2683

Исследование AppDomain

После некоторого дальнейшего исследования стоимости AppDomains, похоже, есть предположение, что последующие AppDomains должны повторно использовать системные DLL-системы, и поэтому при создании AppDomain существует первоначальная стоимость запуска. Это то, что здесь происходит? Я бы ожидал, что JIT-ing был бы на создании AppDomain, но, возможно, это EF JIT-ing, когда он вызывается?

Ссылка для повторной JIT: http://msdn.microsoft.com/en-us/magazine/cc163655.aspx#S8

Сроки звучат одинаково, но не уверены, связаны ли они: Первое соединение WCF, созданное в новом AppDomain, очень медленное

Обновление 1

Основываясь на предположении @Yasser о наличии связи EF через AppDomains, я попытался выделить это дальше. Я не считаю, что это так.

Я полностью удалил ссылку EF из EF.csproj. У меня теперь достаточно репутации для отправки изображений, поэтому это структура решения:

EF.sln

Как вы можете видеть, только плагин имеет ссылку на Entity Framework. Я также подтвердил, что только плагин имеет папку bin с EntityFramework.dll.

Я добавил помощника, чтобы проверить, была ли загружена сборка EF в AppDomain. Я также проверил (не показан), что после вызова в базу данных также загружаются дополнительные сборки EF (например, динамический прокси).

Итак, проверяя, загружен ли EF в разных точках:

  • В Main перед вызовом плагина
  • В плагине перед удалением базы данных
  • В плагине после попадания в базу данных
  • В Main после вызова плагина

... производит:

Main - IsEFLoaded: False
Plugin - IsEFLoaded: True
Plugin - new MyContext() : 367
Plugin - FirstOrDefault(): 2693
Plugin - IsEFLoaded: True
Main - IsEFLoaded: False

Итак, кажется, что AppDomains полностью изолированы (как и ожидалось), а тайминги одинаковы внутри плагина.

Обновленный пример кода

Program.cs

class Program
{
    static void Main(string[] args)
    {
        var dir = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF.Plugin\bin\Debug");
        var evidence = new Evidence();
        var setup = new AppDomainSetup { ApplicationBase = dir };
        var domain = AppDomain.CreateDomain("other", evidence, setup);
        var pluginDll = Path.Combine(dir, "EF.Plugin.dll");
        var plugin = (IPlugin) domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");

        Console.WriteLine("Main - IsEFLoaded: " + Helper.IsEFLoaded());
        plugin.FirstPost();
        Console.WriteLine("Main - IsEFLoaded: " + Helper.IsEFLoaded());

        Console.ReadLine();
    }
}

Helper.cs

(Да, я не собирался добавлять еще один проект для этого...)

public static class Helper
{
    public static bool IsEFLoaded()
    {
        return AppDomain.CurrentDomain
            .GetAssemblies()
            .Any(a => a.FullName.StartsWith("EntityFramework"));
    }
}

SamplePlugin.cs

public class SamplePlugin : MarshalByRefObject, IPlugin
{
    public void FirstPost()
    {
        Console.WriteLine("Plugin - IsEFLoaded: " + Helper.IsEFLoaded());

        var watch = Stopwatch.StartNew();
        var context = new MyContext();
        watch.Stop();
        Console.WriteLine("Plugin - new MyContext() : " + watch.ElapsedMilliseconds);

        watch = Stopwatch.StartNew();
        var posts = context.Posts.FirstOrDefault();
        watch.Stop();
        Console.WriteLine("Plugin - FirstOrDefault(): " + watch.ElapsedMilliseconds);

        Console.WriteLine("Plugin - IsEFLoaded: " + Helper.IsEFLoaded());
    }
}

Обновление 2

@Yasser: System.Data.Entity загружается в плагин только после попадания в базу данных. Изначально в плагин загружается только EntityFramework.dll, но также загружаются другие сборки EF после базы данных:

Loaded assemblies

Zipped solution. Сайт хранит файлы только в течение 30 дней. Не стесняйтесь предлагать лучший сайт для обмена файлами.

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

Обновление 3

Чтобы быть ясным, это первые тайминги звонков, которые меня интересуют в анализе, который включает запуск EF. При первом вызове переход от ~ 800 мс в родительском AppDomain до ~ 2700 мс в дочернем приложении AppDomain очень заметен. При последующих вызовах переход от ~ 1 мс до ~ 3 мс практически не заметен. Почему первый вызов (включая запуск EF) намного дороже внутри дочерних приложений AppDomains?

Ive обновил образец, чтобы сосредоточиться только на вызове FirstOrDefault(), чтобы уменьшить шум. Некоторые тайминги для запуска в родительском AppDomain и запуске в 3 дочерних AppDomains:

EF.vshost.exe|0|FirstOrDefault(): 768
EF.vshost.exe|1|FirstOrDefault(): 1
EF.vshost.exe|2|FirstOrDefault(): 1

AppDomain0|0|FirstOrDefault(): 2623
AppDomain0|1|FirstOrDefault(): 2
AppDomain0|2|FirstOrDefault(): 1

AppDomain1|0|FirstOrDefault(): 2669
AppDomain1|1|FirstOrDefault(): 2
AppDomain1|2|FirstOrDefault(): 1

AppDomain2|0|FirstOrDefault(): 2760
AppDomain2|1|FirstOrDefault(): 3
AppDomain2|2|FirstOrDefault(): 1

Обновленный пример кода

    static void Main(string[] args)
    {
        var mainPlugin = new SamplePlugin();

        for (var i = 0; i < 3; i++)
            mainPlugin.Do(i);

        Console.WriteLine();

        for (var i = 0; i < 3; i++)
        {
            var plugin = CreatePluginForAppDomain("AppDomain" + i);

            for (var j = 0; j < 3; j++)
                plugin.Do(j);

            Console.WriteLine();
        }

        Console.ReadLine();
    }

    private static IPlugin CreatePluginForAppDomain(string appDomainName)
    {
        var dir = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF.Plugin\bin\Debug");
        var evidence = new Evidence();
        var setup = new AppDomainSetup { ApplicationBase = dir };
        var domain = AppDomain.CreateDomain(appDomainName, evidence, setup);
        var pluginDll = Path.Combine(dir, "EF.Plugin.dll");
        return (IPlugin) domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");
    }

public class SamplePlugin : MarshalByRefObject, IPlugin
{
    public void Do(int i)
    {
        var context = new MyContext();

        var watch = Stopwatch.StartNew();
        var posts = context.Posts.FirstOrDefault();
        watch.Stop();
        Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + "|" + i + "|FirstOrDefault(): " + watch.ElapsedMilliseconds);
    }
}

Zipped-решение. Сайт хранит файлы только в течение 30 дней. Не стесняйтесь предлагать лучший сайт для обмена файлами.

4b9b3361

Ответ 1

Это, по-видимому, просто стоимость дочерних AppDomains. A довольно древний пост (который уже не может быть релевантным) предполагает, что могут быть другие соображения, не связанные с JIT-компиляцией каждого дочернего AppDomain, например оценка политик безопасности.

Entity Framework имеет относительно высокую начальную стоимость, поэтому эффекты увеличиваются, но для сопоставления других частей System.Data(например, прямой SqlDataReader) так же ужасно:

EF.vshost.exe|0|SqlDataReader: 67
EF.vshost.exe|1|SqlDataReader: 0
EF.vshost.exe|2|SqlDataReader: 0

AppDomain0|0|SqlDataReader: 313
AppDomain0|1|SqlDataReader: 2
AppDomain0|2|SqlDataReader: 0

AppDomain1|0|SqlDataReader: 290
AppDomain1|1|SqlDataReader: 3
AppDomain1|2|SqlDataReader: 0

AppDomain2|0|SqlDataReader: 316
AppDomain2|1|SqlDataReader: 2
AppDomain2|2|SqlDataReader: 0
public class SamplePlugin : MarshalByRefObject, IPlugin
{
    public void Do(int i)
    {
        var watch = Stopwatch.StartNew();
        using (var connection = new SqlConnection("Data Source=.\\sqlexpress;Initial Catalog=EF.Plugin.MyContext;Integrated Security=true"))
        {
            var command = new SqlCommand("SELECT * from Posts;", connection);
            connection.Open();
            var reader = command.ExecuteReader();
            reader.Close();
        }
        watch.Stop();

        Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + "|" + i + "|SqlDataReader: " + watch.ElapsedMilliseconds);
    }
}

Даже новичок в смиренном DataTable завышен:

EF.vshost.exe|0|DataTable: 0
EF.vshost.exe|1|DataTable: 0
EF.vshost.exe|2|DataTable: 0

AppDomain0|0|DataTable: 12
AppDomain0|1|DataTable: 0
AppDomain0|2|DataTable: 0

AppDomain1|0|DataTable: 11
AppDomain1|1|DataTable: 0
AppDomain1|2|DataTable: 0

AppDomain2|0|DataTable: 10
AppDomain2|1|DataTable: 0
AppDomain2|2|DataTable: 0
public class SamplePlugin : MarshalByRefObject, IPlugin
{
    public void Do(int i)
    {
        var watch = Stopwatch.StartNew();
        var table = new DataTable("");
        watch.Stop();

        Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + "|" + i + "|DataTable: " + watch.ElapsedMilliseconds);
    }
}

Ответ 2

Вы должны запустить этот тест несколько раз при запуске приложения

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

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

Вы можете увидеть эту проблему при разработке приложений в хранимых процедурах [SQL Server/.NET CLR], которые выполняются в отдельном домене приложения, а не в сервере sql server.

Ответ 3

Возможно, я ошибаюсь, но со следующим кодом:

public class SamplePlugin : MarshalByRefObject, IPlugin
{
    public void Do()
    {
        using (AppDb db = new AppDb())
        {
            db.Posts.FirstOrDefault();
        }
    }
}

и эти коды:

[LoaderOptimization(LoaderOptimization.MultiDomain)]
    static void Main(String[] args)
    {
        AppDomain.CurrentDomain.AssemblyLoad += CurrentDomain_AssemblyLoad;

        var dir = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF\bin\Debug");

        var evidence = new Evidence();

        var setup = new AppDomainSetup { ApplicationBase = dir };

        var domain = AppDomain.CreateDomain("Plugin", evidence, setup);

        domain.AssemblyLoad += domain_AssemblyLoad;

        var pluginDll = Path.Combine(dir, "EF.Plugin.dll");

        var anotherDomainPlugin = (IPlugin)domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");

        var mainDomainPlugin = new SamplePlugin();

        mainDomainPlugin.Do();    // To prevent side effects of entity framework startup from our test

        anotherDomainPlugin.Do(); // To prevent side effects of entity framework startup from our test

        Stopwatch watch = Stopwatch.StartNew();

        mainDomainPlugin.Do();

        watch.Stop();

        Console.WriteLine("Main Application Domain -------------------------- " + watch.ElapsedMilliseconds.ToString());

        watch.Restart();

        anotherDomainPlugin.Do();

        watch.Stop();

        Console.WriteLine("Another Application Domain -------------------------- " + watch.ElapsedMilliseconds.ToString());

        Console.ReadLine();
    }

    static void CurrentDomain_AssemblyLoad(Object sender, AssemblyLoadEventArgs args)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Main Domain : " + args.LoadedAssembly.FullName);
    }

    static void domain_AssemblyLoad(Object sender, AssemblyLoadEventArgs args)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine("Another Domain : " + args.LoadedAssembly.FullName);
    }

В этом сценарии нет реальной разницы в производительности между основным доменом приложения и другим доменом приложения. Вы получаете разные результаты, потому что ваши тесты неверны (-: (по крайней мере, я думаю, что они ошибаются), я также протестировал основное приложение домен, напрямую вызывая DbContext и первый или по умолчанию, Мои времена одинаковы, а разница между 1 - 2 миллисекундами, я не могу понять, почему мои результаты отличаются от ваших результатов.