連載:熱血VBプログラマ応援団


第8回 Visual Basic .NETは遅いのか!?

―― 起動の遅さも事前にJITコンパイルで解決可能! ――

株式会社ピーデー 川俣 晶
2004/07/07
 

− 今回のご相談 −

 現在、Visual Basic 6.0でたいていのプログラムを書くことができています。しかし、永遠にVisual Basic 6.0を使い続けることができないことは理解しています。近い将来、Visual Basic .NETに移行しなければならないだろうという覚悟もできているつもりです。しかし、それが本当に進歩になるのか、主に性能面で疑問があります。

 まず、せっかくVisual Basic 6.0でネイティブ・コードへのコンパイルが可能になり、高速なプログラムを作成できていたのに、Visual Basic .NETではネイティブ・コードのコンパイルがサポートされないことが不安です。再びインタープリタの世界に逆戻りでは厳しい感じです。

 それから、いくつかVisual Basic .NETで開発したプログラムを試用していますが、実行しようとしてもすぐには起動せず、待たされるケースがあります。起動が遅いとしたら、これも大きな問題です。これはインタープリタを読み込む待ち時間なのでしょうか。ネイティブ・コードのコンパイルができれば問題ないのに、と思います。

 こういったことから考えると、Visual Basic .NETに移行することにためらいを感じてしまいます。少なくとも、Visual Basic 6.0並みのコンパイル機能がそろうまでは移行したくないと思うのは間違いでしょうか?

愛犬ぶいハチの飼い主 より

Visual Basic 6.0はネイティブ・コード・コンパイラを持つ

 愛犬ぶいハチの飼い主さんのような不安を感じる人は多いようです。しかし、ネイティブ・コードのコンパイラがサポートされないからといって、インタープリタの時代に逆戻り、と思うのは早計です。論より証拠、Visual Basic .NET(以下、VB.NET)が決して遅くないということは、実際にテスト・プログラムを実行して確認するのがよいと思います。ここでは実際に筆者が小さなプログラムを実行した結果から、決してVB.NETは遅くなく、起動時の待ち時間も解消できることを示すことにします。

 さて本題に入る前に、Basicはインタープリタだと思い込んでいる読者の誤った常識を解きほぐしておきましょう。

 最初に生まれたBASIC言語はインタープリタ型言語でした。インタープリタの特徴は、コンパイルという手順なしで、即座に実行できる手軽さでした。それが、初歩的な教育用のプログラム言語にはマッチしていたようです。しかし、ビジネス用の本格開発言語としてBASICが使われるようになると、すぐにコンパイラが登場します。実行速度に優れるコンパイラは、よりスピーディに業務システムを動かすためには、大変重宝なものとしてニーズがあったのです。

 Visual Basicの世界では、ずっとインタープリタが主流で、コンパイラはサポートされていませんでした。インタープリタといっても中間コードに変換されてから実行するので、往年のBASICインタープリタよりは高速でしたが、それでもコンパイラに比べれば見劣りするのは否めません。

 しかし、Visual Basic 5.0からネイティブ・コードへのコンパイル機能がサポートされるようになり、実行速度はグッと向上しました。EXEファイル作成のメニューから実行ファイルを生成させることにより、それがコンパイルの手順に相当し、ネイティブ・コードが生成されるようになったのです。一方で、統合開発環境内ではインタープリタで実行する構造を保っており、コンパイルの手順抜きで即座に実行し、デバッグすることができる手軽さも残りました。

 ではインタープリタとコンパイラの性能差を軽く見てみましょう。テストのために以下のようなプログラムをVisual Basic 6.0(以下、VB 6)で作成してみました。10万までの素数を1万回計算します。

Option Explicit

Private Sub Form_Load()
  Debug.Print Time

  Const size As Long = 100000

  Dim flags() As Boolean

  Dim i As Long, j As Long, repeatCount As Long
  For repeatCount = 0 To 9999
    ReDim flags(size - 1) As Boolean
    For i = 2 To size - 1
      If flags(i) = 0 Then
        For j = i * 2 To size - 1 Step i
          flags(j) = True
        Next
      End If
    Next
  Next

  Dim k As Long, count As Long
  For k = 2 To size - 1
    If Not flags(k) Then count = count + 1
  Next

  Debug.Print "found " & count

  Debug.Print Time
  Unload Me
End Sub
10万までの素数を1万回計算するVB 6プログラム

 インタープリタ実行の場合は、Debug.Printで出力した時刻の差を取り、ネイティブ・コード実行の場合は現在時刻を表示してから実行ファイルを実行し、再び現在時刻を表示するようなバッチ・ファイルを作成して実行時間を調べました。その結果、以下のような速度差がありました(処理時間は筆者のパソコン(Pentium 4/1.5GHz)による。以下同じ)。

実行の種類 実行時間(秒)
VB 6インタープリタ
461
VB 6ネイティブ・コード
35

 まさにけた違いのスピードアップといえます。

 このようなインタープリタとコンパイラの長所を兼ね備えたバージョン5.0以降のVisual Basicは、1つの理想的な開発環境になっていたといえます。

 それにもかかわらず、VB.NETにはネイティブ・コード・コンパイラが含まれていないと聞けば、不安になるのも当然のことでしょう。

Visual Basic .NETは決して遅くない

 では、Visual Basic .NETで同じプログラムを実行すると、どのような結果になるでしょうか。論より証拠、まずは実際に実行してみましょう。言語仕様の非互換性に対応するために一部書き換えた以下のようなコードを作成してReleaseビルドしました。

Public Class Form1
  Inherits System.Windows.Forms.Form

……Windows フォームデザイナで生成されたコード……

  Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)

Handles MyBase.Load
    Trace.WriteLine(DateTime.Now)

    Const size As Long = 100000

    Dim flags(size - 1) As Boolean

    Dim i As Integer, j As Integer, repeatCount As Integer
    For repeatCount = 0 To 9999
      ReDim flags(size - 1)
      For i = 2 To size - 1
        If flags(i) = 0 Then
          For j = i * 2 To size - 1 Step i
            flags(j) = True
          Next
        End If
      Next
    Next

    Dim k As Integer, count As Integer
    For k = 2 To size - 1
      If Not flags(k) Then count = count + 1
    Next

    Trace.WriteLine("found " & count)

    Trace.WriteLine(DateTime.Now)
    Close()
  End Sub
End Class
10万までの素数を1万回計算するVB.NETプログラム

 筆者のパソコンで実行した結果の処理時間を表に加えてみます。

実行の種類 実行時間(秒)
VB 6インタープリタ
461
VB 6ネイティブ・コード
35
Visual Basic .NET
32

 なんと、VB.NETにはネイティブ・コード・コンパイラの機能がないにもかかわらず、VB 6のネイティブ・コードよりも速く処理が終わっています。

 CPUが備えている生の命令で記述されたネイティブ・コードよりも高速に実行可能な手段などあるはずがありません。いったい、これはどのようなトリックの結果なのでしょうか。

 まず、ここで1つの誤解を解かねばなりません。VB.NETがネイティブ・コード・コンパイラを持たないということと、インタープリタで実行されることはイコールではありません。

 VB.NETはMSIL(Microsoft Intermediate Language)と呼ばれる中間コードを出力することから、インタープリタで実行するかのように受け取られる例が見られますが、そうではないのです。このMSILのコードは、JIT(Just-In-Time)コンパイラと呼ばれる機能によって、ネイティブ・コードにコンパイルされます。これは、VB.NETではなく実行環境となる.NET Framework側で行われます。

 つまり、VB 6ではEXEファイル作成により高速なネイティブ・コードに直接変換されたのに対して、VB.NETではMSILという中間形態を経由してネイティブ・コードに変換されるようになっただけで、最終的にネイティブ・コードで実行されることに変わりはないのです。そして、おそらくはコンパイラの性能向上のおかげで、実行速度も上がったのだろうと推測します。

 このように、VB.NETがネイティブ・コード・コンパイラを持たないからといって、速度低下を恐れる必要はまったくないのです。

なぜ直接ネイティブ・コードを生成しないのか

 残された問題は、VB.NETで作成したプログラムの起動時に発生する待ち時間です。この待ち時間は、JITコンパイラがMSILからネイティブ・コードを生成したり、悪意ある不正な内容を含んでいないかチェックしたりするために発生します(実際には、これほど単純なものではありませんが、詳細は専門書籍に任せるとして、「あえて」このようなものだということで話を進めましょう)。

 ここで気になることは、VB 6のように直接ネイティブ・コードを出力していれば、この待ち時間は発生しないということです。どうして、待ち時間が発生すると知りつつMSILを経由するという構造を採用したのでしょうか。

 その答えは2つあると思います。1つは、MSILを経由することに大きなメリットがあるから。もう1つは、待ち時間は解消可能だから、といえます。

 MSILを経由するメリットは何でしょうか。不正なアクセスを試みるプログラムを排除しやすいといったセキュリティ面の理由など、いろいろな理由があります。しかし、ここでは性能面に着目してみましょう。

 ネイティブ・コードはCPUが理解する生のプログラムです。同じシリーズのCPUがモデルチェンジする場合、同じネイティブ・コードを理解するように配慮することが普通です。それにより、従来のCPUのためのプログラムも実行できる互換性が生じます。しかし、同じネイティブ・コードが実行できるとしても、実行時間が同じとは限りません。同じ機能を実現する最速の命令の組み合わせ方がCPUによって違う場合があるのです。これに対応するために、高度なコンパイラには、CPUの種類を選択するオプションが提供され、CPUの種類ごとに最適のネイティブ・コードを生成できる場合があります。

 さて、ネイティブ・コード・コンパイラは、一気にネイティブ・コードを生成してしまいます。そのため、新しいCPUが登場した場合、そのCPUで最高性能を発揮させるためには、新しいCPUに対応したネイティブ・コード・コンパイラで再度コンパイルする必要が発生します。しかし、再コンパイルして、その実行ファイルを配布することは、大きな手間となります。

 それに対してMSILを経由する方法では、そのような大きな手間は必要ありません。CPUの違いはJITコンパイルの段階で自動的に処理され、再びVB.NETを開いて再コンパイルする必要はないのです。

起動時の遅れは事前にコンパイルして解決可能

 さて、実行を開始してしまえばVB.NETのMSIL経由方式の方が高速になる可能性がある、ということまでは説明しましたが、実行開始までの待ち時間が大きければ、そのメリットも帳消しになりかねません。しかし、この点は解決可能です。

 通常、JITコンパイラは、プログラムが実行されたときに仕事を始めます。しかし、何回も起動されると分かっているプログラムの変換を、実行されるまで待つ必要はないし、起動されるごとに行う必要もありません。事前にJITコンパイラの仕事を完了させておけば、待ち時間は取られません。

 これを行うには、ngen.exe(ネイティブ・イメージ・ジェネレータ)というコマンドライン・ツールを使用しますが、通常はこのツールを直接使うのではなく、インストールなどの手順の中に組み込んで自動実行することになるでしょう。

 では、実際にngen.exeコマンドを使って起動時間が短縮できていることを確認してみましょう。そのために、以下のような無意味に行数が多いだけのサンプル・プログラムを作成してみました。

Public Class Form1
  Inherits System.Windows.Forms.Form

……Windows フォーム デザイナーで生成されたコード……

  Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    Dim a As Single
    a = Math.Sin(a)
    a = Math.Sin(a)
    a = Math.Sin(a)
    a = Math.Sin(a)
    ……中略、上の行を99999行繰り返す……
    Me.Text = a.ToString()
    Close()
  End Sub
End Class
無意味に行数が多いだけのVB.NETプログラム

 上記のプログラムを10回起動するバッチ・ファイルを作成し、その最初と最後に現在時刻を出力させて、経過時間を調べました。

実行の種類 実行時間(秒)
ngen.exeによる処理なし
15
ngen.exeによる処理あり
4

 このとおり、歴然とした大きな差が生じています。小さなプログラムではメリットが薄いかもしれませんが、大きく複雑なプログラムになると、起動時の快適さが大幅に改善できる可能性もあります。

 このように、事前にJITコンパイルすることは大きなメリットがありますが、意外にその存在を知られていないようです。.NETアプリケーションは起動が遅いから嫌だ、というような意見をしばしば耳にしますが、たいていの場合、事前にJITコンパイルするというような本来なら常識的な対策が採られていないだけであるように思います。

 以上のような状況ですから、VB 6と比較して、VB.NETには性能面での不安はありません。ぜひ、VB.NETを使いこなしていきましょう!

 頑張れVBプログラマ、君たちが使うVisual Basic .NETは取り組む価値のある可能性に満ちたプログラム言語だ!End of Article

インデックス・ページヘ  「熱血VBプログラマ応援団」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH