swapVariantについて(VBAHaskell)

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

##1. **`swapVariant`の実装**
`swapVariant`は[C++API側](https://github.com/mYmd/VBA/blob/master/mapM.cpp)で実装されていて、VARIANT構造体を`std::swap`を使って値をスワップしています。VARIANTはただの構造体[^1]なのでビット単位の単純な値のスワップが実行されます。VBA のVariant変数が動的配列の場合、配列本体は別の場所に確保されていて変数はそのポインタを持つ[^2]だけなので、スワップ動作はポインタの交換として高速に行われ、所有権ごと配列データが入れ替わります。

##2. **戻り値に対する`swapVariant`**
`swapVariant`の一番分かりやすい使い方は関数からの戻り値です。

```vbnet
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 &nbsp; myFunc, tmp**

|変数      |swap前|→ |swap後 |→  |関数終了時|
|:---------|:----|:--|:-----|:--|:-----|
|**tmp**   |配列  |→ |Empty |→  |破棄|
|**myFunc**|Empty |→ |配列  |→  |戻り値になる|

なお、**`y = myFunc(x)`**と書いたときに `y`に対するコピーと`myFunc`の破棄は発生しないようです。速度とメモリ使用状況については下に追記しました。

##3. ジャグ配列を作る時
複数の変数をひとつのジャグ配列に**一時的に**格納する`move_many`という関数が [Haskell_2_stdFun.bas](https://github.com/mYmd/VBA/blob/master/Haskell_2_stdFun.bas) にあります。下のコードは配列`a`,`b`,`c`を`foldl1`と`catR`を使って縦に結合している例で、`m`というジャグ配列に3つの配列を一時的に格納しています。（この間`a`,`b`,`c`は`Empty`になっています。）

```vbnet
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`で実装されています。

```vbnet
' 複数の変数を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
```

<br>
* * *
## 速度とメモリ使用状況について
2.で書いたコピーと破棄の有無は実行時の所要時間とメモリ使用量増減を根拠としています。
以下の比較テストコードを走らせ、メモリ使用量の経過を見るとグラフのような状況でした。（ステップ実行による）

```vbnet
' 単純にコピー
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](https://qiita-image-store.s3.amazonaws.com/0/77153/1821bc6e-6bef-3d86-52d4-42a695382d7d.png)

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

* * *
**リンク**
[VBAHaskellの紹介 その14（変数のムーブ）](http://qiita.com/mmYYmmdd/items/5f5ef2ff2d48489dc0cb)
[VBAHaskellの紹介　その1　（最初はmapF）](http://qiita.com/mmYYmmdd/items/c731edf943acc0a0ebe9)
[mYmd/VBA](https://github.com/mYmd/VBA)
[関数リファレンス](http://home.b07.itscom.net/m-yamada/vh_Manual/VBAHaskell_reference.htm)
* * *
[^1]: コピーコンストラクタ、ムーブコンストラクタ、コピー代入演算子、ムーブ代入演算子等は定義されていない
[^2]: parrayメンバもしくはpparrayメンバ
[^3]: ローカル変数を使わず関数名に直接値を入れればいいと思えますが、`ReDim function_name( )`はできませんし、配列の要素を選択するため `function_name(0)` と書くと再帰呼び出しと解釈されてしまいます。
[^4]: CatR自体が効率のいい処理とは言えないので`move_many`の効果は限定的かもしれませんが。