Excel-DNA のセルに対する書き込み性能を計ってみた

ExcelDnaUtil.Application を F# から使ったらえらい遅かったので、色々試してみました。
結論を書くと、キャッシュしてなかったから遅かっただけでした。

ベンチマーク概要

100 * 100 のすべてのセルに 1 を設定していく時間を計測しました。
セルに対する書き込み性能を見るためのベンチマークなので、実際の使用状況では全く別の結果となることに注意してください。

ベンチマークの取り方

  1. https://github.com/bleis-tift/DNA からコードを取得する
  2. dna.sln をビルドする
  3. Excel を起動する
  4. dna_fsdll/bin/Release/dna_fsdll.xll を起動した Excel に DnD
    • 「このアドインをこのセッションに限り有効にする」を選択
  5. dna_csdll/bin/Release/dna_csdll.xll を起動した Excel に DnD
    • 「このアドインをこのセッションに限り有効にする」を選択
  6. リボンからアドインタブを選択
  7. 好きなベンチマークを走らせる
  8. ステータスバーにかかった時間が秒で表示される

ベンチマーク環境

CPU
Core i5 M560 2.67GHz
メモリ
8GB
OS
Winows 7 Ultimate x64
Excel
Excel 2010 64bit

ベンチマーク結果

コード 結果 (秒)
F#(COM/キャッシュなし) 362
F#(COM/キャッシュあり) 2.65
F#(DNA) 0.56
C#(COM) 1.84
C#(DNA) 0.55
VBA 0.48

キャッシュなしの F#(COM) は VBA の 700 倍以上遅いという結果に。
まぁぶっ飛んだ値はとりあえず置いときましょう (なんでこんなに遅くなったのかは後述)。
傾向としては、セルに対する書き込み性能では

  • 言語が変わってもあまり差はないが、F# よりも C# の方が若干高速
  • COM は Excel-DNA で用意されたオブジェクトを使うよりも大体 3 倍以上遅い
  • VBAが最速

ということが言えるでしょう。

結論

セルへの書き込み速度が重要になるようなものの場合、COM ではなく Excel-DNA で用意されたオブジェクトを使うべきです。
最速は VBA ですが、単純にセルへの書き込みのみのマクロは少なく、何らかの計算などが行われるでしょう。
そうなると C# や F# で書いた方が高速になる (要出典) ため、VBA の出番は限られます。

使用したコード

C#

Range メソッドとかタプルの扱いとかがちょっと面倒でした。
ローカル変数以外で var が使えないのも残念です。


COM を使用したコードでは、dynamic が大活躍です。
Excel-DNA のオブジェクトを使用したコードでは、ExcelDnaUtil.Application をいったん dynamic で受ける必要があるのはちょっと面倒です。

static IEnumerable<Tuple<int, int>> range =
    from r in Enumerable.Range(1, 99)
    from c in Enumerable.Range(1, 99)
    select Tuple.Create(r, c);

[ExcelCommand(MenuName = "Bench(C#)", MenuText = "COM Object")]
public static void ComCs()
{
    var sw = Stopwatch.StartNew();

    dynamic excel = ExcelDnaUtil.Application;
    dynamic sheet = excel.ActiveSheet;
    foreach (var r_c in range)
    {
        dynamic cell = sheet.Cells.Item(r_c.Item1, r_c.Item2);
        cell.Value2 = 1;
    }

    sw.Stop();
    excel.StatusBar = sw.Elapsed.TotalSeconds;
}

[ExcelCommand(MenuName = "Bench(C#)", MenuText = "DNA Object")]
public static void DnaCS()
{
    var sw = Stopwatch.StartNew();

    foreach (var r_c in range)
    {
        var row = r_c.Item1 - 1;
        var col = r_c.Item2 - 1;
        var cell = new ExcelReference(row, col);
        cell.SetValue(1);
    }

    sw.Stop();
    dynamic excel = ExcelDnaUtil.Application;
    excel.StatusBar = sw.Elapsed.TotalSeconds;
}
F#

C# よりもやりたいことに集中できるのはいいのですが、dynamic に相当する仕組みを自分で作らなければならないのは面倒です。
幸い、gist に上げてくれていた人がいたため、ほぼそのまま使わせてもらいました。
が、これが dynamic に比べて遅い遅い・・・
C# の dynamic は裏でキャッシュされるので、上の仕組みにキャッシュ機能を組み込んだら 100 倍以上速くなりました。

let range = [ for r in [1..100] do
              for c in [1..100] -> (r, c) ]

[<ExcelCommand(MenuName="Bench(F#)", MenuText="COM Object ??? Cache")>]
let nonCachedCom () =
  let sw = Stopwatch.StartNew()

  let excel = ExcelDnaUtil.Application
  let sheet = excel?ActiveSheet
  range
  |> List.iter (fun (row, col) ->
       let cell = sheet?Cells?Item(row, col)
       cell?Value2 <- 1
     ) 

  sw.Stop()
  excel?StatusBar <- sw.Elapsed.TotalSeconds

[<ExcelCommand(MenuName="Bench(F#)", MenuText="DNA Object")>]
let dna () =
  let sw = Stopwatch.StartNew()

  range
  |> List.iter (fun (row, col) ->
       let row, col = row - 1, col - 1
       let cell = ExcelReference(row, col)
       cell.SetValue(1) |> ignore
     ) 

  sw.Stop()
  ExcelDnaUtil.Application?StatusBar <- sw.Elapsed.TotalSeconds
VBA

単純なコードだと VBA って一番シンプルに書けるんですね。
Dim t = Timer とかって書けたら割と満足かもしれない・・・
ま、現実世界でかかわる VBA プログラムはホビロンなのでアレですががが。

Sub vba()
  Dim t
  t = Timer

  For r = 1 To 100
  For c = 1 To 100
    ActiveSheet.Cells(r, c).Value2 = 1
  Next c
  Next r
  
  Application.StatusBar = Timer - t
End Sub