【テクニカル・上級編】Byte型配列によるバイナリデータ処理:ファイル読み込みの高速化とメモリ節約術 – Excel VBA解析バイブル

スポンサーリンク

Byte型配列によるバイナリデータ処理:ファイル読み込みの高速化とメモリ節約術

VBA(Visual Basic for Applications)の現場において、数メガバイトを超えるテキストファイルやログデータの処理を任されたことはないだろうか。

`Line Input #` や `Input$()` 関数を使い、`String` 型の変数へ愚直にデータを流し込む。数万行程度であれば体感速度に問題はない。しかし、これが50MB、100MB、あるいはそれ以上の規模になると、VBAは突如として沈黙する。タスクマネージャーを開けば、Excelプロセスが数百MBのメモリを喰らい、ガベージコレクションの嵐の中でCPU使用率が跳ね上がっている。

なぜ、VBAでの巨大ファイル処理はこれほどまでに遅いのか。そして、なぜ破綻するのか。
その答えは、`String` 型の内部表現(UTF-16)と、ファイルI/Oの非効率なアロケーションにある。

今回は、数々のレガシーシステムを裏で支えてきたチーフアーキテクトの視点から、`Byte` 型配列を用いたバイナリ直接読み込みによるメモリ最適化と極限の高速化手法を解き明かす。

1. なぜ `String` 型でのファイル読み込みは破綻するのか?

多くのVBAプログラマは、テキストファイルを扱う際に無意識に `String` 型を使用する。しかし、アーキテクチャの観点から見れば、これは地雷原を裸足で歩くようなものだ。

UTF-16の呪縛とメモリの倍増

Windows環境下のVBAにおける `String` 型は、1文字あたり2バイト(16ビット)のUTF-16で内部保持される。
もし処理対象がUTF-8やShift-JIS(1文字1〜2バイト混在)のファイルであったとしても、メモリ上に展開された瞬間、すべての文字が強制的に2バイトにパディングされる。つまり、ファイルサイズがそのままメモリ上のサイズになるわけではなく、特にシングルバイト文字主体であれば実質的にメモリ効率が半減する。

文字コード変換のオーバーヘッド

`Input$` 関数などでファイルを読み込む際、VBAのランタイムは裏側で文字コードの変換(ANSI⇔Unicode)を行っている。この暗黙の変換処理(Marshalling)が、数百万行のループ回数と相まって膨大なCPUサイクルを浪費する。

文字列結合(`&` 演算子)の悪夢

「1行ずつ読み込んで変数に蓄積する」というコードを見たことがないだろうか。

‘ ──【アンチパターン】絶対にやってはいけない文字列結合 ──
Dim allText As String, lineData As String
Do While Not EOF(1)
Line Input #1, lineData
allText = allText & vbCrLf & lineData ‘ メモリの再割り当てが毎行発生する
Loop

VBAの `String` はイミュータブル(不変)ではないが、文字列が長くなるにつれて結合時のメモリ再割り当て(Reallocation)コストが幾何学的に増大する。これが、ファイルサイズが大きくなるにつれて処理が「指数関数的に遅くなる」根本原因である。

2. 解決策:`Byte` 型配列による一括バイナリ読み込み

この絶望的な状況を打破する唯一にして最強の手段が、ファイルを単なる「バイトの塊(バイナリ)」として `Byte` 型の動的配列へ一括ロードする手法だ。

ディスク上のファイルを一度のI/Oでメモリ上に丸ごとロードし、ポインタ演算やインデックスアクセスによってメモリ上で完結させる。これにより、ファイルI/Oの回数は「1回」に激減し、不要な文字コード変換や文字列結合のオーバーヘッドを完全に排除できる。

実装アーキテクチャ:高速バイナロリーダ

以下に、実業務の現場で即座に使える、極限まで最適化されたバイナリ一括読み込みのコードを示す。

Option Explicit

‘ ── Windows API宣言:大容量ファイル操作の高速化・確実化のため ──
If VBA7 Then
Private Declare PtrSafe Function CreateFileW Lib “kernel32” ( _
ByVal lpFileName As LongPtr, ByVal dwDesiredAccess As Long, _
ByVal dwShareMode As Long, ByVal lpSecurityAttributes As LongPtr, _
ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As LongPtr) As LongPtr

Private Declare PtrSafe Function GetFileSizeEx Lib “kernel32” ( _
ByVal hFile As LongPtr, ByRef lpFileSize As Currency) As Long

Private Declare PtrSafe Function ReadFile Lib “kernel32” ( _
ByVal hFile As LongPtr, ByRef lpBuffer As Any, _
ByVal nNumberOfBytesToRead As Long, ByRef lpNumberOfBytesRead As Long, _
ByVal lpOverlapped As LongPtr) As Long

Private Declare PtrSafe Function CloseHandle Lib “kernel32” ( _
ByVal hObject As LongPtr) As Long

Private Const INVALID_HANDLE_VALUE As LongPtr = -1
Private Const GENERIC_READ As Long = &H80000000
Private Const FILE_SHARE_READ As Long = &H1
Private Const OPEN_EXISTING As Long = 3
Private Const FILE_ATTRIBUTE_NORMAL As Long = &H80
Else
‘ 32bit環境(必要に応じて維持)
‘ ※現代のインフラでは基本的にVBA7前提で設計すべきである
End If

Public Sub ExecuteHighSpeedBinaryRead()
Dim filePath As String
filePath = “C:\Data\HugeLogFile.log” ‘ 処理対象の大規模ファイル

Dim t0 As Double
t0 = Timer

On Error GoTo ErrorHandler

‘ 1. バイナリモードでの高速一括読み込みを実行
Dim fileData() As Byte
fileData = LoadFileAsByteArray(filePath)

Dim byteCount As LongPtr
byteCount = UBound(fileData) – LBound(fileData) + 1

Debug.Print “読み込み完了: ” & byteCount & ” バイト / 処理時間: ” & Format(Timer – t0, “0.00秒”)

‘ ── ここにメモリ上での高速パース処理を記述する ──
‘ 例: 改行コード(LF: &H0A)を検知して行単位の処理を行う等
ProcessBinaryData fileData, byteCount

Exit Sub

ErrorHandler:
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical
End Sub

‘ ── ファイルをByte型配列に一括ロードするコア関数 ──
Private Function LoadFileAsByteArray(ByVal filePath As String) As Byte()
Dim hFile As LongPtr
hFile = CreateFileW(StrPtr(filePath), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)

If hFile = INVALID_HANDLE_VALUE Then
Err.Raise 10001, , “ファイルを開けませんでした: ” & filePath
End If

‘ 64ビットファイルサイズ取得(Currency型はVBAで安全に8バイト整数を扱える)
Dim fileSize As Currency
If GetFileSizeEx(hFile, fileSize) = 0 Then
CloseHandle hFile
Err.Raise 10002, , “ファイルサイズの取得に失敗しました。”
End If

‘ Currency型は10000倍されているため、実バイト数に戻す
Dim totalBytes As LongPtr
totalBytes = CCur(fileSize) 10000

If totalBytes = 0 Then
CloseHandle hFile
Dim emptyArray() As Byte
LoadFileAsByteArray = emptyArray
Exit Function
End If

‘ 巨大なメモリブロックを一括アロケーション
ReDim fileData(0 To totalBytes – 1) As Byte

Dim bytesRead As Long
Dim offset As LongPtr
offset = 0

‘ 2GBを超えるファイルに対応するため、チャンク単位(最大2GB弱)で安全に読み込む
Const CHUNK_SIZE As Long = &H7FFF000 ‘ 約2GB

Do While offset < totalBytes Dim readSize As Long If (totalBytes - offset) > CHUNK_SIZE Then
readSize = CHUNK_SIZE
Else
readSize = CLng(totalBytes – offset)
End If

‘ API経由でバッファへ直読み込み
If ReadFile(hFile, fileData(offset), readSize, bytesRead, 0) = 0 Then
CloseHandle hFile
Err.Raise 10003, , “ファイルの読み込み中にエラーが発生しました。”
End If

offset = offset + bytesRead
Loop

CloseHandle hFile
LoadFileAsByteArray = fileData
End Function

Private Sub ProcessBinaryData(ByRef data() As Byte, ByVal size As LongPtr)
‘ メモリ上のByte配列を直接走査するサンプル
‘ 文字列変換を行わないため、圧倒的な速度を誇る
Dim i As LongPtr
Dim lineCount As Long

lineCount = 0
For i = 0 To size – 1
‘ 例: LF (&H0A) を検出して行数をカウント
If data(i) = &H0A Then
lineCount = lineCount + 1
End If
Next i

Debug.Print “総行数(推計): ” & lineCount
End Sub

3. チーフアーキテクトが教える:メモリ最適化とライフサイクルの極意

プログラミングにおいて「メモリを確保する」こと以上に重要なのは、「使い終わったメモリを確実に、綺麗に消し去る」ことだ。特にVBA(COM基盤)環境では、GC(ガベージコレクション)の挙動がブラックボックスであり、適切なスコープ管理と明示的な解放を行わなければメモリリークやフラグメンテーションの温床となる。

1. 動的配列の明示的消去(Eraseの徹底)

数テンメガバイト、あるいは数百メガバイトの `Byte` 型配列 (`fileData()`) をプロシージャ内で生成した場合、そのプロシージャが終了するまでメモリは保持され続ける。
もし同一モジュール内で何度もファイルを読み込むようなバッチ処理であれば、処理の完了直後に以下を実行し、即座にOSへメモリを返還せよ。

‘ メモリの即時解放
Erase fileData

`Erase` を明示することで、VBAランタイムに対してメモリブロックの解放を促し、プロセスのフットプリントを最小限に抑えることができる。

2. オブジェクトの参照チェーンを切断する

APIを直接叩く場合、ハンドル(`HANDLE`)の解放漏れは致命的だ。ファイルハンドルやデバイスコンテキストを開いたまま例外(Error)が発生すると、リソースリークを引き起こす。
必ず `On Error Goto` 構文を活用し、異常系であっても確実に `CloseHandle` が実行される防御的プログラミングを徹底すること。上記のコード例で `CloseHandle hFile` が各エラーハンドラに配置されているのはそのためだ。

3. バリアント型(Variant)の排除

配列やインデックスカウンタに `Variant` を使う愚を犯してはならない。
ループカウンタ `i` には必ず `LongPtr` または `Long` を使用し、暗黙の型変換(Variantによるボクシング/アンボクシング)を完全に排除する。これだけで数百万回のループ実行速度が数倍〜数十倍変わる。

総括

VBAは「おもちゃの言語」ではない。アーキテクチャの制約を正確に理解し、Windows APIやメモリ管理のプリミティブを正しく使いこなせば、エンタープライズ領域のデータ処理をも高速にこなす実用的なツールへと昇華する。

今回紹介した `Byte` 型配列によるバイナリ直接処理は、単なるファイル読み込みの枠を超え、テキストパース、バイナリファイルの解析、高速なネットワーク通信データの処理など、あらゆるシチュエーションに応用できる強力な武器となる。

レガシーシステムの限界に直面したとき、思い出してほしい。
「文字列で悩んだら、すべてをただのバイトの列として捉えよ」と。

タイトルとURLをコピーしました