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