読者です 読者をやめる 読者になる 読者になる

VBAHaskellの紹介 その14(変数のムーブ)

VBA

Haskellと全く関係ない話題だが、VBAHaskellでは効率上の理由からVariant変数のムーブ・セマンティクスを実装して利用している。APIにある moveVariant 関数だ。*1
効率とは、関数で配列を返す時に発生するローカル変数のコピーなどのことである。

' 0 から n までの自然数配列を返す関数(簡略化版)
Function iota_(ByVal n As Long) As Variant
    Dim ret As Variant, i As Long

    ReDim ret(0 To n - 1)       ' ローカル変数をReDimする
    For i = 0 To n - 1: ret(i) = i: Next i
    iota_ = ret                 ' 普通に返すか
    iota_ = moveVariant(ret)    ' ムーブして返すか
End Function

VBAには return 文がなく、関数名 = 値 とすることによって値を返す。しかし 関数名を直接 ReDim することはできない。上記の例だと、ReDim iota_(0 to n - 1) とはできないので、配列を返すためにはローカル変数をReDimし、関数名 = ローカル変数 とする。
ここで iota_ = ret とするとVariant変数のコピーが発生するため、大きな配列の場合や頻繁に呼び出される場合にはコストが無視できない。
どうせローカル変数は捨てられるのだから、ムーブしてしまえばよい。

//sourceのVARIANT変数をtargetのVARIANTへmoveする
VARIANT __stdcall  moveVariant(VARIANT* source)
{
    VARIANT target;
    ::VariantInit(&target);
    std::swap(target, *source);
    return target;
}

実装はこれだけで、std::swap の VARIANT への特殊化はもちろんしていない。
(その後、moveVariantswapVariant関数を使って実装する形に変更した。変数のスワップの方が汎用性が高いのだ。)

これは実測してかなり効果があることが分かった。元の変数はきれいさっぱり消えてしまうので注意が必要だが、「配列を受け取って何かして返す」というとき、Sub プロシージャでなく Function プロシージャにできるというメリットがある。

' 単に配列の最初の要素をインクリメントするだけ
Function moveIt(ByRef x As Variant) As Variant
    x(0) = x(0) + 1
    moveIt = variantMove(x)
End Function

'aは大きな配列
a = moveIt(a)
b = moveIt(a)    ' こうすると a は消えるが一瞬で処理できる

こうすれば、引数を渡すのも返すのもコストを気にせずに済む。API内でも functionExpr::eval で使用している。

VBAHaskellの紹介 その13 (プレースホルダの追加: _1 と _2 )
VBAHaskellの紹介 その1 (最初はmapF)

*1:後述のように、API側はswapVariantという関数に変更し、moveVariantはVBA側に移動した