[[20250204112342]] 『VBA 動的配列のサイズ変更』(TKG) ページの最後に飛ぶ

[ 初めての方へ | 一覧(最新更新順) | 全文検索 | 過去ログ ]

 

『VBA 動的配列のサイズ変更』(TKG)

 動的配列を定義し、Forループを回しながらIf文で条件にヒットしたら配列のサイズを変更するコードを組みました。下記のようなイメージです。

 -----
 Dim Data() As Variant
 Dim i As Integer, j As Integer : j = 0 
 For i = 1 to 1000 
     If 条件式 Then
         Redim Presereve Data(0 to j, 0 to 4)
         処理
         j = j +1 
     End If
 Next i
 ----

 何度試してもエラーになるので頭を悩ませていたところ、「Redim Preserveでは最大次元のインデックスしか変更できない」というオチでした。
 Redim Presereve Data(0 to 4, 0 to j)と変更し
 最終的にTransposeで入れなおすことで解決しました。
 解決自体はしているので「そういう仕様」と納得はしていますが
 なぜそういった仕様になっているのかや、Transpose以外の対処法をご存じの方いらっしゃいますか?専門的な話になってもかまいません。

< 使用 Excel:Microsoft365、使用 OS:Windows11 >


 VBAの配列の実体はCOMのSAFEARRAYです。
 SAFEARRAYは最大次元のサイズのみ動的に変更することができます。
 よって、仕様としか言いようがないです。
 Transpose自体も扱えるサイズに制約があるので巨大サイズの配列を扱う場合は注意が必要です。
 そういうケースでは愚直にForループで格納していく事になります。
(まる2021) 2025/02/04(火) 12:00:26

 概念的ですが、
 配列をメモリに格納する場合のイメージは Basicではこんな風になっていると思ってください。
 A(1 to 2 ,1 to 4)のとき
 メモリ格納イメージ
 A(1,1)
 A(2,1)
 A(1,2)
 A(2,2)
 A(1,3)
 A(2,3)
 A(1,4)
 A(2,4)

 A(1 to 2 ,1 to 4)をA(1 to 2 ,1 to 5)にするとき
 メモリ格納領域の最後を拡張して、
 A(1,5)
 A(2,5)
 を付加すれば終わりますが、

 A(1 to 2 ,1 to 4)をA(1 to 3 ,1 to 4)にするときは、再配置して格納しなおさないといけません
 なので、できないようになってます

 このようなメモリ格納方法は、言語によって違います。BasicはFortran系とおなじで、Cとは反対
 Column-major と Row-major をキーワードに検索してください。

 言語の設計思想の違いなので、仕様を決めた人がその言語にとってはその方がよいと思ったからそうなっているというしかないです

 余談
 Basicで大規模数値シミュレーションをする人をあまりいませんが、
 Fotranの場合、実行速度を気にプログラムでは多重ループの最内側は一番左の次元でループするようにします。
 ・メモリが連続なのでCPUの内部メモリキャッシュのヒット率があがるかもしれない
 ・メモリが連続なのでベクトル演算プロセッサが使えるかもしれない
(´・ω・`) 2025/02/04(火) 12:16:37

 >Transpose以外の対処法をご存じの方いらっしゃいますか?

 指摘されている通り、Transposeに制限があること、
 インデックスが1から降り直されること、
 Redim Preserveの実行回数によっては時間が馬鹿にならないこと、
 から、必要なサイズの配列を宣言し、格納するようにしています。

 Dim Data() As Variant
 Dim i As Integer, j As Integer: j = -1
 For i = 1 To 1000
     If 条件式 Then j = j + 1
 Next i
 ReDim Data(0 To j, 0 To 4)
 For i = 0 To j
     処理
 Next

 1次元配列なら、ループ前にインデックスを-1にして
 使ったりはします。

 Dim Data() As Variant
 Data = Array()
 Dim i As Integer
 For i = 1 To 1000
     If 条件式 Then
         ReDim Preserve Data(UBound(Data) + 1)
         Data(UBound(Data)) = 値
     End If
 Next i

(tkit) 2025/02/04(火) 13:05:30


 Transposeは配列の要素に Null が含まれているとエラーを起こすので
 外部データベースとの相性は案外良くないな...って印象を持ってます。

 Preserveについては、ループで上限が決まってるなら、
 配列を最大サイズで作っておいて、ループ後にちょん切る書き方も出来ますよ。

    Dim Data() As Variant
    Dim i As Long, j As Long
    ReDim Data(0 To 1000 - 1)
    For i = 1 To 1000
        If i Mod 100 = 0 Then
            Data(j) = i
            j = j + 1
        End If
    Next i
    ReDim Preserve Data(0 To j - 1)
    Debug.Print Join(Data)

(白茶) 2025/02/04(火) 13:27:54


 メモリー管理がご指摘のとおりであることを表示してみました。参考にしてください。
 VarPtr()はメモリアドレスを返す関数です。

 Sub test()
     Dim arr(1 To 2, 1 To 4) As Integer
     Dim i&, j&
     ' 配列に値を代入
     arr(1, 1) = 1: arr(1, 2) = 2: arr(1, 3) = 3: arr(1, 4) = 4
     arr(2, 1) = 5: arr(2, 2) = 6: arr(2, 3) = 7: arr(2, 4) = 8

     ' 配列のメモリアドレスを取得
     For j = 1 To 4
         For i = 1 To 2
             Debug.Print i, j, arr(i, j), VarPtr(arr(i, j))
         Next
     Next
 End Sub

 【出力結果】
  1             1             1             462303552 
  2             1             5             462303554 
  1             2             2             462303556 
  2             2             6             462303558 
  1             3             3             462303560 
  2             3             7             462303562 
  1             4             4             462303564 
  2             4             8             462303566 

 これでどういう順序でメモリーに保持されているかがわかると思います。

【そういうえば昔こんな発言をしていました。

  以下、大分重複していますので、時間の無い方はスキップください】

 |  ■以下、余談です。(お急ぎの方は、そのままスキップしてください)
 | 
 |  話は、配列を行方向に処理するか、列方向に処理するかの順序に関するものです。
 | 
 |  VBAの配列は、列優先順序のメモリーレイアウトを利用していると思います。
 |  つまり、行列(例えば2次元配列)はメモリー上に
 |     1    5    9
 |     2    6   10
 |     3    7   11
 |     4    8   12
 |  といった数値をつけた位置の順番にメモリー上に管理されているものと思われます。
 |  (メモリーはむろん一次元です)
 | 
 |  動的配列のRedim Preserveなどで、「最後の次元に沿ってしか大きさを変更できない」というのは、
 |  このメモリーレイアウトが原因となっているわけです。
 | 
 |  動的配列と固定配列のメモリーレイアウトは同じであると仮定すると、
 |  上の"列優先順序"で配列にアクセスしたほうが、連続した位置のメモリーを扱うことになるので、
 |  速度的にも有利なはずです。
 | 
 |  実験してみました。
 |  縦横1000要素からなる二次元配列に、1000個の要素からなる一次元配列を
 |  (c)縦に配置して全体に繰り返して埋める処理を100回実行
 |  (r)横に配置して全体に繰り返して埋める処理の100回実行
 |  の時間を計測すると、
 |  (c)は1.97秒、(r)は2.20秒と、(c)は約9割の時間で済みます。(10回の平均値)
 |  (もちろん出来上がりの配列は異なりますが、処理時間にのみ注目しています)
 | 
 |  (なお、私の別の処理系(これも列優先順序のルールに従います)で同じ実験をすると、
 |    (c)0.0741秒  (r)0.1846秒と、(c)は(r)の約4割の時間で済みました。)
 | 
 |  ですから、理屈からいうと、ループは、速度面だけから言えば、
 |      For j = 1 To n
 |          For i = 1 To n
 |              mat(i, j) = ary(i)
 |          Next
 |      Next
 |  のように列方向を優先するのが有利ということになります。
 | 
 |  しかし、(今回の例のように)ユーザーの目的が、
 |  上の行から始めて、順次横方向で比較を行い、
 |  次の行に処理を進める、
 |  といったものであれば、速度のことはさておいて、
 |  そうした処理をそのままコード化する必要があるものと思います。
 | 
 |  列優先と行優先の速度の話は余り目にしたことがなかったので、
 |  参考のためにメモしてみました。
[[20220313003116]] から引用
(xyz) 2025/02/04(火) 13:39:11

 みなさま、ご回答誠にありがとうございます。
 メモリが関係しているのは予想してたんですが
 具体的な例や他言語の話も絡めていただき
 非常にわかりやすかったです。
(TKG) 2025/02/04(火) 16:51:37

コメント返信:

[ 一覧(最新更新順) ]


YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki. Modified by kazu.