Mais conteúdo relacionado
Semelhante a 省メモリーに関するデザインパターン 2011.04.18 (20)
省メモリーに関するデザインパターン 2011.04.18
- 1. ULS Copyright © 2011 UL Systems, Inc. All rights reserved.
省メモリーに関するデザインパターン
書籍:『省メモリプログラミング』より
ウルシステムズ株式会社
http://www.ulsystems.co.jp
mailto:info@ulsystems.co.jp
Tel: 03-6220-1420 Fax: 03-6220-1402
2011年4月18日
講師役:近棟 稔
- 2. ULS Copyright © 2011 UL Systems, Inc. 1
今更なぜメモリーをケチるの?
背景
– iPhoneやAndroidのように小さなメモリーしか載っていない携帯端末が流行っ
ています。しかし、アプリケーションに対するユーザーの要望は高くなり続けてい
ます。
– サーバーサイドで扱うデータ量がユーザーのニーズによって巨大化しています。
しかし、特にクラウド系のサービス構築の場合、サーバーリソースを無尽蔵に使
ってしまうと消費電力やハードウエアコストが上がってしまいます。
携帯端末とデータセンターの面白い共通点
– 携帯端末もクラウド系サービスを支えるデータセンターも、「消費電力」がキーワ
ードになっています。
– 携帯端末もクラウド系サービスを支えるデータセンターも、扱いたいデータを単純
なリニア空間のメモリー上にすべて乗せることが難しくなっています。
書籍:『省メモリプログラミング』そのものについて
– 2000年11月に書かれた本ですが、高い効率を持つアプリケーションを構築する
ための基本的な考え方がうまくまとめられています。今後、システムの設計を行
う上でのアイディアの元になりそうな書籍でした。
- 3. ULS Copyright © 2011 UL Systems, Inc. 2
省メモリプログラミングパターン一覧
カテゴリー パターン 内容
small architecture
memory limit モジュール毎にquotaを設定する。(Androidの場合16MBメモリー上限あり)
small interfaces
Listではなくイテレータを関数の戻り値にするなどして、逐次アクセスAPIを活
用する。そうすることで、メモリー上に大きなデータを展開しなくても良くなる。
partial failure
メモリー不足に陥った際は、そのモジュールのみ失敗させ、サービスそのもの
は縮退モードで続行することで、ユーザビリティが向上する。
captain Oates
重要ではないモジュールは、メモリー不足状態を検知したら自らシャットダウン
するように作る。例:Androidではlow-memoryブロードキャスト。(Oatesは
南極探検隊の一人)
read-only memory 組み込み機器の場合、プログラムloadが必要ないROMを活用する。
hooks ROM内に収めたプログラムに対して後でパッチを当てられるようにしておく。
secondary strage
application switching アプリケーション分割をし、1つ1つのプログラムサイズを小さく抑える。
data files
データファイルを小さな複数ファイルに分割し、1つ1つのファイルサイズを小さ
くする。例:大規模データの部分ソート+マージソートの組み合わせ
resource files 画像などは必要なときにロード可能なように外部ファイルとする。
packages
プログラムを多数のモジュールに分割し、ダイナミックにロード、アンロードする
ようにする。windowsではdll、linuxではsoにして実現する。Javaでは普通。
paging OSの提供するスワップの機構を利用する。近代的なOSでは普通。
compression
table compression ハフマン符号化などを用いた圧縮を行う。
difference coding
差分コーディングやランレングスエンコーディングを使う。
(例:androidのKeyEventは複数のイベントが1つに「圧縮」される)
adaptive compression LZ圧縮やBzip2などを用いたデータ圧縮を行う。
説明するもの 説明しないもの
- 4. ULS Copyright © 2011 UL Systems, Inc. 3
省メモリプログラミングパターン一覧
カテゴリー パターン 内容
small data structures
packed data 小さな領域にデータを詰め込む。
sharing flyweightパターンと同等。複数場所でデータを共有。
copy-on-write
データの複製時には物理的なコピーをせず、変更時にはじめてコピーを
作成する方法。
embedded pointers
linked listのコンテナのように、構造を形成する部分に大きな領域を必
要とする状態を改善する方法。
multiple representations
内部表現を個々のオブジェクトに合った形に最適化し、情報を最小化し
ます。
memory allocations
fixed allocations
初期化時にすべてのメモリー領域の確保を済まし、処理中は新たなメモ
リー領域の確保を行わないようにする。
variable allocations mallocを使う。(組み込みではこれをサポートしていない場合もある)
memory discard
fixed allocationsパターンを部分適用し、mallocのオーバーヘッドを
最小化する。一括メモリー確保、一括メモリー開放を行う。
pooled allocation
固定サイズのオブジェクトプールからオブジェクトを貸し出すことによって、
高速なオブジェクトの生成・破棄を実現する。
compaction メモリー領域のデフラグを行う。
reference counting 参照カウント方式のガベージコレクション
garbage collection マーク&スイープやコピー方式のガベージコレクション
説明するもの 説明しないもの
- 6. ULS Copyright © 2011 UL Systems, Inc. 5
hooks
シチュエーション
– BIOSなど、ROMで提供されているルーチンを部分的に修正したり、処理の前後に自前の処理(前処理・後処理)を追加した
りしたくなる場合があります。
解決策
– 割り込みベクタテーブルのような物を作り、ROM内でサブルーチンコールする際や、プログラム内でサブルーチンコールす
る際にはそのベクタテーブル経由で呼び出すようなアーキテクチャにします。
採用例
– ハードウエア割り込みやソフトウエア割り込みで一般的に使われています。
– Javaなどのオブジェクト指向言語における継承によるメソッドオーバーライドはベクタテーブルの一種と考えられます。
– 関数型言語のように関数がファーストクラスである言語ではすべての関数がベクタテーブルに乗っているとも考えられます。
別名
– ベクタテーブル、ジャンプテーブル、パッチテーブル、割り込みベクタテーブル
サブルーチン1
サブルーチン2
サブルーチン3
サブルーチン4
ROM内ベクタテーブル(RAM内)
機能番号 関数ポインタ
0x01 (デフォルトではROM内の処理を指す)
0x02 (デフォルトではROM内の処理を指す)
0x03 RAM内の処理に変更されている
0x04 (デフォルトではROM内の処理を指す)
利用者はベクタテーブル越しに
各種サブルーチン呼び出しをする
(ルーチンの呼び先はROM内では
ないかもしれない)
サブルーチン3'
RAM内
- 8. ULS Copyright © 2011 UL Systems, Inc. 7
data files
シチュエーション
– 処理対象のファイルが大きすぎて処理が出来ない事があります。
解決策
– 処理対象のファイルを処理可能な大きさに分割して処理することでうまくいくことがあります。たとえば、数
テラバイトのファイルのソート処理などです。
採用例
– UNIXのsortコマンドには上記のような分割ソートをサポートするためのコマンドラインオプションが存在し
ます。
– GoogleのBigtableや。HadoopのHDFSはこのような考え方に立脚しています。
(大きなデータは分割したまま保持)
– gccなどのコンパイラは通常個別ソースコードをコンパイル後、リンカによって最後に1つのプログラムモジ
ュールに統合されます。
数テラバイトの
データ
数メガバイトのファイル
数メガバイトのファイル
数メガバイトのファイル
数メガバイトのファイル
・
・
・
ソート済み部分ファイル
ソート済み部分ファイル
ソート済み部分ファイル
ソート済み部分ファイル
・
・
・
ソート
ソート
ソート
ソート
数テラバイトの
ソート済み
データ
マージ
ソートの
マージ
フェーズ
- 10. ULS Copyright © 2011 UL Systems, Inc. 9
difference coding
シチュエーション
– 変化の少ない大量のデータを扱う場合、簡単なロジックで圧縮が可能な場合があります。
解決策
– 「差分コーディング」や「ランレングス圧縮」を用います。
採用例
– キー入力においてキーリピートが発生した場合、大量のKeyEventが発生します。このようなオブジェクトをシステム内で大
量にnewしてしまうと、オブジェクトの生成・破棄だけでも大変な負荷になってしまいます。
通常、このような事を想定し、キーリピートは「ランレングス圧縮」します。
– 例:Androidの場合
KeyEvent
keyCode:int
repeatCount:int
KeyEvent
keyCode:int
KeyEvent
keyCode:int
KeyEvent
keyCode:int
・
・
・ 1つにまとめる
- 12. ULS Copyright © 2011 UL Systems, Inc. 11
packed data
シチュエーション
– ランダムアクセス性能は確保した状態で、限られたメモリーの中で可能な限り大量の情報を
扱いたい場合に使用します。
– プロジェクトでの経験
1千万件(10M件)のデータに対する高速なランダムアクセスを実現するために、Javaのメモ
リー中に情報を保持することになりました。しかし、Bean1つあたりのデータサイズが1Kバイ
トあると10GBのメモリー空間が必要になってしまいました。
解決策
– 対象の情報を十分格納可能な最小のデータ構造を考えます。(圧縮に関してはpacked
dataの範囲外です)
手法
– 情報を表現する際に、使用する型などを工夫することによって、同じ情報を効率的に表現する
方法を考えます。
– packed dataではデータ圧縮までは考えません。圧縮までしてしまうと、高速なBeanの読み
書きが出来なくなってしまうためです。
- 13. ULS Copyright © 2011 UL Systems, Inc. 12
packed dataの例:Pixelクラス (Androidのピクセル表現について)
以下にPixelクラスの各種バリエーションを示します。Androidは(D)方式です。
(A) 最も無駄に作ったPixelクラス
public class Pixel {
Byte alpha;
Byte red;
Byte green;
Byte blue;
}
Pixelオブジェクトそのもの=8バイト
フィールドの4つのポインタ=8バイト * 4=32バイト
(フィールド値はByte.valueOf()でキャッシュ可能)
計:40バイト!
(B) 32bit色を出せれば十分と考えた場合
public class Pixel {
byte alpha;
byte red;
byte green;
byte blue;
}
Pixelオブジェクトそのもの=8バイト
byteもint幅で確保される(!)ため=4バイト * 4=16バイト
計:24バイト!
(D) クラスも不要とすると
int argb;
計:4バイト!
(C) int幅が最小幅なのだから、そこに詰め込み
public class Pixel {
int argb;
}
Pixelオブジェクトそのもの=8バイト
argbフィールド=4バイトではなく、8バイトでした。というのも
フィールド個数が奇数の場合、偶数フィールドまで確保され
てしまうからです。
計:16バイト!
最初のサイズの10分の1のサイズになりました!
10Mピクセルの写真なら1枚40MBのメモリーに入ります。
元のデータ構造では400MBも必要でした。
ピクセルをintで表現する方法はAndroidで採用されています。
1倍4倍
6倍10倍
オブジェクトのサイズを知る方法
java.exe -agentlib:hprof=heap=sites pixel.Main
- 14. ULS Copyright © 2011 UL Systems, Inc. 13
sharing
シチュエーション
– true,falseなど、同一データをがたくさん登場する事が見込まれる場合、それらのデータを別
オブジェクトにするのはもったいない場合があります。
解決策
– flyweightパターンを使って不変オブジェクトを作り、それを色々な場所で参照することでトー
タルのメモリー消費を抑えます。
sharingの例
– Javaでは基本型のラッパークラスの持つvalueOfメソッドが
sharingを促進するために用意されています。
– Javaを含めた各種言語には、文字列に関して
internという機構があり、これを使えばsharing可能です。
– immutableなオブジェクトをシステム全体でsharing
する方法はパフォーマンスを稼ぐ際に一般的な方法です。
プリミティブ型 キャッシュが使われる範
囲
boolean true, false
byte 全範囲
char 0 ~127の範囲
short -128 ~ 127の範囲
int -128 ~ 127の範囲
long -128 ~ 127の範囲
float キャッシュなし
double キャッシュなし
- 15. ULS Copyright © 2011 UL Systems, Inc. 14
sharingにおけるオブジェクト間の接続イメージ
sharingするということは、以下のようなイメージとなります。share対象のオブジェクトは
immutableであった方が安全です。
:Bean
num:Integer
name:String
flag:Boolean
:Bean
num:Integer
name:String
flag:Boolean
:Bean
num:Integer
name:String
flag:Boolean
:Bean
num:Integer
name:String
flag:Boolean
:Bean
num:Integer
name:String
flag:Boolean
:Bean
num:Integer
name:String
flag:Boolean
true:Boolean
false:Boolean
abc:String
def:String
xyz:String
0:Integer
1:Integer
2:Integer
sharingされたオブジェクト
- 16. ULS Copyright © 2011 UL Systems, Inc. 15
copy on write
シチュエーション
– sharingによって複数スレッドなどから共有されている大規模データを、個別スレッド上で自由に編集
したい。しかし、個別スレッドに元データのすべてをコピーしてくるのは非効率である。
– (組込み系で)ROMデータの内容を一部編集したい。
解決策
– コピー時
物理的なコピーはせずに、コピー元のデータをそのままコピー先でsharingします。
– 編集時
コピー元とデータをsharingしたままだと編集結果が元データへ反映されてしまうため、編集時にはじめて物
理的なコピーをし、それに対する編集を行います。「書き込み時(on write)」にはじめて「コピー(copy)」す
るということでcopy on writeといいます。
copy-on-writeの採用例
– Subversionはsharingとcopy-on-writeをうまく活用した例です。Subversion上のファイルコピーは、ど
んな大規模データでも一瞬で完了します。この理由は、ファイルのコピー時には実際のファイルの複製を行
っておらず、元のデータを指し示す小さな情報を保存しているだけだからです。コピー後の情報を書き換える
(チェックインする)場合、そこではじめて変更が生じた部分のコピーを作成して変更内容を保存します。この
ような仕組みによって、Subversion上のデータは多くの部分がsharingされたままとなり、ディスクスペー
スを圧迫しにくい仕組みになっています。
– Linuxカーネルをはじめとする各種OSは、プロセスのfork時にcopy-on-writeを使っています。プロセス
がforkした瞬間、実際にプロセスが分裂したように見えますが、プロセス空間のコピーを実際に行なってい
るわけではありません。また、この場合親プロセスのキャッシュが子プロセス側でも有効なため、キャッシュヒ
ット率を上げる効果もあります。
- 17. ULS Copyright © 2011 UL Systems, Inc. 16
copy on writeの挙動サンプル
ツリー構造をcopy on writeによって操作します。この例はproxyによるcowの実現例です。
1
2
4
3
5 6
システム全体でsharingされているデータ 元オブジェクトをproxyオブジェクトで包みます
コピー
操作
部分的に変更を施します
1
2
4
3
5 6
1
7
4
3
5 8
編集
copy on write
元のオブジェクトのまま
新オブジェクト
元のオブジェクトのまま
オブジェクトツリーを操作している人からすると、
システム全体の共有データをコピーしたものは、
本当にコピーしたものに見えているし、
そのコピーデータの編集操作は自然なものに感じられる。
- 18. ULS Copyright © 2011 UL Systems, Inc. 17
embedded pointers
シチュエーション
– コレクションクラスの構築時、linked listのような構造はデータそのものよりもlist構造を形成するノードその
ものの方が領域を必要とする場合があります。その結果、「入れるものより容器のほうが大きい」といった状
況になってしまいます。しかし、ArrayListや配列を使ってしまうとデータ列の任意の場所への追加・削除コ
ストが大きくなってしまいます。
解決策
– たとえばBeanを格納するlinked listであれば、listのnodeをデータであるBeanとは別に作ることをやめ、
格納対象のBeanそのものの中に次のデータを指し示すポインタを埋め込みます(下図)。
List
first
last
Node
next
value
Node
next
value
Node
next
value
Bean Bean Bean
普通の
linked list
ここだけで大量のメモリーを消費します。
1ノードの表現はオブジェクトが8バイト、
ポインタ2つで2*8=16バイトで
1ノードだけで計24バイトも消費します。
プロジェクトで経験した1千万件(10M件)の
データであれば、ここだけで240MBも必要です。
List
first
last
Bean
next
Bean
next
Bean
next
embedded
pointers
Beanの中にポインタを埋め込むことで、
linked listを構築するための増分は
ポインタだけで済み、8バイトのみ(1/3)で良くな
ります。
24
バイト
8
バイト
- 19. ULS Copyright © 2011 UL Systems, Inc. 18
embedded pointersの実際の設計
Javaでembedded pointersを実現する方法を以下に示します。
解説
– 各Nodeはインターフェースとして定義し、実際の「次のノード」を示すリンク情報は各Beanに埋め込みま
す。
– ここでの「next()」メソッドはこのListの仕組みの予約語になってしまうため、Bean側にnext()メソッドが
存在した場合は名前が重なってしまいます。よって、実際にはnext()のような重なりやすい名前ではなく、
もう少し重なりにくい長い名前にするか、「_next_()」のように、通常の命名規約から外れる名前にするこ
とが望ましいです。
List
first
last
Bean
value
next()
Node
next() Javaのインターフェース
first
last
0..1
- 20. ULS Copyright © 2011 UL Systems, Inc. 19
embedded pointersの他の例 (ポインタ差分による双方向リスト)
双方向リストを実現するにはnextとprevの2つのポインタを個々のノードに持つ必要があり、非
常に空間効率が悪くなってしまう。これを解決するための方法として、「ポインタ差分」という方法
がある。
List
first
last
a:Node
next
prev
通常の
双方向リスト
last
first
ポインタ差分を
使った
双方向リスト
b:Node
next
prev
c:Node
next
prev
a:Node
diff=d-b
last
first
b:Node
diff=a-c
c:Node
diff=b-d
List
first
last
d:Node
diff=c-a
右方向への操作における計算方法
a=既知
d=既知
b=d-(d-b)
c=a-(a-c)
左方向への操作における計算方法
a=既知
d=既知
c=(c-a)+a
b=(b-d)+d
d:Node
next
prev
※ ポインタ差分では、隣り合った2つのノードが分かっていれば、
その2つのノード情報を元に前後にリストをたどることが出来る。
- 21. ULS Copyright © 2011 UL Systems, Inc. 20
multiple representations
シチュエーション
– 同一のAPIを持つ実装が、パフォーマンス特性などの差によって複数存在する場合があります。たとえば
ListにおけるEmptyListとArrayListとLinkedListのように、場合によって選択可能なものがあります。
解決策
– (Javaの場合はListの例のように当然のように考えるものなので、パターンとして考える必要はないくらい
のものです)
応用例
– Beanを1千万件ほど扱い、それらのBeanが「null状態」である場合が多い場合、「null専用Bean」を作る
ことでデータ量を少なくすることが出来ます。
Bean
a()
b()
StdBean
a:int
b:String
a()
b()
NullBean
a()
b()
フィールドを持たないことで、データを
減らすことが可能。
さらにimmutableオブジェクトであるこ
とから、システム全体でsharing可能。
Java標準APIのEmptyListもこの考
え方に沿っている。
- 23. ULS Copyright © 2011 UL Systems, Inc. 22
pooled allocation
シチュエーション
– オブジェクトのnew(malloc)をランタイムに行うとパフォーマンスに影響があるが、アプリケーションの特
性としてオブジェクトの動的生成・破棄が必要な場合。(アクションゲームなど非常にシビアなレスポンスが
求められる場合や、組み込みシステムで遅延が許されない場合が典型例です)
解決策
– 固定数のオブジェクトプールを作り、システム初期化時にすべての必要なオブジェクトをメモリー上に展開
し、このオブジェクトプールからオブジェクトを借りる・返却するといった操作でオブジェクトを利用します。
応用例
– アクションゲームなどで、オブジェクトプールを作る場合。
(1画面に登場する最大キャラクター数までプールしておきます。)
自キャラ
敵キャラA
敵キャラB
敵キャラC
ボスキャラA
ボスキャラB
ボスキャラC
○
○ ○ ○ ○ ○
○ ○ ○ ○ ○ ○ ○ ○
○ ○ ○ ○ ○ ○
○ ○
○
○
オブジェクトプール
システム機同時に全部ロード
(グラフィックスなど含む)
借りる
返す