

VB.netでのWindowsアプリケーション開発をしています。  
String文字列の連結においてOutOfMemoryExceptionのエラーがスローされます。

手法としてはSingleの数値をカンマで区切り、1000個のカンマ区切り文字列を作成します。  
その文字列は「temp_Str as String」の変数に格納していくのですが、  
大型のループ処理で10万件以上作成されたところで上記のエラーが発生します。  
（時間にして2～3時間程度経過した時点）

少なくとも数100万件の処理を行いたいのですが、  
うまくメモリー管理をする方法はないでしょうか？  
今現在GC.CollectやRemove等の処理は入れているのですが、あまり効果がありません。  
よろしくお願いいたします。


CC BY-SA 3.0
2015-12-22T08:49:22.463
20288
2017-02-13T11:44:28.500
2015-12-22T11:35:50.213
2238
13765
question
windows visual-studio vb.net
Stringの連結時のOutOfMemoryExceptionを回避する方法


「String文字列の連結にはStringBuilderを使うのがいいですよ」と回答を書こうとしてコード例を作ったところ、件数が多すぎてStringBuilderを使ってもOutofMemoryExceptionが出てしまいました。

件数が多くてメモリ上で全件を処理できない場合、行ごとに処理をするよう発想を変えるのがベターだと思います。

この方法なら数100万件を超えても、ディスク容量の許す限りエラーにはなりません。  
※分散処理でDBにでも格納するのがベストかもしれませんが、環境のセットアップなどが必要になるためここでは扱いません。

以下は `C:\tmp123456789.txt` というファイルを一時的に書き出して、各行ごとに処理してからファイルを消すサンプルコードです。  
手元の環境では1万行に対して30秒ほどで出力が、80秒ほどで全体の処理が終わりました。

[code]

    Imports System.IO
    
    Module Module1
    
        Sub Main()
            Dim tmpFile = "C:\tmp123456789.txt" 
    
            Dim stopwatch As New Stopwatch
            stopwatch.Start()
    
            Dim rnd As Random = New Random
            '件数が多いのでStringBuilderでもメモリ不足
            'Dim sb As StringBuilder = New StringBuilder
            'ファイルを開いておいて逐次出力
            Dim writer As New StreamWriter(tmpFile, False)
            'ある程度データがたまったら自動書き出しするモード
            writer.AutoFlush = True
    
            '10の5乗 = 10,000
            For i = 1 To Math.Pow(10, 5)
                Dim list As List(Of Single) = New List(Of Single)
                '行データ作成
                For j = 1 To 1000
                    list.Add(CType(rnd.NextDouble(), Single))
                Next
                '行データ出力(全件データを一気に扱う必要がなければ、StreamWriterを使わずにここで各行の処理をしてもいいです)
                writer.WriteLine(String.Join(",", list))
            Next
            writer.Close()
            Dim msg1 = String.Format("テンポラリファイル作成時間: {0} 秒", stopwatch.ElapsedMilliseconds / 1000)
            Console.WriteLine(msg1)
            'ファイル出力が目的ならここでおしまい。
    
            'テンポラリファイルの各行を読み出しながら処理
            Dim total As Double = 0
            Dim reader As New StreamReader(tmpFile)
    
            ' 読み込みできる文字がなくなるまで繰り返す
            While (Not reader.EndOfStream)
                ' ファイルを 1 行ずつ読み込む
                Dim line = reader.ReadLine
                '何か処理する(ここではサンプルとして加算している)
                For Each s In line.Split(",")
                    total += Single.Parse(s)
                Next
            End While
            reader.Close()
            '後始末
            File.Delete(tmpFile)
    
            Dim msg2 = String.Format("処理完了時間: {0} 秒", stopwatch.ElapsedMilliseconds / 1000)
            Console.WriteLine(msg2)
            Console.WriteLine("合計値は {0} です。", total)
            Console.ReadLine()
        End Sub
    
    End Module
    
[/code]


CC BY-SA 3.0
2015-12-22T10:35:25.940
20293
2015-12-22T23:25:00.453
2015-12-22T23:25:00.453
9820
9820
20288
answer


yhataさんの指摘も踏まえて回答を改めます。

String結合を無制限に繰り返すことが原因で`OutOfMemoryException`が発生する場合ですが、例外が表しているようにメモリ不足の可能性があります。32bitアプリケーションでは2GBのメモリを使用できますが、その全てがデータに使えるわけではないため、それよりも少ない使用量でも`OutOfMemoryException`が発生します。  
`String`の代わりに`StringBuilder`を使用することで処理速度は向上しますが、処理可能なデータ量はほぼほぼ改善されません。結局、処理方法を抜本的に見直す必要があります。少なくともString結合を無制限に繰り返すことはできません。  
必然的にString結合はある程度、少量に抑えられることになるはずですが、その際に作成される文字列が約4万文字以下に抑えられるのであればString結合と`StringBuilder`とにメモリ上の優劣はありません。（4万文字の根拠は、.NETにおいてオブジェクトサイズが85KB未満・以上とで管理方法が異なることに起因します。）  
例えば今回であれば、全件を一気に処理せず1件ずつ処理するように見直した方がいいかもしれません。その場合、1件のデータは

> Singleの数値をカンマで区切り、1000個のカンマ区切り文字列

とのことですが、この程度であれば最悪ケースでも1万文字程度に収まります。

* * *

`StringBuilder`はyhataさんが説明されているように大きな文字列を作成しても85KB未満に収まるように分割管理されているため文字列編集のパフォーマンスに優れます。ただし、これは.NET
4にて改善されたもので、.NET
3.5以前では享受できませんので気を付ける必要があります。その場合どうすべきかというと、予め文字列長を見積もり、`StringBuilder.Capacity`プロパティを設定することです。これにより事前にメモリ確保をし、文字列編集時の逐次メモリ拡張を排除できます。


CC BY-SA 3.0
2015-12-22T12:35:03.697
20296
2015-12-24T02:05:58.897
2015-12-24T02:05:58.897
4236
4236
20288
answer