ujimushi(@旧sradjp(15364))の日記

旧スラドの日記の引越先です

急にプロセスが止まる(Julia言語) [勝手に回答]

はじめに

いつも勝手気ままに答えている「勝手に回答」。しばらくぶりですが, 今回は急にプロセスが止まるteratail上での質問です。

色々調べてみましたが, 結論としては

「そもそもメモリを100%使い切る状態が悪いのでは?」

ということに尽きます。それでは順に調べたこと等を説明していきます。

プロセスが止まる原因は?

最初は色々思っていたのですが,結論としてはいたってシンプルです。

質問者の話からすると,メモリ使用量は100%近くで, 外部ストレージ上にあるページファイルにメモリの内容が一部移っている状態です。

ネットの検索,AI検索等からシーク時間(記憶領域から必要な情報を引っぱるまでの時間)を比較したものを調べると, 次のような情報がありました。

デバイス シーク時間 / アクセス遅延 速度の目安(HDD比)
HDD 約 4 ~ 15 ms (ミリ秒) 基準(最も遅い)
SSD 約 0.08 ~ 0.16 ms (100 µs前後) 約 50 ~ 100倍 高速
メモリ (RAM) 約 0.00001 ms (10 ~ 100 ns) 約 100,000倍以上 高速

これだとメモリとSSDでも処理時間の差は 0.1 ms / 0.00001 ms → 10,000倍となり 1秒かかっていたものが1万秒,すなわちおよそ2.8時間ほどになります。

で,おそらく図のようにJulia言語で使用中のメモリの一部が外部ストレージに 移動していると考えられ,特にdf(データフレーム)の内容が移動していると考えられます。

そのため,「プロセスが止まる」というのは物理メモリ量不足により,外部ストレージに 移動したデータを参照していて,その時の処理時間が1万倍かかっているから, ということになりそうです。

別にエラーが発生している訳ではない,ということになります。

なので,「停止している」のではなく「きわめて遅い速度で動作している」状態ではないでしょうか?

対策は?

Julia言語の特徴は「とにかくメモリを使いまくって高速化する」というものです。

なので,自分が取り扱うデータ量に対して少なくても2~3倍ぐらいのサイズのメモリが確保できないと正直話になりません。

ということで対策は

  • PCの物理メモリ量を増やす
  • 処理するデータ量を減らす

のどちらかということになります。

質問に対するteratail上での回答やコメントについて

自分が試した限りでは別にpush!を使っても使わなくても状況は変わらなかったので, おそらくほとんどの回答は的外れだと思います。

結局はソースリストに関係なく「処理するデータに対するメモリ不足」ということに尽きます。

回答者・質問者はおそらくJulia言語を使わないユーザーなのでしょう。

唯一「分割して一度に処理するデータ量を減らしてみれば?」というコメントが正しい回答になります。

仮のデータで実行してメモリ使用量を確認してみた例(メモリ容量が十分の場合)

メモリの使用量について

Windowsの場合はtasklistコマンドで使用量が確認できるそうです。なのでその出力を 利用してメモリ使用量を確認してみます。

次のような関数を作成すると文字列(~ KB)として取得可能です。

function memory_usage_string()
    最後の二つ(x) = last(x, 2)
    s = read(`tasklist /FI "IMAGENAME eq julia.exe"`, String) |> split |> 最後の二つ
    "$(s[1]) $(s[2])"
end

上の関数内部のsの配列について,parse(Int64, replace(s[1], "," => ""))とすると, 数値(KB)が得られます。

仮のテスト用のDataFrameを次のように作成して メモリ使用量を確認してみました。

using DataFrames
using Dates

df = begin
    entry_dates = repeat(Date(2011):Day(1):Date(2026), inner=2600)
    exit_dates = map(x -> x + Day(rand(1:10)), entry_dates)
    invests = map(_ -> randn(), entry_dates)
    codes = map(x -> rand() > 0.5 ? x : "cd$x", eachindex(entry_dates))
    DataFrame(entry_date = entry_dates, exit_date=exit_dates, invest = invests, code=codes)
end

次の図がそのメモリ使用量の推移です。ループの数が横軸,縦軸がメモリ使用量です。

ガーベジコレクションが働いた時にメモリ使用量が大きく減少し, 配列(eventsという変数名のもの)の再配置が起きた時にメモリ使用量が大きく増加しているということではないかと思います。

今回もし仮に

events = Vector{NamedTuple{(:date, :type, :trade_idx, :invest, :code),
                           Tuple{Date,Symbol,Int,Float64,String}}}(undef, 2n)

のような形で最初に確保した場合,1GBのメモリを一度に確保することになり, 参照元のdfのメモリ領域はループに入る前にページファイルに移動することになるので, おそらく1回もループが回ることなく停止(見かけ上だけで実際は1万分の1の実行速度で 少しずつ動作している)することになると考えられます。

今回,回答者の回答が的外れと自分が思った理由となります。

さいごに

今回の場合は質問者がかなり緻密な情報を提示しているので, 一番回答者にとって身近な「ソースリスト」の中に原因があるとミスリーティングされています。

現在Teratailの回答者が少なくなって回答の多様性が失われている一つの例になっているような気がします。