メモリ管理

IDLで大規模データを扱う際には、メモリ管理が重要となる。物理メモリのサイズを超えるような巨大なデータを扱ったり、複数プロセスで並列処理を行いプロセス間でデータの交換・共有を行う必要が生じる。

共有メモリ

共有メモリは複数のプロセスから同時に読み書きできるメモリのことであり、複数のプロセス間でデータの交換・共有に使われる。IDL では特に IDL_IDLBridge 使用時に威力を発揮する。

Linuxでは共有メモリとして、System V (shmget) とPOSIX (shm_open)があるが、IDLでは両方の形式をサポートする。WindowsではCreateFileMapping関数が用いられる。

System V共有メモリを使う

Linuxの場合、/proc/sys/kernel/shmmax でプロセスごとの共有メモリの最大サイズを調べる。x86_64 環境ではデフォルトで十分なサイズになっているはず。

  1. $ cat /proc/sys/kernel/shmmax
  2. 68719476736

i386環境では小さすぎることがあるので、この場合はrootで次のようにして変更する。

  1. # sysctl -w kernel.shmmax=536870912
  2. kernel.shmmax = 536870912

IDLでSystem V共有メモリを操作する。SHMMAP 関数に /SYSV キーワードをつけることで、共有メモリを使えるようになる (FILENAME キーワードは指定しない)。

  1. ;DOUBLE型10000個分の共有メモリセグメントを割り当てる
  2. ; segname はセグメントを区別する文字列でIDLのみで使われる。ここではIDLに自動的に選ばせている。
  3. ; handle はセグメントを区別する整数で、OSでも使われる。
  4. IDL> SHMMAP, /DOUBLE, DIMENSION=10000, GET_NAME=segname, GET_OS_HANDLE=handle, /SYSV
  5. ; 共有メモリセグメントの情報を表示する。
  6. ; 参照数(Refcnt)は子プロセスやその他のプロセスによる参照は含まれない。
  7. IDL> HELP, /SHARED_MEMORY
  8. IDL_SHM_31051_0 DOUBLE = <SysV(229380), Offset(0), DestroyOnUnmap, Refcnt(0)> Array[10000]
  9. ; 配列としてアクセスできるようにする
  10. IDL> z = SHMVAR(segname)
  11. IDL> HELP, z
  12. Z DOUBLE = SharedMemory<IDL_SHM_31051_0> Array[10000]
  13. IDL> Z[0] = DINDGEN(10000)
  14. ; 子プロセスを生成する。
  15. ; 子プロセスの出力は一時ディレクトリ (Linux では /usr/tmp) の idloutput に書き出すようにしている。
  16. IDL> outputfile = FILEPATH('idloutput', /TMP)
  17. IDL> p = OBJ_NEW('IDL_IDLBridge', OUTPUT = outputfile)
  18. % Loaded DLM: IDL_IDLBRIDGE.
  19. ; OS_HANDLE キーワードを指定することで、既存の共有メモリセグメントにアタッチできる。
  20. IDL> p->Execute,"SHMMAP, /DOUBLE, DIMENSION=10000, GET_NAME=segname, OS_HANDLE=" + STRING(handle) + ", /SYSV"
  21. IDL> p->Execute,"z = SHMVAR(segname)"
  22. ; 配列の各要素の和を計算する
  23. IDL> p->Execute,"sum = TOTAL(z)"
  24. IDL> PRINT, p->GetVar('sum')
  25.        49995000.
  26. ; 子プロセスを破棄する
  27. IDL> p->Cleanup

このときシェルで現在の共有メモリセグメントの使用状況を調べてみる。2つのプロセスからアタッチされていることが分かる。

  1. $ ipcs -m
  2. ------ Shared Memory Segments --------
  3. key shmid owner perms bytes nattch status
  4. 0x00000000 229380 nishida 777 80000 2

共有メモリセグメントを破棄するには SHMUNMAP 関数を使う。共有メモリセグメントを参照する全てのプロセスが終了した場合も自動的に破棄される。IDLプロセスが異常終了した場合には共有メモリセグメントがそのまま残ってしまうことがあるので、ipcrm コマンドで削除する。

  1. ; まず、変数 z からの参照を削除する。
  2. IDL> p->Execute, 'z = 0'
  3. IDL> z = 0
  4. ; 共有メモリセグメントを親・子両方で破棄する
  5. IDL> p->Execute, 'SHMUNMAP, segname'
  6. IDL> SHMUNMAP, segname
  7. ; もし、zの参照を削除せず SHMUNMAP を実行した場合、次の例のように共有メモリの状態が UnmapPending に変わる。
  8. ; この場合、z の参照が削除され、参照数(Refcnt)が0になった時点で、共有メモリセグメントも破棄される。
  9. IDL> SHMUNMAP, segname
  10. IDL> HELP, /SHARED_MEMORY
  11. IDL_SHM_31051_0 DOUBLE = <SysV(229380), Offset(0), DestroyOnUnmap, UnmapPending, Refcnt(1)> Array[10000]
  12. IDL> z = 0
  13. IDL> HELP, /SHARED_MEMORY
  14. IDL>
  15. ; 子プロセスを破棄する
  16. IDL> p->Cleanup

POSIX共有メモリを使う

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 キーワードを指定しない場合には、共有メモリが用いられる)。

  1. ; あらかじめファイルを作成しておく必要がある
  2. ; この方法(sparse file)は巨大なファイルを素早く作るのに便利だが、書き込んでいくと断片化を起こしやすい
  3. IDL> filename = 'test'
  4. IDL> openw, unit, filename, /get_lun
  5. IDL> point_lun, unit, 8 * 1024ll^3 - 1
  6. IDL> writeu, unit, 0b
  7. IDL> free_lun, unit
  8. ; メモリマップドファイル
  9. IDL> SHMMAP, /DOUBLE, DIMENSION=[1024,1024,1024], GET_NAME=segname, FILENAME=filename
  10. ; 配列としてアクセスできるようにする
  11. IDL> z = SHMVAR(segname)
  12. IDL> help,z
  13. Z DOUBLE = SharedMemory<IDL_SHM_784_0> Array[1024, 1024, 1024]
  14. IDL> help,/shared_memory
  15. IDL_SHM_1081_0 DOUBLE = <MappedFile(test), Offset(0), Refcnt(1)> Array[1024, 1024, 1024]
  16. ; 配列としてファイルを読み書きできる
  17. IDL> z[0,*,10] = dindgen(1024)
  18. ; 参照を外す (Refcntが0になる)
  19. IDL> z=0
  20. IDL> help,/shared_memory
  21. IDL_SHM_1081_0 DOUBLE = <MappedFile(test), Offset(0), Refcnt(0)> Array[1024, 1024, 1024]
  22. IDL> SHMUNMAP, segname

次の例のように、構造体の配列としてアクセスすることもできる。ただし、データ構造アライメントに注意。構造体のメンバの型に応じてパディングが挿入される。

  1. ; テスト用ファイル
  2. IDL> filename = 'test2'
  3. IDL> openw, unit, filename, /get_lun
  4. IDL> writeu, unit, bindgen(256)
  5. IDL> free_lun, unit
  6. ; 構造体を定義
  7. IDL> template = {a:0b, b:0l, c:0, d:0ll, e:0b, f:bytarr(3, /nozero)}
  8. IDL> print,n_tags(template, /data_length) ; 構造体の各メンバのサイズの合計は19バイトだが
  9. 19
  10. IDL> print,n_tags(template, /length) ; メモリ上では32バイトを占める
  11. 32
  12. ; メモリマップドファイル
  13. IDL> SHMMAP, TEMPLATE=template, DIMENSION=8, GET_NAME=segname, FILENAME=filename
  14. IDL> z = SHMVAR(segname)
  15. IDL> help,z
  16. Z STRUCT = -> <Anonymous> SharedMemory<IDL_SHM_515_0> Array[8]
  17. ; A, B間に3バイト、C, D間に6バイト、末尾に4バイトのパディングが入る
  18. IDL> help,z[0]
  19. ** Structure <1fb7a18>, 6 tags, length=32, data length=19, refs=4:
  20. A BYTE 0
  21. B LONG 117835012 ; = 0x07060504
  22. C INT 2312 ; = 0x0908
  23. D LONG64 1663540288323457296 ; = 0x1716151413121110
  24. E BYTE 24 ; = 0x18
  25. F BYTE Array[3]
  26. IDL> print,format='(z0)',z[0].f
  27. 19
  28. 1a
  29. 1b
  30. IDL> z=0
  31. IDL> SHMUNMAP, segname

バイナリファイルから構造体を単純にREADU/WRITEUで読み書きする時、パディングは削除され詰められる。構造体メンバのアライメントを保ったままファイルを読み書きするには、メモリマップドファイルを使うとよい。

ASSOC 関数でもメモリマップドファイルと同様に、ASSOCも、IDLから配列の読み書きとしてファイルアクセスできるが、通常のIOを利用しているので、メモリマップドファイルのほうが速度の点で有利だと思われる(要検証)。ASSOCの利点として、gzipで圧縮されたファイルから直接読み込むこともできる。

セマフォを用いた排他制御

共有メモリなどの資源を、複数のプロセスで共有する場合、競合を防ぐために排他制御が必要になる。このとき、IDLでは排他制御の手段としてセマフォ(semaphore)を用いることができる(実態はミューテックス(mutex))。

  1. ; セマフォを作成
  2. IDL> status = SEM_CREATE('semaphore1')
  3. ; セマフォをロック (ロックに成功した場合1が返る、他のプロセスで既にロックされている場合はブロックは行われず直ちに0が返る)
  4. IDL> status = SEM_LOCK('semaphore1')
  5. ; ロックされているセマフォを解放
  6. IDL> SEM_RELEASE, 'semaphore1'
  7. ; 不要になったセマフォを削除
  8. IDL> SEM_DELETE, 'semaphore1'

他のプロセスでロックされたセマフォが解放されるまで待つには次のようにする。

  1. WHILE SEM_LOCK('semaphore1') EQ 0 DO WAIT, 0.01

例えば、以下の例では、共有メモリ上に確保した64ビット整数を、z[0]という名前で複数プロセスで共有する。

  1. IDL> SHMMAP, /L64, DIMENSION=1, GET_NAME=segname, GET_OS_HANDLE=handle, /SYSV
  2. IDL> z = SHMVAR(segname)
  3. IDL> HELP,z
  4. Z LONG64 = SharedMemory<IDL_SHM_20553_0> Array[1]
  5. ; プロセスを4つ作成し、共有メモリを使えるようにする
  6. IDL> p = OBJARR(4)
  7. IDL> FOR i=0,3 DO BEGIN &$
  8. IDL> p[i] = OBJ_NEW('IDL_IDLBridge') &$
  9. IDL> p[i]->Execute,"SHMMAP, /L64, DIMENSION=1, GET_NAME=segname, OS_HANDLE=" + STRING(handle) + ", /SYSV" &$
  10. IDL> p[i]->Execute,"z = SHMVAR(segname)" &$
  11. IDL> ENDFOR

z[0]に対して、4つのプロセスから同時に値の読み書きを行うと、結果は正しくない。これは、共有メモリから値を読み出し、その値に1を加え、共有メモリにその値を書き戻すという一連の動作の間に、他のプロセスが同じ共有メモリを読み書きしてしまうため。

  1. IDL> z[0] = 0
  2. IDL> FOR i=0,3 DO p[i]->Execute,"FOR i=1,10000000 DO z[0]++", /NOWAIT
  3. ; 全ての子プロセスの処理の終了を待つ
  4. IDL> FOR i=0,3 DO WHILE p[i]->Status() EQ 1 DO WAIT, 0.01
  5. IDL> PRINT, z[0]
  6. 11856225 ; 値は毎回異なる (正しくは 40000000 になるべき)

複数プロセスで同時に処理されるとまずい部分(クリティカルセッション)、つまりz[0]++の直前でセマフォをロックし、クリティカルセションの直後でセマフォを解放する。

  1. ; セマフォを作成する
  2. IDL> FOR i=0,3 DO p[i]->Execute,"status = SEM_CREATE('semaphore1')"
  3. IDL> z[0] = 0
  4. ; クリティカルセッションをSEM_LOCK と SEM_RELEASE ではさむ
  5. 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
  6. ; 全ての子プロセスの処理の終了を待つ
  7. IDL> FOR i=0,3 DO while p[i]->Status() eq 1 DO WAIT, 0.01
  8. ; セマフォを削除する
  9. IDL> FOR i=0,3 DO p[i]->Execute,"SEM_DELETE,'semaphore1'"
  10. IDL> PRINT, z[0]
  11. 40000000

最後に子プロセスを破棄するのを忘れずに。

  1. IDL> FOR i=0,3 DO p[i]->Cleanup
西田圭佑 (NISHIDA Keisuke)
nishida at kwasan.kyoto-u.ac.jp
$Id: memory.html,v 1.8 2021/01/20 09:41:48 nishida Exp nishida $