SBCL GENCGC @ x86 Linux

1 はじめに : SBCL の二つの GC

SBCL には GC が二つある。

1.1 Stop and Copy GC

stop and copy GC based on Cheney's algorithm

いわゆる「コピーGC」を行う。ヒープを二分割し、 それらの間でオブジェクトを移動させることで GC する。

関連ファイル:

1.2 GENCGC

GENerational Conservative Garbage Collector for SBCL

かつて、 x86 アーキテクチャのために、 「世代別かつ保守的な GC」として実装されたために、この名前がある。 現在は、 PowerPC などにも移植されている。 アーキテクチャによっては、(名前に反して) Precise な動作をする場合もあるらしい。

関連ファイル:

1.3 どちらが使われるか

CPU アーキテクチャ や OS によって使用できる GC が決まっている。 Makefile からは、 LISP_FEATURE_GENCGC という定数を参照すれば、 どちらが使われているかが分かる。

ざっと見たところ、以下のようになっているようだ。

  • x86 と x86-64 では、 GENCGC しか使用できない。
  • SPARC と PowerPC では、 OS によっては Stop and Copy GC と GENCGC を選択できる。
  • MIPS, HP-PA, Alpha では、 Stop and Copy GC しか使用できない。

ここでは、 x86 Linux での GENCGC について追ってみる。

2 GENCGC の主要な構造体

2.1 struct page (gencgc-internal.h)

GENCGC では、動的なメモリ割り当てのために使用するメモリ空間を (x86 の場合) 4KB ごとに分割して管理している。 この管理単位を ページ と呼んでおり、各ページ毎の情報を struct page に格納している。

以下のような情報を含む:

bytes_used
使用中のバイト数
allocated
未使用のページか、またはどんなオブジェクトのために使用されているか。
gen
そのページの属する世代

allocated では、そのページはどのようなオブジェクトに使用されているか ということについて、以下のように分類して表現する:

free
空のページ
boxed data
ポインタを含む Lisp オブジェクトに使用
unboxed data
ポインタを含まない Lisp オブジェクトに使用。
code
コンパイル済みコードを含むオブジェクト。 boxed data と同様、 ポインタ類を含むものとして扱われる。 (また、 x86 ではコードの移動時に特殊な処理が必要なため、 その判定にも使われているようだ)
open region
後述の alloc_region から参照されているかどうかを示す。 このページから、近い将来にメモリ割り当てが行われることとなる。 回収 (Scavenge) の対象とならない。

任意のアドレス値について対応する struct page を探すには、 そのアドレス値に対して簡単な計算をするだけでよいように設計されている。 (find_page_index() :: gencgc.c)

2.2 struct generation (gencgc.c)

GENCGC での「世代」は、ページの集合として表現されている。そして、 struct generation は、各世代の情報を持つ構造体である。

以下のような情報を含む:

alloc_start_page
次の割り当ての時に使用するページ
alloc_unboxed_start_page
次の unboxed data 割り当ての時に使用するページ
bytes_allocated
この世代から使用されたバイト数
gc_trigger
GC を起動する閾値となるバイト数。 bytes_allocated がこの値を越えると、 GC が起動される。
bytes_consed_between_gc
GC を起動するまでに割り当てられるバイト数。 ユーザの設定で変更可能な値で、この値を参考に gc_trigger が算出される。
num_gc
最後に raise してから走った GC 回数
number_of_gcs_before_promotion
次の世代へとオブジェクトを raise するまでに走る GC 回数。 初期値は 1 (詳細は後述)

GENCGC は第 0 世代から第 5 世代までを使用する。 (GENCGC の内部的には、第 6 世代を一時的に使用している)。

GENCGC では、ページが属する世代を引くのは簡単だが、 世代からそれに属するページを探す方法は (割り当ての高速化のためのフィールドを除いて) ない。 ほとんどの GC 処理は、各ページが持つ世代情報を調べて実行される。

2.3 struct alloc_region (gencgc-alloc-region.h)

メモリ割り当ての時に用いる構造である。割り当てを高速に行い、 またその結果をページに反映するための情報を含んでいる。

以下のような情報を含む:

free_pointer
その alloc_region で使用可能なメモリ領域の先頭を指すポインタ。
end_addr
その alloc_region で使用可能なメモリ領域の末尾を指すポインタ。
first_page
その alloc_region が対象としている最初のページ。
last_page
その alloc_region が対象としている最後のページ。
start_addr
その alloc_region を初期化した時の、最初の free_pointer の値。

3 詳しい動作

3.1 初期化

3.1.1 struct page の配列を作成する。 (gc_init() :: gencgc.c)

動的なメモリ割り当てに使用する予定の全メモリ空間について struct page の配列を生成する。

GENCGC は、起動時に、動的なメモリ割り当てに使用する予定の全メモリ空間について struct page を生成する (これは、 calloc(3) で確保される)。 動的なメモリ割り当てに使用する空間の大きさは、起動時のオプション --dynamic-space-size で指定する。起動後は、この大きさを変更する方法は (おそらく) ない。

3.1.2 回収処理 (Scavenge) 用の関数配列を作る。 (gc_init_tables() :: gencgc.c)

回収処理 (Scavenge) の際には、各種 Lisp オブジェクトの構造に応じて、 大きさを算出したり、含まれるポインタを見分けたり、別のページへと移動させたりする。

このために、各種のオブジェクトに対応する処理を、 C の関数ポインタの配列として引けるようにしている。 この配列は、 GENCGC 内部の静的なメモリ領域にあり、この時点で初期化される。

3.1.3 generationalloc_region を初期化する。 (gc_init() :: gencgc.c)

各世代に対応する generation と、オブジェクトごとの alloc_region を初期化する。

3.1.4 OS にメモリを要求する。 (validate() :: validate.c)

動的なメモリ割り当てのために使用する領域を、 OS に一括で要求する。 Unix 系の OS では、 mmap(2) を使う。

3.2 メモリの割り当て

3.2.1 割り当て要求を受ける (alloc())

GENCGC にメモリの割り当てを要求するインターフェイスが alloc() である。 この関数は、第 0 世代からの boxed data の割り当てを試みる。

第 0 世代と boxed data に対応する alloc_region を引数に、 general_alloc_internal() を呼ぶ。

3.2.2 高速の割り当てを試す (general_alloc_internal())

まず、対象 alloc_region の残りの空き領域を確認する。

空き領域が十分ある場合、ポインタの移動だけで割り当ては完了する。 free_pointerend_pointer を調べて、 空き領域(end_pointer - free_pointer) が十分ある場合、 new_pointer の指す領域を返し、 new_pointer を要求されたバイト数だけ移動するだけである。

一方、対象 alloc_region に空きがない場合、まず動的空間全体に空きがない場合は、 GC を起動する。その後、 gc_alloc_with_region() を呼ぶ。

3.2.3 低速な割り当て処理を開始 (gc_alloc_with_region())

ここに来た時点で、 alloc_region には空きがない。ここでは、 alloc_region がより空きのある空間を指すように更新することで、 空き領域の確保を試みる。

この関数は、以下のループである。

  1. alloc_region に要求されたバイト数だけの空き領域があれば、それを返して終了する。
  2. alloc_region には要求された空きがないので、この alloc_region を解放する (gc_alloc_update_page_tables())。
  3. 要求された大きさの空き領域を持つ alloc_region を作成する (gc_alloc_new_region())。
  4. 1. に戻る。

3.2.4 struct page 配列の更新と alloc_region の解放 (gc_alloc_update_page_tables())

alloc_region が管理しているページを alloc_region から切り離し、 alloc_region が行った変更を struct page 配列に反映させる。

以下のことを行う:

  1. alloc_region の持つ各ページの open region 指定を外す。これにより、 そのページは回収処理 (Scavenge) の対象となるようになる。
  2. alloc_region の持つ各ページの使用量を、 bytes_used に反映させる。
  3. 全体の割り当て量や、使用した世代からの割り当て量を更新する。
  4. alloc_region を空にする。

3.2.5 alloc_region の再割り当て (gc_alloc_new_region())

alloc_region に、要求された大きさの空き領域を持つ新しいページ群を与える。

以下のことを行う:

  1. 要求された大きさの空きがあるページを見つける (gc_find_freeish_pages())。この時、末尾に十分な空きを持ったページと、 存在すればそれに続く複数の空きページが見つかる。
  2. 見つかったページ群を、 alloc_region に指定する。
  3. 見つかったページ群に、 open region 指定をする。このページは、 回収処理 (Scavenge) の対象とならなくなる。(struct pageallocated に、 open region と指定される)。
  4. 見つかったページを 0 で埋めて初期化する。

3.3 GC の起動と終了

3.3.1 GC 起動の契機

GC の起動は、以下の二つの場合に起こる:

  1. メモリ割り当て時に、 GC 起動の閾値を越えた場合。GENCGC では、上記の alloc() の先で起こる。
  2. ユーザが Lisp 関数 SB-EXT:GC を呼んだ場合

(ソースコードのコメントには、 3 つめの場合として 「 *NEED-TO-COLLECT-GARBAGE*T のときに WITHOUT-GCING から抜けた場合」 という状況が記述されているが、これはバージョン 0.7 以前のみである。 *NEED-TO-COLLECT-GARBAGE* という変数も、もはや定義されていない。)

GENCGC から GC が呼ばれる経路は、シグナル処理なども絡んでいて、 かなり複雑である。おおよそ、以下のようになっている。

  1. alloc() (gencgc.c)

    メモリ割り当て開始を開始する。

  2. general_alloc_internal() (gencgc.c)

    メモリが足らない場合、 GC 要求を上げる。

    この GC 要求を上げる処理が、 SBCL のイベントハンドリング構造を使っているので、 かなり難解になっている。 (psendo-atomic セクションの中で、 シグナルを受信したかのように psendo-atomic-interrupted を設定する、 という処理のようだが、詳細は調査中)

  3. interrupt_handle_pending() (interrupt.c)

    上記の GC 要求から、ここに処理が飛ぶ。この関数で、要求の内容が GC の要求だと分かった場合、 maybe_gc() を呼ぶ。

  4. maybe_gc() (gc-common.c)

    Lisp 関数 SUB-GC, POST-GC を呼ぶ。

3.3.2 GC の起動処理 (SUB-GC)

GC の起動処理は、 Lisp 関数 SUB-GC である。以下のようなことをする:

  1. 割り込みを止める。 (without-interrupts)
  2. 他の箇所からの GC 起動を止める。 (without-gcing)
  3. ロックを取り、確実に GC が動いてない状態にする。
    ((sb!thread:with-mutex (*already-in-gc* :wait-p nil) ...))
    この時、 GC が既に稼働中だった場合、若い世代向けの GC 要求なら、 何もしないで終了する。全世代への GC 要求なら、再度ロックの取得を試みる。
  4. ポインタに誤認されそうなものや、キャッシュを整理する。 (unsafe-clear-roots)
    • 制御スタックの未使用領域を 0 埋めする。(scrub-control-stack)
    • 多倍長整数プリンタで使用されるキャッシュを整理する。 (scrub-power-cache)
    • 各種のキャッシュを空にする。(drop-all-hash-caches)
      ここで言うキャッシュとは、 SBCL で内部的に使用されている hash cache というものである。 SBCL では、型情報を判定する関数にこれを使用している (early-extensions.lisp の define-hash-cache)
  5. 他の全スレッドを停止する。 (gc-stop-the-world)
    • 全スレッド停止のためのロック (all_threads_lock) を取る。
    • 他のスレッドに (Unix の) シグナル (SIG_STOP_FOR_GC, Linux では SIGUSR2#define されている) を送る。
    • スレッドの終了か、スレッドの停止を待つ。
  6. GC する (perform-gc)。
    C 関数 collect_garbage() を起動する。この内容は後述する。
    終了したら、全スレッドを再開させる。 (gc-start-the-world)
    • 各スレッドを、動作中の状態に変更する。
    • 全スレッド停止のためのロック (all_threads_lock) を解放する。

3.3.3 GENCGC の起動 (collect_garbage())

GENCGC の GC 起動処理は、 C 関数の collect_garbage() である。

この関数は、一番若い世代から、指定された世代までの GC 処理 (garbage_collect_generation(); 後述) を起動する。 普通は、最も若い世代(nursery) のみの GC を試みるよう指定されるが、 メモリ不足の時には全世代に対しての (full) GC が指定される場合もある。 また、ユーザが Lisp 関数 SB-EXT:GC を呼ぶ場合には、任意の世代数を指定することも出来る。

終了後、動的領域の大きさを示す変数や、 GC の閾値を調節する。

3.3.4 GC の終了処理 (POST-GC)

GC の終了処理は、 Lisp 関数 POST-GC である。以下のようなことをする:

  1. 各オブジェクトの後始末処理を呼ぶ。 (run-pending-finalizers)
  2. GC 終了時フック関数 (*after-gc-hooks*) を呼ぶ。

3.4 個々のオブジェクトの回収方法

世代の内容を回収する garbage_collect_generation() の前に、 その中で個々のオブジェクトをどのように回収しているかを書く。

3.4.1 Scavenge (scavenge())

GENCGE でのごみ集めは、 from_space (新しい世代) から、 new_space (より古い世代) へとオブジェクトを移動させることで行なわれる。 このオブジェクトを移動させることを、 Scavenge と呼んでいる。

Scavenge の方法は、対象の Lisp オブジェクト毎に異なり、型ごとに対応する 処理を呼び出す (初期化の際に作られる、 Scavenge 用の関数配列を引く)。

例えば、 cons の scavenge は以下のように行う (scav_list_pointer):

  1. new_space に、新しい cons を割り当て、元の cons の car と cdr をコピーする。この時、その car と cdr が指す先のオブジェクトまではコピーせず、 ポインタだけをコピーする。 (trans_list())
  2. 元の cons を指していたポインタを、新しい cons を指すように置きかえる。
  3. 元の from_space にあった cons を、 forward pointer に置きかえる。 この forward pointer とは、オブジェクトが移動されたことと、 移動先の新たなオブジェクトを示すのに使用される構造である。
    他のポインタがこの古い cons を指していた場合、 cons オブジェクトの代わりにこの forwarding pointer が見つかることとなる。 この場合の Scavenge 処理は、そのポインタを forward pointer の示す移動先へと置きかえるだけとなる。

cons は、その car と cdr に他のオブジェクトへのポインタを含んでいるが、 それらの指す先は移動しない。もちろん最終的には、 cons に含まれるポインタも書きかえられるのだが、それは各世代の GC の際に行われる。 このことは、 vector や structure などにも同様である。

3.4.2 Raise

Raise , すなわちオブジェクトを別の世代へと移動させる処理のほとんどは、 上の Scavange に含まれている。 Raise する場合は、 Scavenge でのオブジェクトの移動先が raise 先の世代になる。 Raise しない場合は、オブジェクトを一旦 GENCGC の内部的な世代である SCRATCH_GENERATION に移動し、 その後で SCRATCH_GENERATION に含まれたページを元の世代へと書き戻す。

Raise するかどうかは、先の collect_garbage() で判定される。 Raise の基準は、以下である:

  • GC 対象の世代が最も古い世代だった場合は、 raise 先がないので raise しない。
  • GC 対象の世代が GC された回数が、閾値を上まわった場合は raise する。 これはデフォルトで一回であり、 raise されるごとに 0 に戻される。
  • あまりにメモリが足りないと判定された場合は raise する (collect_garbage() での more フラグ)。

また、 raise した場合は、 raise 先の世代にも scavenge を試みる。

3.5 各世代の GC (garbage_collect_generation())

ここからは、各世代の GC を行う garbage_collect_generation() を追う。

3.5.1 C 言語レベルのスタックから指されたページの保存

まず、 各スレッドが持っているスタックで使用されている Lisp オブジェクトを保存する。 CPU アーキテクチャ依存の方法で、各スレッドのスタック領域を割り出す。 そして、そこに含まれるポインタから指されているページに、 全て印(dont_move フラグ)をつける (preserve_pointer())。 ここで印がつけられたページに含まれているオブジェクトは、 Scavenge の対象にならず、 そのまま new_space へと移動される (move_pinned_pages_to_newspace())。

ここで、 スタック領域として何を使用するかによって、 GENCGC の conservative さが決まる。 x86 系では、 CPU のもつスタックポインタを取って各スレッドの C 言語レベルのスタック領域を調査する。 このために、スタックに含まれる値が(真に何らかの領域を指す)ポインタなのかどうか判定できないので、 かなり conservative な動作となる。 他の CPU アーキテクチャでは、SBCL のスレッド構造体にスタックの情報が含まれており、 これは SBCL の管理下にあるため、 x86 より precise になっている。

3.5.2 GC のルートから指されているものを scavenge

GC のルートとして、以下のものを順に Scavenge していく:

  1. 割り込みハンドラに設定された Lisp オブジェクトから指されており、 from_space に含まれているもの
  2. binding スタックが保持している Lisp オブジェクトから指されていて、 from_space に含まれているもの
  3. static space 中の Lisp オブジェクトから指されていて、 from_space に含まれているもの。
    この static space とは、 SBCL 独自の仕組みで、 GC の対象にならないオブジェクトが格納されている領域である。 SB-EXT:PURIFY 関数により、 オブジェクトを static space に移動させることができる。

この時点で new_space には、 GC のルートから直接指されていたオブジェクトが移動されている。 それらの中から指されているオブジェクトは移動されていない。

3.5.3 GC 対象外の世代から指されているものを scavenge

次に、 new_space 以外の from_space より古い世代から指されているオブジェクトのうち、 from_space に含まれているものを、 new_space へと Scavenge する (scavenge_generations())。

この時点で、 new_space には、より古い世代から直接指されているオブジェク トが移動されたこととなる。まだ、それらの中から指されているオブジェクト は移動されていない。

3.5.4 new_space を scavenge

最後に、 new_space の内容を scavenge する (scavenge_newspace_generation())。ここでついに、 new_space の中から指 されているオブジェクトが移動される。

以下のようなことを行う:

  1. new_space 中のオブジェクトから指されているものを、 new_space へと scavenge する。このとき、 new_area という構造体を使って、 new_space に新たに割り当てられた領域を記録しておく。
  2. new_area を確認し、 new_space に新たに割り当てられたオブジェクトが あれば、その領域をまた scavenge する。ここでも、 new_area で新たに 割り当てられた領域を記録する。これを、新たな割り当てがなくなるまで (new_area に割り当てが記録されなくなるまで) 繰り返す。

一回の scavenge により、 scavenge した領域から指されている領域が、新た に割り当てられて new_space へと移動される。この割り当てられた領域を scavenge すれば、さらにそこから指された領域を new_space へと移動できる。

この「新たに割り当てられた領域を scavenge する」ことを繰り返すことによ り、各オブジェクトが持つポインタを再帰的に辿って、それの指す領域を移動 させることと同様の効果が得られる。新たな割り当てがなくなったということ は、オブジェクトの持つポインタを辿りきったということであり、つまり拾う べきオブジェクトは全て拾ったということである。

3.5.5 from_space を解放 (free_oldspace())

ここまでで、回収すべきオブジェクトは全て new_space へと移動されたので、 この時点で from_space に含まれるメモリは、全てごみと見なされる。

from_space に含まれるページを、全て未使用 (allocatedfree にする)とすることで解放する。

4 その他の機能

4.1 large object

大きなオブジェクトは、 割り当てにも GC にも負荷になるため、専用の処理が行われている。

一番異なるのは、 Scavenge 処理である。小さなオブジェクトの場合は、 それをコピーして new_space のページに詰めこむのが効率がいいが、 大きなオブジェクトはコピー自体が大きな負荷となる。そのため、コピーするのではなく、 そのオブジェクトが含まれるページ自体をそのまま new_space の所属に変えてしまう、 という処理をしている (general_copy_large_object())。

これを助けるため、大きなオブジェクト (ページサイズの4倍以上) の割り当ての時には、 大きなオブジェクト専用の alloc_region を用意して、 そこから割り当てるようになっている (gc_alloc_large())。

4.2 weak pointer や weak hash table

weak pointer や weak hash table は、それが指すオブジェクトが GC の対象 となることを妨げないようなポインタやハッシュテーブルである。この実装も GENCGC で用意されている。

各世代の GC を行う (garbage_collect_generation()) 時に、これらが 指す先は scavenge の対象とならない。 Scavenge が終わった後に、全ての weak pointer を確認し、それが生き残って移動されていれば移動先を指すようにし、 死んでいれば死んだと印を付けることで実装されている (scan_weak_pointers())

4.3 generation age

GENCGC では、 オブジェクトの raise をするかどうかの基準に、 age というものも使用している。これは、各世代の持つ二つの変数で指定される。

cum_sum_bytes_allocated
前回に GC が走って以後、 割り当てられたバイト数。新しく割り当てられたオブジェクトが多ければ多いほど、 その世代は age が大きいと見なされる。
minimum_age_before_gc
age がこの値より小さい場合、 raise しない。

世代の age が小さいということは、 その世代の全体に占める若い世代のオブジェクトが少ないということである。 このような時には GC をしない方が効率上の利点がある場合があるので、 これらの変数で抑制している。

この age については、 SBCL からも以下の関数で参照できる。

  • sb-ext:generation-average-age
  • sb-ext:generation-minimum-age-before-gc

5 GENCGC 実装で気づいたこと

5.1 OS とのメモリのやりとり

GENCGC では、起動時に、動的なメモリ割り当てのために使用する領域を全部確保する。

Linux では、 mmap(2) を呼ぶことで実装されている。この時に、 OS に対して「プロセス空間の拡張は要求するが、スワップ空間は予約しない」 ようにフラグを付けている (MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE) 。 このため、この時点では、プロセスの仮想メモリ空間は大きくなる一方、 実メモリとの関連付けはされないので、実メモリを使い果たすようなことはあまり起こらない。 SBCL のプロセス情報を ps コマンドで見ると、 VSZ (仮想メモリ空間の大きさ) は大きいが、 RSS (実メモリ空間の大きさ) はそれほど大きくないのが確認できる。

一方で、このような mmap をすることは、実際のメモリアクセス時には常に、 実メモリの不足が起こる可能性もはらんでいる。 GENCGC では、その通知を扱う(SIGSEGV シグナルを拾う)ように実装されている。

また、特に大量のメモリを解放した場合に、それを OS に返す実装もされている。 OS に返したいページを一旦 munmap(2) し、その後 mmap(2) し直す。 これにより、 OS 側のページテーブルから、実メモリとの関連付けがなくなるため、 実メモリを解放する効果が得られる。

5.2 メモリ保護機能の利用

GENCGC では、 Scavenge でオブジェクトを移動させる際に、 効率化のためにいくつかフラグ (write_protected) を使用している。

ここで、特定のビルドオプション (SC_NS_GEN_CK) を有効にすると、 このフラグに反する書き込みが行われていないかどうかを検証するコードが有効になる。

この実装は、対象のページを mprotect(2) でページを「書き込み禁止」 に設定しておくことで、そのページへの不正な書き込みを OS によって保護違反として報告させる、というものである。

5.3 Linux での起動

Linux での x86 と x86-64 において、 SBCL の起動 (os_init()) は、 とても奇妙な構造になっている。 これは、 Linux の address randomization という機能を無効にするためである。

Linux の address randomization とは、 プロセス毎にメモリ空間上のスタックやヒープの場所をずらす機能である。 これにより、メモリ空間上の特定のアドレスを狙った攻撃がやりにくくなるため、 セキュリティの向上が見こめる。

GENCGC は、スタック領域の場所を判定するために、メモリ空間のアドレス値を (SBCL 自身の) コンパイル時に定数として組み込んでいる。しかし、 Linux の addresses randomization が有効だと、 これらのアドレス値は全く意味をなさなくなってしまうので、この機能を無効にする必要がある。

実際の処理は、次のようになっている:

  1. addresses randomization が有効かどうかを確認する。これには、 Linux 固有のシステムコール (personality(2)) を用いる。
  2. addresses randomization が有効だった場合、それを無効にするよう設定する。 (同じく、 personality(2) を使用)
  3. 固定アドレスで再起動するため、自分自身をもう一度起動する。 SBCL 自身のバイナリファイルを探して、それを exec(2) する。

5.4 マルチスレッド対応

マルチスレッド環境下での GENCGC は、概ね以下のようになっている。

  • メモリ割り当ては、個々のスレッドで実行される。上記の alloc_region は、 スレッドごとに存在している。
  • GC の際には、 GC を行うスレッド以外の全てのスレッドは停止される。 GC 中は、完全にシングルスレッドである。

GENCGC の実装は、 SBCL の psendo-atomic や、 futex といった仕組みに依存していて、これらも興味深い。 特に futex は、これは Linux 固有の同期化機構なのだが、 GC 上の利点があるということで、 わざわざ FreeBSD 向けの独自実装を SBCL で用意して使っていたりしている。

6 References

Author: 數理システム知識工學部

Date: 2014-05-07T12:57+0900

Generated by Org version 7.9.3f with Emacs version 24

© Mathematical Systems Inc. 2014