c# - 为什么 Entity Framework 在不同的 AppDomain 中运行时速度明显变慢?

标签 c# .net entity-framework

我们有一个 Windows 服务,可以将一堆插件(程序集)加载到它们自己的 AppDomain 中。每个插件都与 SOA 意义上的“服务边界”对齐,因此负责访问自己的数据库。我们注意到,在单独的 AppDomain 中时,EF 的速度要慢 3 到 5 倍。

我知道 EF 第一次创建 DbContext 并访问数据库时,它必须执行一些必须针对每个 AppDomain 重复的设置工作(即不跨 AppDomain 缓存)。考虑到 EF 代码完全独立于插件(因此独立于 AppDomain),我希望时间与父 AppDomain 的时间相当。它们为什么不同?

已尝试同时针对 .NET 4/EF 4.4 和 .NET 4.5/EF 5。

示例代码

EF.csproj

程序.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; }
}

示例插件.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 行。
  • 时间安排有意只考虑第一次通话。与父 AppDomain 相比,子 AppDomain 中的后续调用速度,但仍然相对慢 3 到 5 倍。

运行 1


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

Run 2


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

Run 3


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

AppDomain research

After some further research in to the cost of AppDomains, there seems to be a suggestion that subsequent AppDomains have to re-JIT system DLLs and so there is an inherent start-up cost in creating an AppDomain. Is that what is happening here? I would have expected that the JIT-ing would have been on AppDomain creation, but perhaps it is EF JIT-ing when it is called?

Reference for re-JIT: http://msdn.microsoft.com/en-us/magazine/cc163655.aspx#S8

Timings sounds similar, but not sure if related: First WCF connection made in new AppDomain is very slow

Update 1

Based on @Yasser's suggestion that there is EF communication across the AppDomains, I tried to isolate this further. I don't believe this to be the case.

I have completely removed any EF reference from EF.csproj. I now have enough rep to post images, so this is the solution structure:

EF.sln

As you can see, only the plugin has a reference to Entity Framework. I have also verified that only the plugin has a bin folder with an EntityFramework.dll.

I have added a helper to verify if the EF assembly has been loaded in the AppDomain. I have also verified (not shown) that after the call to the database, additional EF assemblies (e.g. dynamic proxy) are also loaded.

So, checking if EF has loaded at various points:

  1. In Main before calling the plugin
  2. In Plugin before hitting the database
  3. In Plugin after hitting the database
  4. In Main after calling the plugin

... produces:

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

So it seems that the AppDomains are fully isolated (as expected) and the timings are the same inside the plugin.

Updated Sample code

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"));
    }
}

示例插件.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 启动。在第一次调用时,从父 AppDomain 中的 ~800ms 到子 AppDomain 中的 ~2700ms 是非常明显的。在后续调用中,从 ~1ms 到 ~3ms 几乎不会引起注意。为什么第一次调用(包括 EF 启动)在子 AppDomains 中的成本要高得多?

我更新了示例以仅关注 FirstOrDefault() 调用以减少噪音。在父 AppDomain 中运行和在 3 个子 AppDomain 中运行的一些时间安排:

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

Updated Sample Code

    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 solution .该站点仅将文件保留 30 天。欢迎推荐更好的文件共享网站。

最佳答案

这似乎只是子 AppDomain 的成本。 rather ancient post (这可能不再相关)表明除了必须对每个子 AppDomain 进行 JIT 编译之外,可能还有其他考虑因素,例如评估安全政策。

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);
    }
}

关于c# - 为什么 Entity Framework 在不同的 AppDomain 中运行时速度明显变慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18456491/

相关文章:

c# - Web 服务不断引用旧版本的 Entity Framework

c# - 如何使用 Entity Framework 执行插入查询

c# - IComparer 不对简单的 a b c 进行排序?

c# - 如何以编程方式设置 TARGETDIR?

.net - 从.NET调用Clojure

c# - 并发读写 NamePipeClientStream

c# - 如何用几行代码产生 StackOverflowException?

c# - 传递实例对象时事件订阅丢失

c# - SharePoint - 将文件夹下载为 zip 文件

c# - 实体泛型 dbSet 获取数据表