LoginSignup
1
1

More than 5 years have passed since last update.

swapVariantについて(VBAHaskell)

Posted at

最近VBAHaskellにAPIやユーティリティ関数を追加したり、少し性能改善したりしました。VBAHaskellの性能という点に関しては swapVariant 関数がひとつのポイントになっています。そのことはVBAHaskellの紹介 その14(変数のムーブ)でも少し書いたのですが、他の関数との関係も含めてもう一度書いてみようと思います。

1. swapVariantの実装

swapVariantC++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 と書くと、myFunctmpが入れ替わって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,cfoldl1catRを使って縦に結合している例で、mというジャグ配列に3つの配列を一時的に格納しています。(この間a,b,cEmptyになっています。)

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

image

どちらも30,000,000要素の配列を作る段階でメモリ使用量が上がりますが、test1では通常やる「関数名 = 値」の個所で再度上昇しています。swapVariantを使うtest2では最初の配列確保以外でメモリ使用量は上昇しません。
メモリ使用量は元に戻るので問題ないですが、当然その都度時間はかかるので、繰り返し呼ばれる関数であれば無視できない問題になります。実測したところtest1test2の速度比は約 1 : 1.5 でした。
なお、戻り値代入式ではメモリ量は変化しません。yと戻り値は同一アドレスではなかったので、どういう戻り値最適化なのかよくわかりません。


リンク
VBAHaskellの紹介 その14(変数のムーブ)
VBAHaskellの紹介 その1 (最初はmapF)
mYmd/VBA
関数リファレンス



  1. コピーコンストラクタ、ムーブコンストラクタ、コピー代入演算子、ムーブ代入演算子等は定義されていない 

  2. parrayメンバもしくはpparrayメンバ 

  3. ローカル変数を使わず関数名に直接値を入れればいいと思えますが、ReDim function_name( )はできませんし、配列の要素を選択するため function_name(0) と書くと再帰呼び出しと解釈されてしまいます。 

  4. CatR自体が効率のいい処理とは言えないのでmove_manyの効果は限定的かもしれませんが。 

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1