処理速度が遅い原因を調べる方法
投稿者 魔界の仮面弁士  (社会人)
投稿日時
2022/12/12 20:07:20
> Redim Preserveにて行数を増やし読み込んでいく処理をしています。
ReDim Preserve は、処理としては、比較的『遅い』処理命令となります。
これは、呼び出すたびに、「メモリの確保と破棄」「新旧配列のコピー」がその都度発生するためです。
データ件数が数百件程度ならばさほど問題にはなりませんが、
データ件数が多くなると、処理時間が加速度的に増加していき、
そのうち、現実的なパフォーマンスを得られなくなってしまうでしょう。
ReDim Preserve は、配列の要素数を直接拡縮する命令ではなく、内部的には
(1) 元の配列とは別に、新しいサイズの別の配列をもう一つ用意する
(2) 元の配列の要素を、新しい配列にコピーしていく
(3) 古い方の配列を解放する
(4) 元の変数が新しい配列を参照するように書き換える
という手続きを行っているためです。
ひとまず対策としては:
(A案) 配列ではなくコレクションを使うようにする
(B案) ReDim の回数をできるだけ減らす。
B案については、たとえば確保していた要素数が足りなくなって要素を増やす場合は、
1 件ずつ増加させるのではなく、3千件などといった大きなブロックで一気に確保し、
ReDim Preserve の呼び出し回数を抑えるようにします。
大きく確保すると、列挙が終わった後で末尾に未使用領域が残るので、最後に
ループ完了後に使わなかった分を Preserve で減らすのが良いでしょう。
> ADODBを用いて、txtファイルに30万行前
ADODB ということは VB.NET ではなく、
VBA / VBScript / VB6 などの言語でしょうか?
もしも VB.NET から ADODB を利用されているのだとしたら、メモリ解放の手間が
著しく面倒なので、できるだけ避けた方が良いでしょう。ADO.NET への切り替えをお奨めします。
KB321415:高負荷下で ADO を .NET COM interop または Java で使用するとアクセス違反が発生する
https://bit.ly/3hpBWIu
ReDim Preserve は、処理としては、比較的『遅い』処理命令となります。
これは、呼び出すたびに、「メモリの確保と破棄」「新旧配列のコピー」がその都度発生するためです。
データ件数が数百件程度ならばさほど問題にはなりませんが、
データ件数が多くなると、処理時間が加速度的に増加していき、
そのうち、現実的なパフォーマンスを得られなくなってしまうでしょう。
ReDim Preserve は、配列の要素数を直接拡縮する命令ではなく、内部的には
(1) 元の配列とは別に、新しいサイズの別の配列をもう一つ用意する
(2) 元の配列の要素を、新しい配列にコピーしていく
(3) 古い方の配列を解放する
(4) 元の変数が新しい配列を参照するように書き換える
という手続きを行っているためです。
ひとまず対策としては:
(A案) 配列ではなくコレクションを使うようにする
(B案) ReDim の回数をできるだけ減らす。
B案については、たとえば確保していた要素数が足りなくなって要素を増やす場合は、
1 件ずつ増加させるのではなく、3千件などといった大きなブロックで一気に確保し、
ReDim Preserve の呼び出し回数を抑えるようにします。
大きく確保すると、列挙が終わった後で末尾に未使用領域が残るので、最後に
ループ完了後に使わなかった分を Preserve で減らすのが良いでしょう。
> ADODBを用いて、txtファイルに30万行前
ADODB ということは VB.NET ではなく、
VBA / VBScript / VB6 などの言語でしょうか?
もしも VB.NET から ADODB を利用されているのだとしたら、メモリ解放の手間が
著しく面倒なので、できるだけ避けた方が良いでしょう。ADO.NET への切り替えをお奨めします。
KB321415:高負荷下で ADO を .NET COM interop または Java で使用するとアクセス違反が発生する
https://bit.ly/3hpBWIu
投稿者 魔界の仮面弁士  (社会人)
投稿日時
2022/12/12 20:55:10
> 30万行前後記載されている数列を読み込んでおります。
上限が分かっているのなら、たとえば 40万行などの巨大サイズを最初にドカッと確保し、
ループ中での ReDim Preserve を一切行わないようにしてみたらどうでしょうか?
事前確保であれば、そんなに遅くはならないはず…。
参考資料として:
🔹VBAのRedim Preserveは本当に遅いのか(データ型を指定しないと遅くなる)
https://vbabeginner.net/redim-preserve-really-slow/
🔹VBA 配列の速度比較 (ループ中に毎回 ReDim Preserve で +1 ずつ増やすと遅い)
https://howto-it.com/excelvbacollection.html
🔹【VB.NET】Redim・Redim Preserve・UBoundは古い!Generic.Listを使おう
https://www.totaltech365.net/entry/generic-list
上限が分かっているのなら、たとえば 40万行などの巨大サイズを最初にドカッと確保し、
ループ中での ReDim Preserve を一切行わないようにしてみたらどうでしょうか?
事前確保であれば、そんなに遅くはならないはず…。
参考資料として:
🔹VBAのRedim Preserveは本当に遅いのか(データ型を指定しないと遅くなる)
https://vbabeginner.net/redim-preserve-really-slow/
🔹VBA 配列の速度比較 (ループ中に毎回 ReDim Preserve で +1 ずつ増やすと遅い)
https://howto-it.com/excelvbacollection.html
🔹【VB.NET】Redim・Redim Preserve・UBoundは古い!Generic.Listを使おう
https://www.totaltech365.net/entry/generic-list
投稿者 湯  (社会人)
投稿日時
2022/12/14 16:54:33
丁寧なご指導ありがとうございました。
最初の大きく確保し、最後にRedim Preserve を一回するようにいたしました。
すると20分かかっていた処理が10秒で完了するようになりました。こんなに影響があるとは...
ADO.netについてはまだ勉強不足で試してせていないのですが、処理時間の問題は解決できました。
迅速な回答ありがとうございました。
最初の大きく確保し、最後にRedim Preserve を一回するようにいたしました。
すると20分かかっていた処理が10秒で完了するようになりました。こんなに影響があるとは...
ADO.netについてはまだ勉強不足で試してせていないのですが、処理時間の問題は解決できました。
迅速な回答ありがとうございました。
投稿者 魔界の仮面弁士  (社会人)
投稿日時
2022/12/14 19:40:27
ADODB.Recordset を使っているのなら、
→ GetRows メソッドで二次元配列化
→ GetString メソッドで CSV/TSV 化
をループなしで一括変換できますね。
ループ中で変換・判定処理とかを行っている場合には使えませんが、
単純出力の場合には便利かと思います。
> ADO.netについてはまだ勉強不足で試してせていないのですが
ということは、VB.NET をお使いですか? (VBA では ADO.NET を使えませんので)
もしも VB.NET から ADODB を扱う場合、先に紹介した URL にもありますように、
使用後に COM オブジェクトを Marshal.ReleaseComObject メソッドで
解放する必要があります。
Recordset の解放は、手順的にはこんなイメージですね。
(掲示板に直接記述したので、試してはいませんが)
Recordset などと同様に、Connection オブジェクトも解放対象です。
なお、上記で IsComObject メソッドで事前チェックしているのは、
「Fields が COM オブジェクトかどうか」が異なる可能性があるためです。
これは、参照設定している ADODB のライブラリが
自動生成された Interop アセンブリ (IA) を使っているのか、それとも
システムに登録された Primary Interop アセンブリ(PIA) を使っているのかで変わります。
そして 基本的には、PIA の利用が強く推奨されています。
(しかしそれ以上に、ADODB から ADO.NET に移行することがさらに望ましい)
https://bit.ly/3hsV9JC
もし、Fields や Field のオブジェクト管理が面倒なのであれば、
フィールドへのアクセスに Collect プロパティを使う手もあります。
「rs.Fields("Col1").Value = NewValue」ではなく
「rs.Collect("Col1") = NewValue」とする感じで。
Collect プロパティが読み書きする値は、COM オブジェクトではないはずなので、
この場合、解放処理は Recordset (と Connection) だけで済むでしょう。
※ただし稀に、COM オブジェクトが返されるケースもあります。
たとえば "Provider=MSDataShape" の場合、階層型 Recordset として、
列の値として「子階層の Recrodset」が入れ子上に返されてきたりします。
→ GetRows メソッドで二次元配列化
→ GetString メソッドで CSV/TSV 化
をループなしで一括変換できますね。
ループ中で変換・判定処理とかを行っている場合には使えませんが、
単純出力の場合には便利かと思います。
> ADO.netについてはまだ勉強不足で試してせていないのですが
ということは、VB.NET をお使いですか? (VBA では ADO.NET を使えませんので)
もしも VB.NET から ADODB を扱う場合、先に紹介した URL にもありますように、
使用後に COM オブジェクトを Marshal.ReleaseComObject メソッドで
解放する必要があります。
Recordset の解放は、手順的にはこんなイメージですね。
(掲示板に直接記述したので、試してはいませんが)
Recordset などと同様に、Connection オブジェクトも解放対象です。
Dim fs = rs.Fields 'Fiedls コレクション オブジェクト
Dim fCol1 = fs("Col1") 'Field オブジェクト
Dim fCol2 = fs("Col2") 'Field オブジェクト…フィールドの数だけ用意する
row = -1
Do Until rs.EOF
row += 1
ary(0, row) = fCol1.Value
ary(1, row) = fCol2.Value
rs.MoveNext()
Loop
If Marshal.IsComObject(fCol2) Then Marshal.ReleaseComObject(fCol2) '解放処理
If Marshal.IsComObject(fCol1) Then Marshal.ReleaseComObject(fCol1) '解放処理
If Marshal.IsComObject(fs) Then Marshal.ReleaseComObject(fs) '解放処理
rs.Close()
If Marshal.IsComObject(rs) Then Marshal.ReleaseComObject(rs) '解放処理
ReDim Preserve ary(1, row)
なお、上記で IsComObject メソッドで事前チェックしているのは、
「Fields が COM オブジェクトかどうか」が異なる可能性があるためです。
これは、参照設定している ADODB のライブラリが
自動生成された Interop アセンブリ (IA) を使っているのか、それとも
システムに登録された Primary Interop アセンブリ(PIA) を使っているのかで変わります。
そして 基本的には、PIA の利用が強く推奨されています。
(しかしそれ以上に、ADODB から ADO.NET に移行することがさらに望ましい)
https://bit.ly/3hsV9JC
もし、Fields や Field のオブジェクト管理が面倒なのであれば、
フィールドへのアクセスに Collect プロパティを使う手もあります。
「rs.Fields("Col1").Value = NewValue」ではなく
「rs.Collect("Col1") = NewValue」とする感じで。
Collect プロパティが読み書きする値は、COM オブジェクトではないはずなので、
この場合、解放処理は Recordset (と Connection) だけで済むでしょう。
※ただし稀に、COM オブジェクトが返されるケースもあります。
たとえば "Provider=MSDataShape" の場合、階層型 Recordset として、
列の値として「子階層の Recrodset」が入れ子上に返されてきたりします。
ADODBを用いて、txtファイルに30万行前後記載されている数列を読み込んでおります。
ReadTextにて1行ずつ読み込み、総行数が不定の為Redim Preserveにて行数を増やし読み込んでいく処理をしています。
動作としては問題ないのですが、8MB程の容量のファイルを読み込むのに20分程かかっており、処理が遅いと感じています。
そこで、どの部分の処理が遅いのか判別する記述や方法、もしくは考え方があれば教えていただけますでしょうか。