27

一連のプラグイン (アセンブリ) を独自の AppDomain にロードする Windows サービスがあります。各プラグインは、SOA の意味での「サービス境界」に合わせて調整されているため、独自のデータベースへのアクセスを担当します。別の AppDomain では、EF が 3 倍から 5 倍遅くなることがわかりました。

EF が初めて DbContext を作成してデータベースにアクセスするとき、AppDomain ごとに繰り返す必要がある (つまり、AppDomain 間でキャッシュされない) いくつかのセットアップ作業を行う必要があることを私は知っています。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 行) に対してクエリを実行しています。
  • タイミングは意図的に最初の呼び出しだけを見ています。後続の呼び出しははるかに高速ですが、親 AppDomain と比較して、子 AppDomain では相対的に 3 倍から 5 倍遅くなります。

実行 1

    外部プラグイン - new MyContext() : 55
    外部プラグイン - FirstOrDefault(): 783
     プラグイン内 - new MyContext() : 352
     プラグイン内 - FirstOrDefault(): 2675

実行 2

    外部プラグイン - new MyContext() : 53
    外部プラグイン - FirstOrDefault(): 798
     プラグイン内 - new MyContext() : 355
     プラグイン内 - FirstOrDefault(): 2687

実行 3

    外部プラグイン - new MyContext() : 45
    外部プラグイン - FirstOrDefault(): 778
     プラグイン内 - new MyContext() : 355
     プラグイン内 - FirstOrDefault(): 2683

AppDomain 調査

AppDomains のコストをさらに調査した結果、後続の AppDomains はシステム DLL を再 JIT する必要があるため、AppDomain の作成には固有の起動コストがかかるという提案があるようです。それがここで起こっていることですか?JIT 処理は AppDomain の作成時に行われると予想していましたが、呼び出されたときはおそらく EF JIT 処理でしょうか?

再 JIT のリファレンス: http://msdn.microsoft.com/en-us/magazine/cc163655.aspx#S8

タイミングは似ていますが、関連があるかどうかはわかりません: 新しい AppDomain で作成された最初の WCF 接続が非常に遅い

更新 1

AppDomains全体でEF通信があるという@Yasserの提案に基づいて、これをさらに分離しようとしました。私はこれが事実だとは思わない。

EF.csproj から EF 参照を完全に削除しました。画像を投稿するのに十分な担当者ができたので、これがソリューションの構造です。

EF.sln

ご覧のとおり、プラグインのみが Entity Framework への参照を持っています。また、プラグインのみが EntityFramework.dll を含む bin フォルダーを持っていることも確認しました。

EF アセンブリが AppDomain に読み込まれているかどうかを確認するヘルパーを追加しました。また、データベースへの呼び出し後、追加の EF アセンブリ (動的プロキシなど) も読み込まれることを確認しました (表示されていません)。

したがって、EF がさまざまな時点で読み込まれたかどうかを確認します。

  1. プラグインを呼び出す前にメインで
  2. データベースにアクセスする前のプラグイン
  3. データベースにアクセスした後のプラグイン
  4. プラグインを呼び出した後のメイン

... を生成します:

メイン - IsEFLoaded: False
プラグイン - IsEFLoaded: True
プラグイン - new MyContext() : 367
プラグイン - FirstOrDefault(): 2693
プラグイン - IsEFLoaded: True
メイン - 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 アセンブリも読み込まれます。

読み込まれたアセンブリ

圧縮されたソリューション。このサイトはファイルを 30 日間しか保持しません。より良いファイル共有サイトを提案してください。

また、メイン プロジェクトで EF を参照し、元のサンプルのタイミング パターンが再現可能かどうかを確認することで、私の調査結果を検証できるかどうかを知りたいです。

アップデート 3

明確にするために、私が分析に興味を持っているのは、EF の起動を含む最初の呼び出しのタイミングです。最初の呼び出しで、親 AppDomain の ~800 ミリ秒から子 AppDomain の ~2700 ミリ秒になることは非常に顕著です。後続の呼び出しでは、1 ミリ秒から 3 ミリ秒になることはほとんど目立ちません。最初の呼び出し (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

更新されたサンプル コード

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

圧縮されたソリューション。このサイトはファイルを 30 日間しか保持しません。より良いファイル共有サイトを提案してください。

4

3 に答える 3

4

これは、子 AppDomains のコストにすぎないようです。かなり古い投稿(これはもはや関連性がない可能性があります) は、セキュリティ ポリシーの評価など、各子 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|データ テーブル: 0
EF.vshost.exe|1|データ テーブル: 0
EF.vshost.exe|2|データ テーブル: 0

AppDomain0|0|データテーブル: 12
AppDomain0|1|データテーブル: 0
AppDomain0|2|データテーブル: 0

AppDomain1|0|データテーブル: 11
AppDomain1|1|データテーブル: 0
AppDomain1|2|データテーブル: 0

AppDomain2|0|データテーブル: 10
AppDomain2|1|データテーブル: 0
AppDomain2|2|データテーブル: 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);
    }
}
于 2013-09-02T23:45:39.313 に答える
3

アプリケーションを起動するときに、そのテストを数回実行する必要があります

初回以降のパフォーマンスの違いは、メイン アプリケーション ドメインとプラグイン アプリケーション ドメインの間のオブジェクトのシリアル化に関するものです。

アプリケーション ドメイン間の各通信には、コストがかかりすぎるシリアライゼーションとデシリアライゼーションが必要であることに注意してください。

この問題は、[SQL Server / .NET CLR] ストアド プロシージャでアプリケーションを開発しているときに発生します。このストアド プロシージャは、SQL サーバー エンジンではなく、分離されたアプリケーション ドメインで実行されます。

于 2013-08-27T04:30:37.610 に答える
0

多分私は間違っているかもしれませんが、次のコードで:

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 と first または default を呼び出します。私の時間は同じで、違いは 1 ~ 2 ミリ秒です。なぜ私の結果があなたの結果と異なるのか理解できません

于 2013-08-31T10:15:31.343 に答える