IDLで大規模データを扱う際には、メモリ管理が重要となる。場合によっては、物理メモリのサイズを超えるような巨大なデータを扱ったり、複数プロセスで並列処理を行いプロセス間でデータの交換・共有を行う必要が生じる。
現在のIDLのdinamic memory使用量を表示する(単位: バイト)。
print,memory(/current)
; heap memory usedの項目
help,/memory
IDLのコアプロセスが使用するメモリのみが表示される。共有メモリ、子プロセスや外部ライブラリが使用するメモリは含まれない。
ある処理に必要なdynamic memory使用量を見積もる。
start_mem = memory(/current)
; 適当な処理を行う
a = bytarr(1024,1024,1024)
print, memory(/highwater) - start_mem
memory(/highwater)
は前回memory
関数が呼び出されたか、help,/memory
の実行以降でのdynamic memoryの使用量の最大値を返す。
共有メモリは複数のプロセスから同時に読み書きできるメモリのことであり、複数のプロセス間でデータの交換・共有に使われる。IDL では特に IDL_IDLBridge
使用時に威力を発揮する。
Linuxでは共有メモリとして、System V (shmget) とPOSIX (shm_open)があるが、IDLでは両方の形式をサポートする。WindowsではCreateFileMapping関数が用いられる。
Linuxの場合、/proc/sys/kernel/shmmax でプロセスごとの共有メモリの最大サイズを調べる。最近の x86_64 環境ではデフォルトで十分なサイズになっているはず。
$ cat /proc/sys/kernel/shmmax
18446744073692774399
i386環境では小さすぎることがあるので、この場合はrootで次のようにして変更する。
# sysctl -w kernel.shmmax=536870912
kernel.shmmax = 536870912
IDLでSystem V共有メモリを操作する。SHMMAP
関数に /SYSV
キーワードをつけることで、共有メモリを使えるようになる (FILENAME
キーワードは指定しない)。
;DOUBLE型10000個分の共有メモリセグメントを割り当てる
; segname はセグメントを区別する文字列でIDLのみで使われる。ここではIDLに自動的に選ばせている。
; handle はセグメントを区別する整数で、OSでも使われる。
IDL> SHMMAP, /DOUBLE, DIMENSION=10000, GET_NAME=segname, GET_OS_HANDLE=handle, /SYSV
; 共有メモリセグメントの情報を表示する。
; 参照数(Refcnt)は子プロセスやその他のプロセスによる参照は含まれない。
IDL> HELP, /SHARED_MEMORY
IDL_SHM_31051_0 DOUBLE = <SysV(229380), Offset(0), DestroyOnUnmap, Refcnt(0)> Array[10000]
; 配列としてアクセスできるようにする
IDL> z = SHMVAR(segname)
IDL> HELP, z
Z DOUBLE = SharedMemory<IDL_SHM_31051_0> Array[10000]
IDL> Z[0] = DINDGEN(10000)
; 子プロセスを生成する。
; 子プロセスの出力は一時ディレクトリ (Linux では /usr/tmp) の idloutput に書き出すようにしている。
IDL> outputfile = FILEPATH('idloutput', /TMP)
IDL> p = OBJ_NEW('IDL_IDLBridge', OUTPUT = outputfile)
% Loaded DLM: IDL_IDLBRIDGE.
; OS_HANDLE キーワードを指定することで、既存の共有メモリセグメントにアタッチできる。
IDL> p->Execute,"SHMMAP, /DOUBLE, DIMENSION=10000, GET_NAME=segname, OS_HANDLE=" + STRING(handle) + ", /SYSV"
IDL> p->Execute,"z = SHMVAR(segname)"
; 配列の各要素の和を計算する
IDL> p->Execute,"sum = TOTAL(z)"
IDL> PRINT, p->GetVar('sum')
49995000.
; 子プロセスを破棄する
IDL> p->Cleanup
このときシェルで現在の共有メモリセグメントの使用状況を調べてみる。2つのプロセスからアタッチされていることが分かる。
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 229380 nishida 777 80000 2
共有メモリセグメントを破棄するには SHMUNMAP
関数を使う。共有メモリセグメントを参照する全てのプロセスが終了した場合も自動的に破棄される。IDLプロセスが異常終了した場合には共有メモリセグメントがそのまま残ってしまうことがあるので、ipcrm コマンドで削除する。
; まず、変数 z からの参照を削除する。
IDL> p->Execute, 'z = 0'
IDL> z = 0
; 共有メモリセグメントを親・子両方で破棄する
IDL> p->Execute, 'SHMUNMAP, segname'
IDL> SHMUNMAP, segname
; もし、zの参照を削除せず SHMUNMAP を実行した場合、次の例のように共有メモリの状態が UnmapPending に変わる。
; この場合、z の参照が削除され、参照数(Refcnt)が0になった時点で、共有メモリセグメントも破棄される。
IDL> SHMUNMAP, segname
IDL> HELP, /SHARED_MEMORY
IDL_SHM_31051_0 DOUBLE = <SysV(229380), Offset(0), DestroyOnUnmap, UnmapPending, Refcnt(1)> Array[10000]
IDL> z = 0
IDL> HELP, /SHARED_MEMORY
IDL>
; 子プロセスを破棄する
IDL> p->Cleanup
RAMディスク形式の共有メモリ /dev/shm が使われる。利用できるシステムは限られているが、一般的なディストリビューションでは利用可であることが多い。RedHat ではデフォルトで物理メモリの半分が最大容量になっている。共有メモリを確保したままIDLがクラッシュした場合、/dev/shm/ でセグメント名を探して削除すればよい。WindowsではCreateFileMapping関数が呼び出される。
IDLでは、SHMMAP
関数はデフォルトで POSIX 共有メモリを使う (FILENAME
と /SYSV
キーワードの両方が指定されない場合)。使い方は SystemV 共有メモリの場合とほとんど同じ。異なるのは、SHMMAP
関数に /SYSV
キーワードをつけないことと、GET_OS_HANDLE
が返す値が文字列型になっている点。
メモリマップドファイルは、ファイルを仮想メモリ空間にマッピングすることにより、ファイルをメモリと同じ方法でアクセスできるようになる。IDLからはWRITEU/READU/POINT_LUN等を使うかわりに、配列を操作するのと同様に扱える。
ランダムアクセスの場合に速くて簡単・便利。複数プロセス間での共有メモリとしても利用可能。読み書きで実際に必要になったページ分のデータのみディスクから実メモリにロードされる(遅延ロード)ので、巨大ファイルの一部のみを読み書きするという場合にも利用可能。
SHMMAP
関数に FILENAME
キーワードを指定することでメモリマップドファイルを利用できる(FILENAME
キーワードを指定しない場合には、共有メモリが用いられる)。
; あらかじめファイルを作成しておく必要がある
; この方法(sparse file)は巨大なファイルを素早く作るのに便利だが、書き込んでいくと断片化を起こしやすい
IDL> filename = 'test'
IDL> openw, unit, filename, /get_lun
IDL> point_lun, unit, 8 * 1024ll^3 - 1
IDL> writeu, unit, 0b
IDL> free_lun, unit
; メモリマップドファイル
IDL> SHMMAP, /DOUBLE, DIMENSION=[1024,1024,1024], GET_NAME=segname, FILENAME=filename
; 配列としてアクセスできるようにする
IDL> z = SHMVAR(segname)
IDL> help,z
Z DOUBLE = SharedMemory<IDL_SHM_784_0> Array[1024, 1024, 1024]
IDL> help,/shared_memory
IDL_SHM_1081_0 DOUBLE = <MappedFile(test), Offset(0), Refcnt(1)> Array[1024, 1024, 1024]
; 配列としてファイルを読み書きできる
IDL> z[0,*,10] = dindgen(1024)
; 参照を外す (Refcntが0になる)
IDL> z=0
IDL> help,/shared_memory
IDL_SHM_1081_0 DOUBLE = <MappedFile(test), Offset(0), Refcnt(0)> Array[1024, 1024, 1024]
IDL> SHMUNMAP, segname
次の例のように、構造体の配列としてアクセスすることもできる。ただし、データ構造アライメントに注意。構造体のメンバの型に応じてパディングが挿入される。
; テスト用ファイル
IDL> filename = 'test2'
IDL> openw, unit, filename, /get_lun
IDL> writeu, unit, bindgen(256)
IDL> free_lun, unit
; 構造体を定義
IDL> template = {a:0b, b:0l, c:0, d:0ll, e:0b, f:bytarr(3, /nozero)}
IDL> print,n_tags(template, /data_length) ; 構造体の各メンバのサイズの合計は19バイトだが
19
IDL> print,n_tags(template, /length) ; メモリ上では32バイトを占める
32
; メモリマップドファイル
IDL> SHMMAP, TEMPLATE=template, DIMENSION=8, GET_NAME=segname, FILENAME=filename
IDL> z = SHMVAR(segname)
IDL> help,z
Z STRUCT = -> <Anonymous> SharedMemory<IDL_SHM_515_0> Array[8]
; A, B間に3バイト、C, D間に6バイト、末尾に4バイトのパディングが入る
IDL> help,z[0]
** Structure <1fb7a18>, 6 tags, length=32, data length=19, refs=4:
A BYTE 0
B LONG 117835012 ; = 0x07060504
C INT 2312 ; = 0x0908
D LONG64 1663540288323457296 ; = 0x1716151413121110
E BYTE 24 ; = 0x18
F BYTE Array[3]
IDL> print,format='(z0)',z[0].f
19
1a
1b
IDL> z=0
IDL> SHMUNMAP, segname
バイナリファイルから構造体を単純にREADU/WRITEUで読み書きする時、パディングは削除され詰められる。構造体メンバのアライメントを保ったままファイルを読み書きするには、メモリマップドファイルを使うとよい。
ASSOC 関数でもメモリマップドファイルと同様に、ASSOCも、IDLから配列の読み書きとしてファイルアクセスできるが、通常のIOを利用しているので、メモリマップドファイルのほうが速度の点で有利だと思われる(要検証)。ASSOCの利点として、gzipで圧縮されたファイルから直接読み込むこともできる。
共有メモリなどの資源を、複数のプロセスで共有する場合、競合を防ぐために排他制御が必要になる。このとき、IDLでは排他制御の手段としてセマフォ(semaphore)を用いることができる(実態はミューテックス(mutex))。
; セマフォを作成
IDL> status = SEM_CREATE('semaphore1')
; セマフォをロック (ロックに成功した場合1が返る、他のプロセスで既にロックされている場合はブロックは行われず直ちに0が返る)
IDL> status = SEM_LOCK('semaphore1')
; ロックされているセマフォを解放
IDL> SEM_RELEASE, 'semaphore1'
; 不要になったセマフォを削除
IDL> SEM_DELETE, 'semaphore1'
他のプロセスでロックされたセマフォが解放されるまで待つには次のようにする。
WHILE SEM_LOCK('semaphore1') EQ 0 DO WAIT, 0.01
例えば、以下の例では、共有メモリ上に確保した64ビット整数を、z[0]
という名前で複数プロセスで共有する。
IDL> SHMMAP, /L64, DIMENSION=1, GET_NAME=segname, GET_OS_HANDLE=handle, /SYSV
IDL> z = SHMVAR(segname)
IDL> HELP,z
Z LONG64 = SharedMemory<IDL_SHM_20553_0> Array[1]
; プロセスを4つ作成し、共有メモリを使えるようにする
IDL> p = OBJARR(4)
IDL> FOR i=0,3 DO BEGIN &$
IDL> p[i] = OBJ_NEW('IDL_IDLBridge') &$
IDL> p[i]->Execute,"SHMMAP, /L64, DIMENSION=1, GET_NAME=segname, OS_HANDLE=" + STRING(handle) + ", /SYSV" &$
IDL> p[i]->Execute,"z = SHMVAR(segname)" &$
IDL> ENDFOR
z[0]
に対して、4つのプロセスから同時に値の読み書きを行うと、結果は正しくない。これは、共有メモリから値を読み出し、その値に1を加え、共有メモリにその値を書き戻すという一連の動作の間に、他のプロセスが同じ共有メモリを読み書きしてしまうため。
IDL> z[0] = 0
IDL> FOR i=0,3 DO p[i]->Execute,"FOR i=1,10000000 DO z[0]++", /NOWAIT
; 全ての子プロセスの処理の終了を待つ
IDL> FOR i=0,3 DO WHILE p[i]->Status() EQ 1 DO WAIT, 0.01
IDL> PRINT, z[0]
11856225 ; 値は毎回異なる (正しくは 40000000 になるべき)
複数プロセスで同時に処理されるとまずい部分(クリティカルセッション)、つまりz[0]++
の直前でセマフォをロックし、クリティカルセションの直後でセマフォを解放する。
; セマフォを作成する
IDL> FOR i=0,3 DO p[i]->Execute,"status = SEM_CREATE('semaphore1')"
IDL> z[0] = 0
; クリティカルセッションをSEM_LOCK と SEM_RELEASE ではさむ
IDL> FOR i=0,3 DO p[i]->Execute,"FOR i=1,10000000 DO BEGIN & WHILE SEM_LOCK('semaphore1') EQ 0 DO WAIT, 0.01 & z[0]++ & SEM_RELEASE,'semaphore1' & ENDFOR", /NOWAIT
; 全ての子プロセスの処理の終了を待つ
IDL> FOR i=0,3 DO while p[i]->Status() eq 1 DO WAIT, 0.01
; セマフォを削除する
IDL> FOR i=0,3 DO p[i]->Execute,"SEM_DELETE,'semaphore1'"
IDL> PRINT, z[0]
40000000
最後に子プロセスを破棄するのを忘れずに。
IDL> FOR i=0,3 DO p[i]->Cleanup