Почему структура 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 дней. Не стесняйтесь предлагать лучший сайт для обмена файлами.
Ответы
Ответ 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 миллисекундами, я не могу понять, почему мои результаты отличаются от ваших результатов.