PAP-40-Layer Framework 第29層のアイキャッチ

大量データを扱うAI生成コードのメモリ消費を見積もり、確認する

  • URLをコピーしました!

大量データのメモリ消費は、ファイルサイズだけでは見積もれません。全件を読み込んだ配列、変換後のオブジェクト、一時コピー、集計結果が同時に存在する区間を探し、そのピークを測ります。ストリームや分割処理へ変える場合も、途中結果を全て保持していないか、分割をまたぐ集計と出力順序が元の要件を満たすかを確認します。

以下は、架空のCSVから地域別の金額と件数を集計する例です。Python 3.14、標準ライブラリだけを想定します。掲載コードは未実行で、メモリ測定も実施していません。数値は説明用の仮定または算術結果であり、特定環境で確保された容量ではありません。

目次

入力から出力まで同時に残るデータを探す

AIが提案した処理を読むときは、読込み、型変換、絞込み、並べ替え、集計、出力という順に、何がメモリ上へ作られるかを書き出します。ファイルを一度文字列へ読み、CSVの行一覧へ変換し、さらに別の配列へ整形する実装なら、前の表現が解放されるまで複数の形が同時に残ります。

ただし、コピーと書かれた処理が全て同じ量を複製するわけではありません。容器だけを複製して中身の参照を共有する場合と、各要素まで新しく作る場合があります。元の一覧と複製を見て、同じ文字列を機械的に二重計上するのも誤りです。どのオブジェクトが共有され、どの時点まで生存するかを確認します。

概算は「実行環境の基礎使用量+同時に生存する入力・変換結果・一時領域・出力」として組み立てます。全ての工程の最大値を足すのではなく、重なる区間ごとに見積もります。順番に解放される領域まで合算すると過大になり、処理の最後だけを見ると途中のコピーを見落とします。

仮に、読み込んだ1行の平均的な保持費用が320バイト、対象が100万行なら、その表現だけで3億2,000万バイト、約305.2MiBです。ここで1MiBは1,048,576バイトです。320バイトは仮定値であり、CSVの平均行長やPythonの固定仕様ではありません。文字列長、列数、型、処理系によって変わるため、実際の標本で確認します。

同じ320バイトという仮定を1,000万行へ延ばすと約2.98GiBになります。これは一つの表現だけの値で、実行環境、変換中の領域、出力、同時ジョブを含みません。「メモリが4GiBだから入る」と結論する前に、コンテナや処理基盤から実際に割り当てられる上限と、他の使用量を確かめます。

Pythonのsys.getsizeofは、対象オブジェクトに直接属するサイズを返し、参照先まで自動的に合計しません。一覧そのもののサイズだけで全行分を測ったことにしないでください。共有参照を再帰的に数える場合も、同じオブジェクトの二重計上を避ける必要があります。

全件読込をやめる前に集計の意味を固定する

架空のCSVは、列がregionとamount_yenの二つ、文字コードがUTF-8、金額は非負の整数円とします。地域はeastとwestだけを認め、不明な地域や金額は入力不正です。税、通貨変換、値引きは扱いません。出力は地域別の合計と件数で、個々の明細を保存する要件はないものとします。

region,amount_yen
east,100
west,500
east,300
west,100
east,500

この入力の期待値は、eastが900円・3件、westが600円・2件です。総額は1,500円、全体は5件になります。この値を先に手作業で定め、全件読込でも分割読込でも同じ結果になることを確かめる材料にします。処理方式を変えた後の出力を、そのまま正解にしてはいけません。

変更前の典型は、CSVリーダーをlistへ変換してから処理する構造です。Pythonのcsvは行を読み取るリーダーを提供しますが、その結果をlistへ詰めれば全件を保持します。ジェネレーターを使ったという説明があっても、直後に全件を具体化しているなら、メモリ上の性質は変わります。

#変更前の概略。readerはcsv.DictReaderを想定する。
all_rows = list(reader)
#all_rowsを集計するまで全件を保持する。

ここでの修正は、読み取る行数を絞り、合計と件数だけを次の分割へ引き継ぐことです。行ごとの処理で足りるなら一行ずつ、ライブラリがまとまった行を効率よく処理できるなら一定件数ずつ読みます。分割サイズは速さと使用量の交換条件になるので、名前だけで「省メモリ」と判断しません。

CSVを固定バイト数や改行文字だけで手作業分割する方法にも注意が必要です。引用されたフィールドに改行が含まれる形式では、物理的な一行と一レコードが一致しません。CSVパーサーでレコードとして読み、そのレコードを分割する方針を先に決めます。区切り文字や引用規則を変える場合も、見た目だけで推測せず入力の契約と照合します。

分割の外側に必要な状態だけを保持する

次は、一定件数の行をまとめながら集計する説明用コードです。ファイルの読取り以外の外部操作はなく、出力ファイルへの書込みもありません。CSVのヘッダーと値の条件を確認し、不正な行があれば処理全体を失敗させます。既存の成果物を更新する本番コードではありません。

import csv
import re
from itertools import batched

def summarize_csv(path, batch_size=1000):
    if type(batch_size) is not int or batch_size < 1:
        raise ValueError("invalid batch size")
    totals = {"east": 0, "west": 0}
    counts = {"east": 0, "west": 0}
    with open(path, encoding="utf-8", newline="") as handle:
        reader = csv.DictReader(handle, strict=True)
        if reader.fieldnames != ["region", "amount_yen"]:
            raise ValueError("unexpected header")
        for batch in batched(reader, batch_size):
            for row in batch:
                if None in row or any(v is None for v in row.values()):
                    raise ValueError("invalid column count")
                region = row["region"]
                raw = row["amount_yen"]
                if region not in totals:
                    raise ValueError("unknown region")
                if re.fullmatch(r"[0-9]{1,9}", raw) is None:
                    raise ValueError("invalid amount")
                totals[region] += int(raw)
                counts[region] += 1
    return {key: {"total_yen": totals[key], "count": counts[key]}
            for key in ("east", "west")}

金額を1〜9桁へ制限したのは、この架空入力の一行当たりの規則です。全業務に共通する上限ではありません。空欄、負数、小数、桁あふれを勝手に丸めたりゼロにしたりせず、要件に合わせて入力規則を変更します。許容上限を変えたら、メモリと異常時の条件も見直します。

分割サイズを2とすると、最初の2行、次の2行、最後の1行に分かれます。totalsとcountsは分割の外側にあるため、eastの100円、300円、500円が別の分割でも合計されます。各分割の結果を新しい辞書で上書きすると、最後の500円だけを残してしまいます。少量の例でこの境界を意図的に作り、状態の引継ぎを点検します。

標準ライブラリのitertools.batchedは、入力から必要な分を取り出してタプルへまとめ、既定では最後の短い分割も扱います。ここではその性質を用いますが、関数全体のメモリが一定だという保証ではありません。CSVの一行自体が非常に大きければ、その行を保持する領域が必要です。

また、地域を固定せず任意の顧客IDで集計するなら、集計辞書の要素数は一意な顧客数に応じて増えます。入力を小分けにしても、この状態が増え続ければ上限へ達します。多数のキーを扱う場合は、外部へ部分集計を退避する方式や、DB側の集計なども候補にします。その際は保存先の権限、容量、失敗時の再開方法まで別途設計します。

分割すると答えが変わる処理を見分ける

合計と件数は、分割ごとの値を加える形で結合できます。ただし、平均値を分割ごとに計算して、その平均だけを再び平均すると、分割の大きさが違う場合に誤ります。上の5行を2・2・1行に分けた平均は、300円、200円、500円です。この三つの単純平均は約333.3円ですが、全体の正しい平均は1,500円÷5件の300円です。

平均が必要なら、合計と件数を引き継ぎ、最後に割ります。丸めも途中で繰り返すのか最後に行うのかを決めます。今回は整数の合計なので途中の小数丸めはありませんが、実数や通貨の換算を含めるなら、その計算規則を別に固定する必要があります。

重複除去、順位、中央値、全体の並べ替えは、単純な分割ごとの処理だけで完結しません。分割内で重複がなくても、最初と最後に同じIDがある可能性があります。全体の情報が必要な処理では、必要な状態を保持する、外部ソートする、別の実装へ委ねるなど、結果の意味を守る方法を選びます。

pandasの大規模データの扱いも、分割間の調整が少ない処理ではchunkingが適し、複雑な処理には別の方法を検討する説明をしています。この記事のコードはpandasを使いませんが、分割可能性を処理の意味から判断する点は共通します。pandasの例で示された削減率を、今回のコードの効果へ転用はしません。

入力順序を保つ要件がある場合は、集計とは分けて確認します。明細を書き出す処理で分割を並列化すると、完了した順に出力して原順序が変わることがあります。元の行番号などで順序を管理する必要がありますが、順序待ちの結果を無制限にためればメモリを再び消費します。順序、並列度、待機結果の上限を一緒に定めます。

ピークメモリは測定対象と開始位置を揃える

メモリの確認では、現在値とピーク値を分けます。処理終了時に一時配列が解放されていても、処理中に上限を超えた問題は消えません。比較する方式ごとに新しいプロセスを使うなど、前の試行で確保した領域やキャッシュが結果へ混ざる条件を揃えます。

Pythonのtracemallocは、Pythonが追跡するメモリ割当てについて、現在量・ピーク量や発生箇所を調べられます。追跡開始前に作った入力まで含まれたとは限らず、プロセス全体の常駐メモリを表す値でもありません。計測器自身の費用もあるため、プロセスや実行基盤の観測と測定対象を分けて記録します。

全件読込と分割読込を比べるなら、同じCSV、文字コード、入力検証、合計の規則、出力範囲を使います。前者だけ全明細を出力し、後者は合計だけ返す比較では、入力方式以外も変わっています。保持する成果物が同じ条件になっているかを先に確認してください。

記録には、入力バイト数と行数、最大行長、列数、キーの種類数、分割サイズ、同時実行数、Python・OSの版、割当上限を入れます。ピークの単位、観測方法、対象プロセス、測定開始と終了の位置も残します。MiBとMBを混ぜず、計測不能な箇所をゼロとして扱いません。

割当上限から観測ピークを引いた余裕も、次の入力で確実に使える容量とは限りません。最大行長や一意キー数が変わる条件、同じプロセスで別処理が動く条件を含めて評価します。余裕の決め方は運用の変動幅に合わせ、根拠のない一律の安全率で置き換えません。分割サイズを下げてもピークが変わらないなら、入力以外の状態やネイティブ領域が支配している可能性を調べます。

件数を段階的に増やし、長い文字列や偏ったキーを含む条件も試験計画に加えます。平均行長が短いCSVだけで合格しても、一行が極端に長い入力へ適合したとは言えません。計測前に入力サイズと資源の上限を設定し、共有環境で限界まで増やして障害を起こす試し方を避けます。

境界と失敗を確認してから利用範囲を決める

今回のコードで確認する計画は、以下のように整理できます。全て未実行の期待結果です。合計が一致したという一点だけで、文字コード、形式、上限への適合まで合格にしません。

確認する入力・条件 期待する結果 失敗・未確認時の対応
5行を分割サイズ1・2・10で集計 east900円3件、west600円2件で一致 状態の初期化位置と最後の分割を確認
同じ地域が分割境界の両側にある 前後の金額が両方加算される 部分結果の上書きや取りこぼしを修正
ヘッダーだけでデータ0行 両地域とも0円0件 空入力を許す業務かを別途確認
金額空欄・不明地域・列数不正 例外となり完成結果を返さない 入力箇所を特定し、修正後に再処理
読取り途中でI/Oエラー 処理失敗となり完了扱いしない 入力の完全性と再開単位を確認
設定したメモリ上限へ接近 継続条件を見直す 分割サイズ・キー数・同時実行を再評価

実際に出力ファイルやDBへ途中結果を書き込むなら、後半の入力エラーで既に書いた分が残る可能性があります。完成前の出力を公開しない、処理単位の識別子を持つ、再実行時の重複を防ぐなど、書込み側の設計が必要です。本稿の関数は全体を読み終えてから結果を返すため、この永続化の回復処理を省略しています。

読込み中に元ファイルが変更される条件も、分割方式とは別の再現性の問題です。処理対象の固定された版を用意し、入力の識別情報と完了時の照合を残します。途中失敗後に更新済みのファイルへ同じ位置から戻ると、行の重複や欠落につながるため、入力版を確認できない再開は保留します。

ストリーム化で全件の保持を減らせても、行の最大長、集計キー数、出力の保持、並列処理が大きければメモリは増えます。どの領域が支配的かを観測し、その領域へ対処します。件数に比例する入力を減らす変更なのか、コピーを減らす変更なのか、状態を外部へ移す変更なのかが分かれば、測定で確かめる条件も明確になります。

出典・参考資料

  1. Python Software Foundation「Python 3.14 sys.getsizeof」(確認日:2026年10月12日)
  2. Python Software Foundation「Python 3.14 csv」(確認日:2026年10月12日)
  3. Python Software Foundation「Python 3.14 itertools.batched」(確認日:2026年10月12日)
  4. pandas開発チーム「Scaling to large datasets」(確認日:2026年10月12日)
  5. Python Software Foundation「Python 3.14 tracemalloc」(確認日:2026年10月12日)
よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

さかっぺのアバター さかっぺ PAP Research運営 / AIガバナンス・アーキテクト / プロンプトデザイナー

ITインフラの実務経験をもとに、AIの安全性と実用性を両立する仕組みを研究。PAP Researchでは、AIモデルの継続観測と、プロンプト・AI技術の実務解説を発信しています。

経歴や研究方法、発信方針は運営者プロフィールで紹介しています。

目次