28

さまざまなラムダ式の構文を使用して、パフォーマンスの違いをテストしています。簡単な方法がある場合:

public IEnumerable<Item> GetItems(int point)
{
    return this.items.Where(i => i.IsApplicableFor(point));
}

pointラムダの観点からは自由変数であるため、パラメータに関連する変数リフティングがここで行われています。このメソッドを 100 万回呼び出すとしたら、そのままにしておくか、何らかの方法で変更してパフォーマンスを向上させる方がよいでしょうか?

どのようなオプションがあり、どれが実際に実行可能ですか? 私が理解しているように、フリー変数を取り除く必要があるため、コンパイラはクロージャ クラスを作成して、このメソッドを呼び出すたびにインスタンス化する必要がありません。通常、このインスタンス化には、非クロージャ バージョンと比較してかなりの時間がかかります。

問題は、大ヒットのラムダ式を書くたびに時間を無駄にしているように見えるので、一般的に機能するある種のラムダ記述ガイドラインを考え出したいということです。従うべきルールがわからないため、動作することを確認するために手動でテストする必要があります。

代替方法

& サンプル コンソール アプリケーション コード

また、変数のリフティングを必要としない同じメソッドの別のバージョンも作成しました (少なくともそうではないと思いますが、これを理解している皆さんは、その場合はお知らせください)。

public IEnumerable<Item> GetItems(int point)
{
    Func<int, Func<Item, bool>> buildPredicate = p => i => i.IsApplicableFor(p);
    return this.items.Where(buildPredicate(point));
}

ここで要旨をチェックしてください。コンソール アプリケーションを作成し、コード全体をブロックProgram.cs内のファイルにコピーするだけです。namespace2 番目の例は、自由変数を使用していませんが、はるかに遅いことがわかります。

矛盾した例

ラムダの最適な使用ガイドラインを作成したい理由は、以前にこの問題に遭遇したことがあり、驚いたことに、述語ビルダーのラムダ式を使用すると、より高速に動作することが判明したためです。

では、それを説明してください。コードに頻繁に使用するメソッドがあることがわかっている場合、ラムダをまったく使用しないことが判明する可能性があるため、ここで完全に迷っています。でも、そんな事態は避けて、真相を究明したい。

編集

あなたの提案はうまくいかないようです

コンパイラが自由変数ラムダで行うのと同様に内部的に動作するカスタム ルックアップ クラスを実装しようとしました。ただし、クロージャー クラスを使用する代わりに、同様のシナリオをシミュレートするインスタンス メンバーを実装しました。これはコードです:

private int Point { get; set; }
private bool IsItemValid(Item item)
{
    return item.IsApplicableFor(this.Point);
}

public IEnumerable<TItem> GetItems(int point)
{
    this.Point = point;
    return this.items.Where(this.IsItemValid);
}

興味深いことに、これは遅いバージョンと同じくらい遅く動作します。理由はわかりませんが、速いもの以外は何もしていないようです。これらの追加メンバーは同じオブジェクト インスタンスの一部であるため、同じ機能を再利用します。ともかく。私は今、非常に混乱しています!

この最新の追加でGist ソースを更新したので、自分でテストできます。

4

4 に答える 4

3

2 番目のバージョンでは変数のリフティングが必要ないと考える理由は何ですか? Lambda 式を使用して を定義していますがFunc、これには、最初のバージョンで必要だったのと同じコンパイラのトリッキーが必要になります。

さらに、Funcを返すを作成していますがFunc、これは私の脳を少し曲げており、ほぼ確実に各呼び出しで再評価が必要になります。

これをリリース モードでコンパイルしてから、ILDASM を使用して生成された IL を調べることをお勧めします。これにより、どのコードが生成されるかについての洞察が得られるはずです。

より多くの洞察を得るために実行する必要があるもう 1 つのテストは、クラス スコープで変数を使用する別の関数を述語呼び出しにすることです。何かのようなもの:

private DateTime dayToCompare;
private bool LocalIsDayWithinRange(TItem i)
{
    return i.IsDayWithinRange(dayToCompare);
}

public override IEnumerable<TItem> GetDayData(DateTime day)
{
    dayToCompare = day;
    return this.items.Where(i => LocalIsDayWithinRange(i));
}

これにより、day変数を巻き上げると実際にコストがかかるかどうかがわかります。

はい、これにはより多くのコードが必要であり、使用することはお勧めしません。同様のことを示唆した以前の回答への回答で指摘したように、これにより、ローカル変数を使用したクロージャーに相当するものが作成されます。ポイントは、物事を機能させるために、あなたかコンパイラーのどちらかがこのようなことをしなければならないということです。純粋な反復ソリューションを作成する以外に、コンパイラがこれを行う必要がないようにするために実行できる魔法はありません。

ここでの私のポイントは、私の場合の「クロージャーの作成」は単純な変数の割り当てであるということです。これが Lambda 式を使用したバージョンよりも大幅に高速である場合は、コンパイラーがクロージャーのために作成するコードに非効率性があることがわかります。

自由変数を排除しなければならないこと、および閉鎖のコストに関する情報をどこで入手しているのかわかりません。いくつか参考にしていただけますか?

于 2011-11-29T04:19:18.687 に答える
1

あなたの2番目の方法は、最初の方法よりも8倍遅くなります。@DanBryant がコメントで述べているように、これはメソッド内でデリゲートを構築して呼び出すことであり、変数のリフティングではありません。

2番目のサンプルが最初のサンプルよりも高速であると予想していたように、あなたの質問は混乱しています。また、「可変リフティング」が原因で、最初のものが許容できないほど遅いため、それを読みました。2 番目のサンプルにはまだ自由変数 ( point) がありますが、追加のオーバーヘッドが追加されます。自由変数が削除されると考える理由がわかりません。

あなたが投稿したコードが確認するように、上記の最初のサンプル (単純なインライン述語を使用) は、単純なforループよりも jsut を 10% 遅く実行します - コードから:

foreach (TItem item in this.items)
{
    if (item.IsDayWithinRange(day))
    {
        yield return item;
    }
}

つまり、要約すると:

  • ループは最もfor単純なアプローチであり、「最良のケース」です。
  • インライン述語は、追加のオーバーヘッドが原因で、わずかに遅くなります。
  • 各反復内Funcで返されるa の構築と呼び出しは、どちらよりも大幅に遅くなります。Func

どれも驚くべきことではないと思います。「ガイドライン」は、インライン述語を使用することです。パフォーマンスが悪い場合は、ストレート ループに移動して単純化します。

于 2011-11-29T04:49:30.630 に答える
0

私はあなたのためにあなたのベンチマークをプロファイリングし、多くのことを決定しました:

まず、 をreturn this.GetDayData(day).ToList();呼び出す回線に半分の時間を費やしToListます。それを削除し、代わりに結果を手動で反復すると、メソッドの違いを相対的に測定できます。

2 つ目は、IterationCount = 1000000とであるため、さまざまなメソッドの実行にかかる時間ではなく、さまざまなメソッドの初期化RangeCount = 1のタイミングを計っています。これは、イテレータの作成、変数レコードのエスケープ、およびデリゲートに加えて、そのすべてのガベージの作成から生じる何百もの後続の gen0 ガベージ コレクションによって、実行プロファイルが支配されることを意味します。

第 3 に、「遅い」方法は x86 では非常に遅くなりますが、x64 では「速い」方法とほぼ同じ速さです。これは、さまざまな JITter がデリゲートを作成する方法によるものだと思います。結果からデリゲートの作成を割り引くと、「速い」方法と「遅い」方法の速度は同じです。

第 4 に、イテレータを実際に何度も呼び出すと (私のコンピューターでは、x64 をターゲットにして、RangeCount = 8.

結論として、「リフティング」の側面は無視できます。私のラップトップでテストすると、ラムダが作成されるたびに (呼び出されるたびにではなく) 追加の10nsが必要であり、追加の GC オーバーヘッドが含まれていることがわかります。さらに、 「foreach」メソッドで反復子を作成するのはラムダを作成するよりもいくらか高速ですが、実際にはその反復子を呼び出すのはラムダを呼び出すよりも遅くなります。

デリゲートの作成に必要な数ナノ秒の余分な時間がアプリケーションにとって長すぎる場合は、それらをキャッシュすることを検討してください。これらのデリゲート (つまりクロージャー) にパラメーターが必要な場合は、独自のクロージャー クラスを作成して、デリゲートを再利用する必要があるときにプロパティを変更することを検討してください。次に例を示します。

public class SuperFastLinqRangeLookup<TItem> : RangeLookupBase<TItem>
    where TItem : RangeItem
{

    public SuperFastLinqRangeLookup(DateTime start, DateTime end, IEnumerable<TItem> items)
        : base(start, end, items)
    {
        // create delegate only once
        predicate = i => i.IsDayWithinRange(day);
    }

    DateTime day;
    Func<TItem, bool> predicate;

    public override IEnumerable<TItem> GetDayData(DateTime day)
    {
        this.day = day; // set captured day to correct value
        return this.items.Where(predicate);
    }
}
于 2011-11-29T04:53:08.343 に答える