ページ

2012年6月1日金曜日

[VS2012] async/await のパフォーマンスの注意点

前の記事 で紹介した 「What’s New for Parallelism in Visual Studio 2012 RC」 の補足記事が来てました。

Performance consideration for Async/Await and MarshalByRefObject
これ、個人的にはものすごく重要なことのような気ガス

前の記事にあった StreamReader.ReadLineAsync メソッドが 3倍速くなったとかはどういうことなのか?
詳しくは上の記事を読んでもらった方がいいと思いますが、ざっくりと説明します。
上記の記事からコードの重要な部分をコピペします。

class MyObj 
{ 
    const int ITERS = 100000000; 
    private int m_data; 

    public async Task Foo1() 
    { 
        for (int i = 0; i < ITERS; i++) m_data++; 
    } 

    public async Task Foo2() 
    { 
        int localData = m_data; 
        for (int i = 0; i < ITERS; i++) localData++; 
        m_data = localData; 
    } 
}

この Foo1 メソッドと Foo2 メソッドの実行速度を調べると Foo1 の方が 3倍くらい遅いそうです。
なぜか?
async なメソッドはコンパイラによって中身がゴニョゴニョされて await が使用できるようなコードに変換されます。(メソッドの中身が IAsyncStateMachine を継承した別クラスに切りだされて、await ごとに状態遷移するようなステートマシンなコードになる) コード上では単なるフィールドへのアクセス(m_data へのアクセス)に見えますが、コンパイル結果は単なるアクセスじゃなくなっているわけです。そのため、その分遅くなってしまうということだそうです。

さらに

class MyObj

class MyObj : MarshalByRefObject

とすると差が大きくなります。
実に Foo1 は Foo2 の 72倍くらい遅くなってしまいます。Foo2 の方は MarshalByRefObject であっても無くてもほとんど速度は変わりません。
これは、どうやら以下の理由だそうです。
MarshalByRefObject を継承したクラスはリモートアクセスが可能になります。リモートアクセスする場合はプロキシーが生成され引数などがマーシャリングされて渡されるわけですが、これはかなり重い処理です。なので無駄にプロキシー経由にならないようになっています。プロキシーが必要かどうかは 「別 AppDomain かどうか」 で判断します。しかし、この判断自体もオーバーヘッドになってしまいます。なので、JIT は自分自身(すなわち “this”)にアクセスする場合は絶対に同じ AppDomain だとしてチェックを省略し、普通にローカルなフィールドにアクセスするのと同じ速度になるようにしています。
ところが、async なメソッドになると上に書いたようにメソッドの中身が別クラスに切り出されるため m_data へのアクセスは this へのアクセスではなくなります。そのため m_data にアクセスするたびに 「同じ AppDomain かどうかのチェック」 をしなくちゃいけなくなります。これがオーバーヘッドになって 72倍という速度差になります。(ちなみに、もし別 AppDomain になって、プロキシー経由のアクセスになると数百倍くらいの速度差にはなるんじゃないかと思います。COM のころはそんな感じでした)
ちなみに、Stream や TextReader、TextWriter といったクラスは MarshalByRefObject から派生しています。

これはプロパティにするとちょっとましになるそうです。

private int Data { get { return m_data; } set { m_data = value; } } 

public async Task Foo3() 
{ 
    for (int i = 0; i < ITERS; i++) Data++; 
}

意味的には Foo1 とまったく同じですが、JIT が最適化するヒントになって Foo3 は Foo2 に比べて 10倍くらい遅い(Foo1 に比べて6倍ちょっと速い)となるそうです。

まぁ、こうしても遅くなるのは確かなので結局のところ 「async なメソッドでは極力フィールドやプロパティにアクセスせず、可能な限りローカル変数アクセスになるようにする」 ということになりそうです。
別の意味でもなるべくローカル変数になるようにした方が安全です。
async なメソッドで await を使うと非同期で動くようになるわけですからそこからフィールドやプロパティにアクセスするときは排他を考えてやらなくちゃいけません。async/await を使うとあまりにお手軽に非同期できちゃうので忘れがちになってしまいますが、非同期で動いている以上、外部のリソースにアクセスする場合は常に 「別の非同期メソッドとの読み書きがバッティングして内容が破壊されるようなことは無いか」 を考えてやらなくちゃいけません。ローカル変数は外部ではありませんので、そういったややこしいことを考える必要が無くなります。(注意: 別のクラスのインスタンスをローカル変数に持っていて、そのインスタンスが別の非同期メソッドからアクセスされるような場合は当然ちゃんと考えてやらなくちゃいけません。ローカル変数ならなんでも大丈夫ってわけじゃありませんのでご注意を)

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。