t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA 配列とコレクションの違いをメモリ上のデータ構造から理解する

VBAでは複数データを格納できるデータ型として、配列とコレクションがある。
それぞれ一長一短あり、どちらが優れているというものではないのだが、どちらかといえばデータの追加・削除が簡単に行えるコレクションのほうが使い勝手は良いかもしれない。

さて、今回は配列とコレクションのデータ構造に焦点を当ててそれぞれの違いを説明する。

配列のデータ構造

例えばInteger型の配列を次のように作成する。

Dim Arr(3) As Integer
Arr(0) = 10
Arr(1) = 20
Arr(2) = 30
Arr(3) = 40

すると、メモリ上には単に直列にデータが並ぶ。VBAのIntegerは2バイトなので、ちょうど2バイトずつ隙間なく配置される。
f:id:t-hom:20160831000135p:plain

もし次のようにLong型で宣言したら、

Dim Arr(3) As Long
Arr(0) = 100
Arr(1) = 200
Arr(2) = 300
Arr(3) = 400

Long型は4バイトなので、やはり4バイトずつ隙間なくメモリに配置される。
f:id:t-hom:20160831000436p:plain

このように隙間なく並べることで、添え字(インデックス)が大きくなっても、データの格納位置を瞬時に割り出すことができる。
どういうことか、先ほどのLong型配列の例で説明する。

例えば次のようにArr(3)の値を表示する命令が実行されたとする。

MsgBox Arr(3)

VBAはまず、Arr(3)がメモリ上のどの場所に格納されているかを探しだす。

配列Arrの先頭アドレスは10000000なので、これに(添え字×型のデータサイズ)を加えてやれば、アドレスが求まる。
添え字は3で、Long型のサイズは4なので、3 × 4 = 12。
先頭アドレスの10000000に12を加えた10000012が目的のデータのアドレスであるとわかる。

そして、アドレスの10000012番地にある値「400」をメッセージボックスで表示する。
以上が配列の参照時の動作である。

配列の添え字(インデックス)を先頭アドレスからのオフセットとして利用するためには、隙間なくデータが並んでいなければならない。

以下のように、別のデータがメモリに入っていたら、もうそれ以上はデータを増やせない。
f:id:t-hom:20160831001754p:plain

なぜなら、仮に次の空き番地にデータを入れたとしても、添え字からデータ位置を割り出すことができなくなるので、データを取り出せなくなってしまうからだ。

配列は宣言時にサイズを決め、そのあと原則サイズ変更ができないが、これはつまり、最初にメモリの連続した領域を確保してしまう必要がある為だ。

ちなみに動的配列はReDimでサイズ変更ができるが、あれも厳密にいえば別の領域に新たな連続したメモリ領域を確保して配列を再作成している。
だから命令の名前もExpand(拡張)じゃなくて、Re(再)Dimとなっている。Preserveを付けないと中身が消えてしまうという説明がなされるが、正確に言えばPreserveを付けると旧メモリ領域からデータをコピーしてくれるということである。

自分で確かめたい場合は以下を試してみると良い。

Sub ReDimでアドレスが変わるサンプル()
    Dim Arr() As Integer
    ReDim Arr(2)
    Arr(0) = 10
    Arr(1) = 20
    Arr(2) = 30
    
    Debug.Print "配列のアドレス"
    For i = 0 To 2
        Debug.Print "Arr(" & i & ") : "; VarPtr(Arr(i))
    Next
    
    ReDim Preserve Arr(3)
    Arr(3) = 40
    
    Debug.Print
    Debug.Print "ReDim後のアドレス"
    For i = 0 To 3
        Debug.Print "Arr(" & i & ") : "; VarPtr(Arr(i))
    Next
End Sub

コレクションのデータ構造

Collectionという名称はMicrosoftが付けたものであるが、データ構造の一般名称としては「連結リスト」または単に「リスト」と呼ばれている。

リスト構造では、以下のようにデータと次のデータ位置を示すアドレスがセットで格納されている。
f:id:t-hom:20160831004315p:plain

(注)メモリ図はあくまでイメージです。紙面の都合でアドレス欄(水色)を1バイトとしましたが、アドレス番号を格納するには少なくとも4バイト必要です。

リストの場合、配列と違って隙間なくデータを並べておく必要がなく、空いているアドレスにデータを追加することができる。

情報処理試験などでは、次のような図で表されることも多い。
f:id:t-hom:20160831004105p:plain

新たにデータを追加したい場合は任意の場所にデータを追加し、前の要素のアドレス欄を書き換えれば良い。
f:id:t-hom:20160831004831p:plain

データの挿入も、前の要素のアドレス欄を書き換えてから、挿入する要素のアドレス欄に次の要素のアドレスを格納すれば良い。
f:id:t-hom:20160831005117p:plain

削除もアドレス欄の書き換えだけで済む。
f:id:t-hom:20160831005556p:plain

VBAのCollection型はリスト構造なので、データの追加や削除が簡単に行うことができる。
データを参照するにはリストの開始位置から順にリストを辿っていけば良い。

例えば次のプログラムは、3番目にAddされた「30」を表示する。

Sub fuga()
    Dim C As New Collection
    C.Add 10
    C.Add 20
    C.Add 30
    
    MsgBox C(3)
End Sub

この時、内部では次のようにリストを先頭から順に辿る処理がなされている。

1番目のアドレス欄を参照し、2番目のデータ位置を確認する。
2番目のアドレス欄を参照し、3番目のデータ位置を確認する。
3番目のデータにたどり着いたので、それを表示する。

配列のように添え字からの計算で格納位置を求めることができないため、愚直にリストを辿るしかないのである。
だから理屈上は、後ろのほうに追加されたデータのほうが参照するのに時間がかかることになる。

これを実証するには、次のマクロを利用すると良い。

Sub コレクションの速度計測()
    Dim C As New Collection
    Debug.Print "準備しています。お待ちください。"
    For i = 1 To 10 ^ 7
        C.Add i
    Next
    Debug.Print "計測を開始します。"
    
    n = 100
    For i = 1 To 7
        T = Timer
        For j = 1 To n
            Void = C(10 ^ i)
        Next
        Debug.Print 10 ^ i; "番目のデータを"; n; "回参照するのに"; Round(Timer - T, 5); "秒かかりました。"
    Next
    Debug.Print "計測を終了しました。"
End Sub

私のPC環境だと、次のような結果になった。
f:id:t-hom:20160831012802p:plain

確かに、後ろのほうに追加されたデータのほうが参照するのに時間がかかっている。

次に配列でやってみよう。

Sub 配列の速度計測()
    Dim Arr(10 ^ 7)
    Debug.Print "準備しています。お待ちください。"
    For i = 0 To 10 ^ 7
        Arr(i) = i
    Next
    Debug.Print "計測を開始します。"
    n = 10000
    For i = 1 To 7
        T = Timer
        For j = 1 To n
            Void = Arr(10 ^ i)
        Next
        Debug.Print 10 ^ i; "番目のデータを"; n; "回参照するのに"; Round(Timer - T, 5); "秒かかりました。"
    Next
    Debug.Print "計測を終了しました。"
End Sub

参照回数が100回ずつだとすべて0秒で終わってしまったので、nの値は10000とした。
これだけ見ても配列がいかに高速かがわかる。

そして、私のPC環境での実行結果は、次のようになった。
f:id:t-hom:20160831013124p:plain

すでに説明したとおり、添え字から計算でデータの格納アドレスを求めているため、参照位置が変わっても速度に差は出ない。
コレクションで後方のデータほど時間がかかるのとは対照的である。

まとめ

今回は普段使っている配列やコレクションのデータ構造について解説した。
サンプルでは検証のためにわざと有意差が出るようにループ回数やサイズを調整したが、これはあくまでデータ構造の説明に説得力を持たせるためのサンプルであって、配列のほうが高速だから優れていると言いたいわけではない。
実際に配列のメリットとして高速であるという説明がなされることがあるが、実務で扱う数万件程度のデータなら大差ないので速度はそれほどアピールポイントにはならない。

配列のメリットとしては、ArrayやSplitなどで動的に生成できることや、Join関数で結合できること、2次元配列のセルとの相互転記ができること、宣言時に型をカチっと決められるためオブジェクト型の配列にしたときにドットでプロパティとメソッドの入力候補が表示されること等が挙げられる。

コレクションのメリットは、データの追加・削除・挿入が容易であることと、キー文字列を設定でき、インデックスの変わりにキーを使ってデータ参照できる点が挙げられる。

それぞれ一長一短あるので、配列派、コレクション派ということではなく、どちらも自在に使いこなせるようになりたい。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。