最近VBAHaskellにAPIやユーティリティ関数を追加したり、少し性能改善したりしました。VBAHaskellの性能という点に関しては swapVariant 関数がひとつのポイントになっています。そのことはVBAHaskellの紹介 その14(変数のムーブ)でも少し書いたのですが、他の関数との関係も含めてもう一度書いてみようと思います。
1. swapVariant
の実装
swapVariant
はC++API側で実装されていて、VARIANT構造体をstd::swap
を使って値をスワップしています。VARIANTはただの構造体1なのでビット単位の単純な値のスワップが実行されます。VBA のVariant変数が動的配列の場合、配列本体は別の場所に確保されていて変数はそのポインタを持つ2だけなので、スワップ動作はポインタの交換として高速に行われ、所有権ごと配列データが入れ替わります。
2. 戻り値に対するswapVariant
swapVariant
の一番分かりやすい使い方は関数からの戻り値です。
Function myFunc(ByRef x As Variant) As Variant
Dim tmp As Variant
Redim tmp(...
tmp = ...
'myFunc = tmp これは効率が悪いのでやらない
swapVariant myFunc, tmp ' これでOK
End Function
VBAでは関数名がそのまま戻り値となる仕様なので、上記でmyFunc = tmp
としたようにローカル変数をコピーすることがよくあります3。tmpが配列の場合はこの動作は高コストですし、tmp
にぶら下がっている配列は関数が終了するとき捨てられるので無駄になってしまいます。
それに対して swapVariant myFunc, tmp
と書くと、myFunc
とtmp
が入れ替わってtmp
は単なるEmpty
値になります。破棄されても惜しくありません。
myFunc = tmp
変数 | 代入前 | → | 代入後 | → | 関数終了時 |
---|---|---|---|---|---|
tmp | 配列 | → | 配列 | → | 破棄 |
myFunc | Empty | → | 配列 | → | 戻り値になる |
swapVariant myFunc, tmp
変数 | swap前 | → | swap後 | → | 関数終了時 |
---|---|---|---|---|---|
tmp | 配列 | → | Empty | → | 破棄 |
myFunc | Empty | → | 配列 | → | 戻り値になる |
なお、y = myFunc(x)
と書いたときに y
に対するコピーとmyFunc
の破棄は発生しないようです。速度とメモリ使用状況については下に追記しました。
3. ジャグ配列を作る時
複数の変数をひとつのジャグ配列に一時的に格納するmove_many
という関数が Haskell_2_stdFun.bas にあります。下のコードは配列a
,b
,c
をfoldl1
とcatR
を使って縦に結合している例で、m
というジャグ配列に3つの配列を一時的に格納しています。(この間a
,b
,c
はEmpty
になっています。)
a = iota(1,10)
b = iota(101, 110)
c = iota(201, 210)
' m は Array(a, b, c) と同じ
m = move_many(a, b, c) ' <<< ここで a, b, c は Empty となっている
' catR(catR(a, b), c) と同じことを foldl1 を使って行う
printM foldl1(p_catR, m)
1 2 3 4 5 6 7 8 9 10
101 102 103 104 105 106 107 108 109 110
201 202 203 204 205 206 207 208 209 210
' a, b, c をもとに戻す
move_back m, a, b, c
通常は m = Array(a, b, c)
と書くところですが、大きな配列の時で速度が問題になる場合はmove_many
で集めて処理したあとmove_back
で元に戻すという方法で効率を高めることができます4。これらは以下のようにswapVariant
で実装されています。
' 複数の変数をmoveしてひとつのジャグ配列にする
Function move_many(ParamArray m() As Variant) As Variant
If LBound(m) <= UBound(m) Then
Dim ret As Variant
ReDim ret(0 To UBound(m) - LBound(m))
Dim i As Long, k As Long: k = 0
For i = LBound(m) To UBound(m) Step 1
swapVariant m(i), ret(k)
k = k + 1
Next i
End If
swapVariant move_many, ret
End Function
' ジャグ配列から複数(可変長)の変数にmove back
Sub move_back(ByRef m As Variant, ParamArray ret() As Variant)
Dim i As Long, k As Long: k = LBound(ret)
For i = LBound(m) To UBound(m) Step 1
swapVariant m(i), ret(k)
k = k + 1
Next i
m = Empty
End Sub
速度とメモリ使用状況について
2.で書いたコピーと破棄の有無は実行時の所要時間とメモリ使用量増減を根拠としています。
以下の比較テストコードを走らせ、メモリ使用量の経過を見るとグラフのような状況でした。(ステップ実行による)
' 単純にコピー
Function test1(ByRef a As Long) As Variant
Dim ret As Variant
ret = iota(1, a)
test1 = ret ' 関数名 = 値
End Function
' swapVariant
Function test2(ByRef a As Long) As Variant
Dim ret As Variant
ret = iota(1, a)
swapVariant test2, ret ' スワップ
End Function
' 比較テスト
Sub test_1_2()
Dim y As Variant
y = Empty
y = test1(30000000) ' 戻り値代入式
y = Empty
y = test2(30000000) ' 戻り値代入式
y = Empty
End Sub
どちらも30,000,000要素の配列を作る段階でメモリ使用量が上がりますが、test1
では通常やる「関数名 = 値」の個所で再度上昇しています。swapVariant
を使うtest2
では最初の配列確保以外でメモリ使用量は上昇しません。
メモリ使用量は元に戻るので問題ないですが、当然その都度時間はかかるので、繰り返し呼ばれる関数であれば無視できない問題になります。実測したところtest1
とtest2
の速度比は約 1 : 1.5 でした。
なお、戻り値代入式ではメモリ量は変化しません。y
と戻り値は同一アドレスではなかったので、どういう戻り値最適化なのかよくわかりません。
リンク
VBAHaskellの紹介 その14(変数のムーブ)
VBAHaskellの紹介 その1 (最初はmapF)
mYmd/VBA
関数リファレンス