最近、SQL ServerにPDFファイルなどを保存する必要があって、Insert文を生成するためのツールを作成していました。
その中で、C#プログラムがメモリ不足で落ちてしまうことがあり、いろいろ調べたのでまとめておきます。

tl;dr

  • SQL文でファイルをSQL Serverのvarbinay型カラムにInsertしたかった
  • SQL文を生成するツールをC#で作ったがメモリ不足で落ちた
  • 原因:ファイルを一気に読み込んでいたため
  • 解決策:BinaryReaderで少しずつ読み込む

問題がおきたコード

やりたいこととしては、ファイルを読みこんでBase64形式の文字列に変換したい、ただそれだけでした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void メモリ不足になるコード() {
// 対象のフォルダ配下のファイル一覧
string[] files = System.IO.Directory.GetFiles(@"フォルダパス", "*", System.IO.SearchOption.AllDirectories);

foreach (var file in files)
{
using (var fs = new System.IO.FileStream(file, System.IO.FileMode.Open, System.IO.FileAccess.Read))
{
// ファイルをバイト配列に変換
byte[] bs = new byte[fs.Length];
fs.Read(bs, 0, bs.Length);
fs.Close();

// base64形式に変換
string base64str = System.Convert.ToBase64String(bs);
}
}

Console.WriteLine("読み込み完了");
}

上記のコードを実行すると、3GBくらいのメモリを食ってしまってることが分かりました。

解決策

調べてみると、

1
fs.Read(bs, 0, bs.Length)

のところで、ファイルを一気に読み込んでしまっているのが原因らしいと言うことが分かりました。

そのため、少しずつ読み込んで行く方針に変えてみるとメモリ使用量を減らすことができました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void 軽くしたバージョン()
{
// 読み込むファイル一覧
string[] files = System.IO.Directory.GetFiles(@"フォルダパス", "*", System.IO.SearchOption.AllDirectories);

foreach (var file in files)
{
using (var fs = new System.IO.FileStream(file, System.IO.FileMode.Open, System.IO.FileAccess.Read))
{
// --------------- ここから変更 ---------------
// 少しずつ読み込むためにBinaryReaderを使う
using (var br = new System.IO.BinaryReader(fs))
{
// 一度に読み込むサイズ
const int CHUNK_SIZE = 1024;

// ファイルをバイト配列に変換
byte[] bs = new byte[CHUNK_SIZE];
bs = br.ReadBytes(CHUNK_SIZE);
// 順次読み込んでBase64形式の文字列に変換した結果を結合する
StringBuilder base64str = new StringBuilder();

// 最後まで読み込む
while (bs.Length > 0)
{
// base64形式に変換
base64str.Append(System.Convert.ToBase64String(bs));

bs = br.ReadBytes(CHUNK_SIZE);
}
}
}
}

Console.WriteLine("読み込み完了");
}

上記のコードだと、メモリ使用量を減らすことができました。
(それでも結構食ってますが…。落ちることはなくなりました。)

まとめ

ファイル数が多かったり、大きすぎると、一気にファイルを読み込むとメモリ不足になる可能性があります。

BinaryReaderで少しずつ読み込んで処理しましょう。

参考