VB.NETでも処理を高速化したい

はじめに

まぁ、今回のテーマは高速化なんですけど『こうすればコードが超高速化される1111111』みたいな即効的なテクニックっていうよりはパフォーマンスに問題がある場合はこうして問題を解決すればいいんじゃないかな〜的なトピックです。

ちなみに途中で処理時間を計測したりしている部分があるのですが弊社の環境は以下の通りですのでその辺もパフォーマンス測定に影響しているかもなのであまり鵜呑みにしない方がいいです。

  • MacBook Pro Mid 2012上で動いているMac OS 10.10上で動いている VMware Fusion 7 Pro 上で動いているWindows 8.1 Pro 64bit
  • 仮装マシンに割り当てているコア数:論理2コア
  • 仮装マシンに割り当ててるメモリ:8ギガバイト

また、ここで議論するのはフォームやWPFなどのクライアントサイドで動く生産性アプリケーションを想定してます。 サーバサイドや科学技術計算、ゲームなどは対象としてませんのでごめんなさい。

と言っても、以下のmsdnページに必要なことはほぼ全て書いてあるので信頼のおける情報だけが欲しい人はこちらをどうぞ。

Visual Basic .NET でのパフォーマンスの最適化

書かれたのが結構昔ですが、書いてある事柄は今でも十分に通用すると思います。

ただ、プロパティのキャッシュはちょっとどうかなって思ったりします。

パフォーマンス測定

実装したアプリケーションが遅い時、いきなり高速化をしようとするのはおすすめできません。 まずは実行に時間がかかっている箇所を探すところから始めましょう。

ちなみに名前を忘れてしまいましたが、『全体の20%のコードが全体の80パーセントの処理時間を消費している』的な経験則があります。 この経験則があなたのアプリケーションにも適用できる場合、残りの80パーセントをいくら最適化してもアプリケーションは全体の80%までしか高速化できません。 逆に20%のコードの処理時間を半分にするだけで60%まで高速化が可能です。

とにかく、アプリケーションが遅い場合はいきなり最適化をせずまずは何が遅いのかを詳しく調べるべきです。 でなければそこまで処理に時間を要していない部分の最適化をして、労力の割には早くならないといった事が起こり得ます。

遅延バインディング

単体で数回程度行われる遅延バインディングはそこまでパフォーマンスには影響を及ぼしません。 しかしながらループ内でアホみたいに呼び出される遅延バインディングは話が違います。

以下のコードで見てみましょう。

Module Module1

    ReadOnly ARRAY_MAX As Integer = 1000000

    Sub Main()
        Dim hogeArray(ARRAY_MAX) As Hoge
        Dim objectArray(ARRAY_MAX) As Object

        For i = 0 To ARRAY_MAX
            Dim h = New Hoge() With {.A = i, .B = ARRAY_MAX - i}
            hogeArray(i) = h
            objectArray(i) = h
        Next

        Dim r1 = Add(hogeArray)
        Dim r2 = Add(objectArray)
    End Sub

    Function Add(array As Hoge()) As Integer()
        Dim result(ARRAY_MAX) As Integer
        Dim watch = New Stopwatch()

        watch.Start()
        For i = 0 To ARRAY_MAX
            Dim h = array(i)
            Dim r = h.A + h.B
            result(i) = r
        Next
        watch.Stop()

        Console.WriteLine("Add(array as Hoge()) : {0}ms", watch.ElapsedMilliseconds)
        Return result
    End Function

    Function Add(array As Object()) As Integer()
        Dim result(ARRAY_MAX) As Integer
        Dim watch = New Stopwatch()

        watch.Start()
        For i = 0 To ARRAY_MAX
            Dim h = array(i)
            Dim r = h.A + h.B
            result(i) = r
        Next
        watch.Stop()

        Console.WriteLine("Add(array as Object()) : {0}ms", watch.ElapsedMilliseconds)
        Return result
    End Function

End Module

Class Hoge
    Public Property A As Integer
    Public Property B As Integer
End Class

ループ内で通常のプロパティ呼び出しと遅延バインディングでの呼び出しをそれぞれ1000001回行っています。 結果はと言いますと

Add(array as Hoge()) : 20ms
Add(array as Object()) : 8266ms

とまぁ、2桁も違う結果となりました。

単体では正直実行時間の差がわからない遅延バインディングですが、ループ内で呼び出す場合は毎回リフレクションが走りその分のオーバーヘッドがちりつもやまとなでことなるので計算を行うメソッド内、特にループ内で遅延バインディングとならないよう気をつけて下さい。

ディスクとIO

IOはCPUより何倍も遅いです。従って断続的にIOからデータを読み込んだり書き込んだりするとそこがボトルネックとなる事があります。 特に書き込む場合は、処理能力的にはCPUでの処理が先行できるはずなのにディスクに書き込みが終わらないばかりにCPUが遊ぶ事があります。 その場合はディスクへの書き込みは別のスレッドで実行し書き込みを計算スレッドから非同期で行う事を検討できます。 適切なバッファサイズを設定できればIOを待たずにCPUが先行して処理する事ができます。

ネットワークは知らん。

並列化

ループで繰り返し行うタスクで、前後のタスクに依存関係がない場合は複数のスレッドで同時に実行させる事ができます。 まぁ、.NETですと直接スレッドを扱わずにカジュアルにパラレルで処理ができるライブラリがありますのでそれを利用するのも手です。

Parallel Processing and Concurrency in the .NET Framework

ただ、並列化する場合はいろんな事に気を使わないと計算結果がマッハでおかしくなったりロックの連発で思った性能が出ない事があるんで、まぁ、頑張ってください。 そもそもクライアントサイドでヘビーな処理を走らせるんかという疑問もありますけど。

小手先だけのテクニックは多用しない

アルゴリズム的な解決ではなく、小手先だけのテクニックで高速化はするべきではないです。 確かに処理が遅くなるようなパターンを把握しておきそれを回避するのは良いことです。 しかしプログラマアルゴリズム的に高速になるように考慮すべきで、コードの最適化はコンパイラに任せるべきです。

言い換えると、コードを最適化するために可読性を下げるべきではないです。 書いた直後は良いですが、すぐにわけわかめになる恐れがあります。

まとめ

なんだか全体的に纏まりのない記事となってしまいました。 まぁ、高速化する場合は以下の事に気をつけてねってことで。

  • いきなり最適化を始める前に十分に調査を行って見当をつけてから高速化しましょう。
    • でないと労力の割にあまり早くならない
  • 遅延バインディングは遅いからループ内では呼ぶな
    • ちりつもやまとなでこになる
  • ある種の問題では並列化するだけの価値はある
    • だだし、生半可な気持ちで並列化に手を出すと割と危ない
  • アルゴリズムでない、小手先だけのテクニックは多用しない
    • 可読性が低くなって後で保守するときに死ぬ

おわり

おまけ

jyuch.hatenablog.com