Anemic Domain Model

Anemic Domain Model という用語を初めて知った。DDDではこれに対して Rich Domain Model という用語もあり、Domain Model は rich であるべきという主張。

anemic は 「貧血(症)の、無気力な、元気のない」と言った意味。

Anemic Domain Model はというと、getter, setter ぐらいでで単なる入れ物にすぎないようなモデリング。DDD から見ると anti-pattern. Domain model を扱ったことのない Data (rdb?) 畑の人が陥りがち、また EJB Entity Beans などは Anemic Domain Model を推奨(logic を Session Beans に任せているので)。

そうじゃなくて POJO based の方が DDD でいいでしょう(~= Springとか?) という事のようだ。

G1 write barrier code generation in OpenJDK/HotSpot

OpenJDK/HotSpot での G1 Write Barrier の実装について確認してみました。

世代別 GC の Young に対する GC で root set の他、 Old から Young への参照も検査する必要があるが、すべてやると時間がかかるので、変更があったところを覚えておくため putfield の時にそれを記録しておく。これを write barrier というが、card table や remembered set など実装はいろいろ。すべてを記録すれば簡単が、それなりにリソースも必要になるため、512 bytes の範囲毎に変更があったか否かだけを記録しておき、必要な時はその範囲を詳しく調べるといった card marking にすると putfield 時のオーバーヘッドは小さくなる。

Old が単一の連続領域だとこれでもよいが、G1 の様に連続領域が比較的小さい場合には話が異なってくる。G1 region ではそれに入ってくる参照を覚えておく様にしているので、write barrier では region から関連するデータ構造をたどって記録するので、連続領域の Old に対する card marking とは処理が異なってくる。

putfield 自身はインタプリタでも compiled code でも行われる処理だが、それを担当する関数があって呼び出されているという訳ではない。シンプルな造りではそれでもいいが、それなりのオーバーヘッドが伴うので、速くするための工夫がある。

Interpreter

HotSpot のインタープリタには2種類あり、一つは cpp で書かれた cpp interpreter (CC_INTERP の ifdef で条件分けしてある) で、移植の初期の段階で使われる。もう一つは template interpreter で、CPU アーキテクチャごとに存在する (share/vm/interpreter/template*, cpu/*/vm/templateInter*)。template interpreter は起動時に動的に macro assembler で生成される。

share/vm/interpreter/templateTable.cpp:

void TemplateTable::initialize() {
  // :
  def(Bytecodes::_putfield            , ubcp|____|clvm|____, vtos, vtos, putfield            , f2_byte      );
  
  //:
void TemplateTable::def(Bytecodes::Code code, int flags, TosState in, TosState out, void (*gen)(), char filler) {
  assert(filler == ' ', "just checkin'");
  def(code, flags, in, out, (Template::generator)gen, 0);
}

五番目の putfield が実際にコードを生成する関数の名前で、x86_64 での実体は次のもの。

cpu/x86/vm/templateTable_x86_64.cpp:

#define __ _masm->
  // ..
void TemplateTable::putfield(int byte_no) {
  putfield_or_static(byte_no, false);
}
void TemplateTable::putfield_or_static(int byte_no, bool is_static) {
  transition(vtos, vtos);
  // :
  Label notVolatile, Done;
  __ movl(rdx, flags);
  __ shrl(rdx, ConstantPoolCacheEntry::is_volatile_shift);
  __ andl(rdx, 0x1);
  // :
  // atos
  {
    __ pop(atos);
    if (!is_static) pop_and_check_object(obj);
    // Store into the field
    do_oop_store(_masm, field, rax, _bs->kind(), false);
    if (!is_static) {
      patch_bytecode(Bytecodes::_fast_aputfield, bc, rbx, true, byte_no);
    }
    __ jmp(Done);
  }
  // ..

putfield_or_static() を見ると movl や shrl など x64 のニーモニックが見て取れる。 write barrier のコード生成は putfield_or_static() から呼び出される do_oop_store() で行われている。

余談だが、atos, vtos というシンボルがある。template interpreter を構成する個々のバイトコードに対する命令列と付随する情報を Codelet と呼ぶが、これらは入力と出力の Top of State (enum TopState)というものを持つ。これはスタックのトップにあるデータのタイプを表しており、atos なら object (への参照) で、itos なら int といった具合。ある codelet の out の tos が次の codelet の in の tos と合致していると stack にその値をセーブ(push)せずに、レジスタ渡しができる仕組み。できることなら4つぐらい register に持たせるともっと速くなりそうだが、top の状態を管理するだけでも結構なコード量になっているので難しいだろう。データ型、サイズも考えると面倒だろう。

do_oop_store() に戻る。

static void do_oop_store(InterpreterMacroAssembler* _masm,
                         Address obj,
                         Register val,
                         BarrierSet::Name barrier,
                         bool precise) {
  assert(val == noreg || val == rax, "parameter is just for looks");
  switch (barrier) {
#if INCLUDE_ALL_GCS
    case BarrierSet::G1SATBCT:
    case BarrierSet::G1SATBCTLogging:
      {
        // flatten object address if needed
        if (obj.index() == noreg && obj.disp() == 0) {
          if (obj.base() != rdx) {
            __ movq(rdx, obj.base());
          }
        } else {
          __ leaq(rdx, obj);
        }
        __ g1_write_barrier_pre(rdx /* obj */,
                                rbx /* pre_val */,
                                r15_thread /* thread */,
                                r8  /* tmp */,
                                val != noreg /* tosca_live */,
                                false /* expand_call */);
    //..
    case BarrierSet::CardTableModRef:
    case BarrierSet::CardTableExtension:
      {
        if (val == noreg) {
          __ store_heap_oop_null(obj);
        } else {
          __ store_heap_oop(obj, val);
          // flatten object address if needed
          if (!precise || (obj.index() == noreg && obj.disp() == 0)) {
            __ store_check(obj.base());
          } else {
            __ leaq(rdx, obj);
            __ store_check(rdx);
          }
        }
      }
      break;
    case BarrierSet::ModRef:
    case BarrierSet::Other:

さらに下請け関数があるが、BarrierSet::Name barrier の引数で渡される (write) barrier のタイプによって処理が分けれている。この関数が VM 起動時に呼ばれる前に、heap のタイプ (Serial, Parallel, ParallelOld, CMS, G1) と barrier set のタイプは確定している。この型により、生成される write barrier 命令が異なってくる。template interpreter を新しいアーキテクチャに移植する人はこれも考慮しないといけないということだ。(ひょっとすると CC_INTERP 側の CPP で書かれたものを利用できるかもしれないが未確認)

Server Compiler

プログラムの実行が進み、しきい値を超えたコードは HotSpot Compile される。C2 Server Compiler は share/vm/opto 以下にある。メソッドのインライン展開の範囲を決めたりした後、コンパイル範囲のバイトコードが順次パースされる。Parse::do_one_bytecode() は bytecode の番号を使った大きな switch case 文を持つ。

putfield の場合は do_putfield() が呼ばれる。

share/vm/opto/parse2.cpp:

void Parse::do_one_bytecode() {
  //..
  switch (bc()) {
  case Bytecodes::_nop:
    // do nothing
    break;
  case Bytecodes::_lconst_0:
    push_pair(longcon(0));
    break;
  // ..
  case Bytecodes::_putfield:
    do_putfield();
    break;

do_putfiled() は do_field_access() を呼び出す。

share/vm/opto/parse.hpp:

  void do_putfield () { do_field_access(false, true); }
  void do_field_access(bool is_get, bool is_field);

do_field_access() は処理 (get, put) に応じて do_get_xxx, do_put_xxx を呼び出す。

share/vm/opto/parse3.cpp:

void Parse::do_field_access(bool is_get, bool is_field) {
  //...
    if (is_get) {
      (void) pop();  // pop receiver before getting
      do_get_xxx(obj, field, is_field);
    } else {
      do_put_xxx(obj, field, is_field);
      (void) pop();  // pop receiver after putting
    }
  //...
void Parse::do_put_xxx(Node* obj, ciField* field, bool is_field) {
  //..
  Node* store;
  if (bt == T_OBJECT) {
    const TypeOopPtr* field_type;
    if (!field->type()->is_loaded()) {
      field_type = TypeInstPtr::BOTTOM;
    } else {
      field_type = TypeOopPtr::make_from_klass(field->type()->as_klass());
    }
    store = store_oop_to_object( control(), obj, adr, adr_type, val, field_type, bt);
  } else {
    store = store_to_memory( control(), adr, val, bt, adr_type, is_vol );
  }

do_put_xxx は store_oop_to_object() で実際のストアの処理の中間コード(IR) を生成する。これは store_oop() を呼び出す。

share/vm/opto/graphKit.hpp:

  Node* store_oop_to_object(Node* ctl,
                            Node* obj,   // containing obj
                            Node* adr,  // actual adress to store val at
                            const TypePtr* adr_type,
                            Node* val,
                            const TypeOopPtr* val_type,
                            BasicType bt) {
    return store_oop(ctl, obj, adr, adr_type, val, val_type, bt, false);
  } 

store_oop() は実際の oop (Ordinary Object Pointer, object/array などへの参照(ポインタ)) をメモリに保存する中間コードを store_to_memory() で生成するが、それは pre_barrier と post_barrier で挟まれている。

pre_barrier, post_barrier は Barrier Set の型により switch case で処理を分けている。

share/vm/opto/graphKit.cpp:

Node* GraphKit::store_oop(Node* ctl,
                          Node* obj,
                          Node* adr,
                          const TypePtr* adr_type,
                          Node* val,
                          const TypeOopPtr* val_type,
                          BasicType bt,
                          bool use_precise) {
  // Transformation of a value which could be NULL pointer (CastPP #NULL)
  // could be delayed during Parse (for example, in adjust_map_after_if()).
  // Execute transformation here to avoid barrier generation in such case.
  if (_gvn.type(val) == TypePtr::NULL_PTR)
    val = _gvn.makecon(TypePtr::NULL_PTR);

  set_control(ctl);
  if (stopped()) return top(); // Dead path ?

  assert(bt == T_OBJECT, "sanity");
  assert(val != NULL, "not dead path");
  uint adr_idx = C->get_alias_index(adr_type);
  assert(adr_idx != Compile::AliasIdxTop, "use other store_to_memory factory" );

  pre_barrier(true /* do_load */,
              control(), obj, adr, adr_idx, val, val_type,
              NULL /* pre_val */,
              bt);

  Node* store = store_to_memory(control(), adr, val, bt, adr_idx);
  post_barrier(control(), store, obj, adr, adr_idx, val, bt, use_precise);
  return store;
}

void GraphKit::pre_barrier(bool do_load,
                           Node* ctl,
                           Node* obj,
                           Node* adr,
                           uint  adr_idx,
                           Node* val,
                           const TypeOopPtr* val_type,
                           Node* pre_val,
                           BasicType bt) {

  BarrierSet* bs = Universe::heap()->barrier_set();
  set_control(ctl);
  switch (bs->kind()) {
    case BarrierSet::G1SATBCT:
    case BarrierSet::G1SATBCTLogging:
      g1_write_barrier_pre(do_load, obj, adr, adr_idx, val, val_type, pre_val, bt);
      break;

    case BarrierSet::CardTableModRef:
    case BarrierSet::CardTableExtension:
    case BarrierSet::ModRef:
      break;

    case BarrierSet::Other:
    default      :
      ShouldNotReachHere();

  }
}

g1_write_barrier_pre, post は G1 の write barrier のデータ構造 SATB を参照、変更する中間コードを生成する。C2 の中間コードは Ideal と呼ばれたりするが SSA ベースで、制御構造をデータフローに変換した、明示的な block の無い高度な最適化を狙ったもの。パース時にも正規化、最適化し、さらにグローバルに GVN, Escape Analysis, immediate dominator を利用した loop 変換などを施した後、低レベルの IR に変換して、さらに最適化を加える。いろいろな変換が伴うので、この write barrier の関数が生成した node, edge の構成と実際のコードは並びが違うかもしれない(意味的にはあっていないといけないが)。

share/vm/opto/graphKit.cpp:

// G1 pre/post barriers
void GraphKit::g1_write_barrier_pre(bool do_load,
                                    Node* obj,
                                    Node* adr,
                                    uint alias_idx,
                                    Node* val,
                                    const TypeOopPtr* val_type,
                                    Node* pre_val,
                                    BasicType bt) {
  //..
  IdealKit ideal(this, true);

  Node* tls = __ thread(); // ThreadLocalStorage

  Node* no_ctrl = NULL;
  Node* no_base = __ top();
  Node* zero = __ ConI(0);

  float likely  = PROB_LIKELY(0.999);
  float unlikely  = PROB_UNLIKELY(0.999);

  BasicType active_type = in_bytes(PtrQueue::byte_width_of_active()) == 4 ? T_INT : T_BYTE;
  assert(in_bytes(PtrQueue::byte_width_of_active()) == 4 || in_bytes(PtrQueue::byte_width_of_active()) == 1, "flag width");

  // Offsets into the thread
  const int marking_offset = in_bytes(JavaThread::satb_mark_queue_offset() +  // 648
                                          PtrQueue::byte_offset_of_active());
  const int index_offset   = in_bytes(JavaThread::satb_mark_queue_offset() +  // 656
                                          PtrQueue::byte_offset_of_index());
  const int buffer_offset  = in_bytes(JavaThread::satb_mark_queue_offset() +  // 652
                                          PtrQueue::byte_offset_of_buf());

  // Now the actual pointers into the thread
  Node* marking_adr = __ AddP(no_base, tls, __ ConX(marking_offset));
  Node* buffer_adr  = __ AddP(no_base, tls, __ ConX(buffer_offset));
  Node* index_adr   = __ AddP(no_base, tls, __ ConX(index_offset));

  // Now some of the values
  Node* marking = __ load(__ ctrl(), marking_adr, TypeInt::INT, active_type, Compile::AliasIdxRaw);

  // if (!marking)
  __ if_then(marking, BoolTest::ne, zero); {
    Node* index   = __ load(__ ctrl(), index_adr, TypeInt::INT, T_INT, Compile::AliasIdxRaw);

    if (do_load) {
      // load original value
      // alias_idx correct??
      pre_val = __ load(no_ctrl, adr, val_type, bt, alias_idx);
    }

    // if (pre_val != NULL)
    __ if_then(pre_val, BoolTest::ne, null()); {
      Node* buffer  = __ load(__ ctrl(), buffer_adr, TypeRawPtr::NOTNULL, T_ADDRESS, Compile::AliasIdxRaw);

      // is the queue for this thread full?
      __ if_then(index, BoolTest::ne, zero, likely); {

        // decrement the index
        Node* next_index = __ SubI(index,  __ ConI(sizeof(intptr_t)));
        Node* next_indexX = next_index;
  //..

ここまでくると、オーバーヘッドヘッドの見積もりも大変だ。

OpenJDK build on CentOS6 and perm gen removal

ちょっと前に入れた CentOS で OpenJDK のビルドを確認。
mercurial 1.4-3.el6 と cups development environment のパッケージを加えただけで行けた。

(cd to work dir)
$ hg clone http://hg.openjdk.java.net/jdk7u/jdk7u-dev
$ cd jdk7u-dev/

get_source.sh を使えば fclone 拡張は必要ない。

$ sh get_source.sh 
 # Repos:  corba jaxp jaxws langtools jdk hotspot 
 Starting on corba
 Starting on jaxp
 :

あとは LANG=C にして、make するだけ。

$ LANG=C
$ make ALLOW_DOWNLOADS=true ALT_BOOTDIR=/usr/java/jdk1.7.0_04 

build 中に使用する bootstrap JVM には /usr/java/jdk1.7.0_04 に Oracle/Sun JDK がインストールしてあったので、そこを ALT_BOOTDIE にセットしておく。ALLOW_DOWNLOADS は jaxp などビルド中にダウンロードが必要なときは自動的にやっていいよという指定。

PermGen をなくして、native heap を使うようにする 6964458 で最近(といっても9月1日か)大きな変更があった。これで Perm はなくなったらしい。

Changeset: da91efe96a93
Author:    coleenp
Date:      2012-09-01 13:25 -0400
URL:       http://hg.openjdk.java.net/hsx/hsx25/hotspot/rev/da91efe96a93

6964458: Reimplement class meta-data storage to use native memory
Summary: Remove PermGen, allocate meta-data in metaspace linked to class loaders, rewrite GC walking, rewrite and rename metadata to be C++ classes
...

! agent/doc/clhsdb.html
! agent/src/os/bsd/ps_core.c
! agent/src/os/linux/ps_core.c
! agent/src/os/solaris/proc/saproc.cpp
! agent/src/share/classes/sun/jvm/hotspot/CommandProcessor.java
! agent/src/share/classes/sun/jvm/hotspot/HSDB.java
! agent/src/share/classes/sun/jvm/hotspot/HotSpotTypeDataBase.java
 - agent/src/share/classes/sun/jvm/hotspot/ci/ciArrayKlassKlass.java
 + agent/src/share/classes/sun/jvm/hotspot/ci/ciBaseObject.java
:

JDK8 は既にそうなってるかと推測してやってみたが、まだのようだった。9/17 日の時点で jdk8 のブランチの hotspot のバージョンは hs24 だった。

$ hg clone http://hg.openjdk.java.net/jdk8/jdk8

6964458 のチェックインログから hsx25 に入っているので、hsx25 をビルドしてみる。

$ hg clone http://hg.openjdk.java.net/hsx/hsx25/hotspot/
$ cd hotspot/make
$ make ALLOW_DOWNLOADS=true ALT_BOOTDIR=/usr/java/jdk1.7.0_04 LP64=1

hotspot/make でビルドすると 32/64, product/fastdebug 両方を作ろうとする。使用した環境では 32-bit 様のヘッダが不十分なようなので、LP64=1 で 64-bit に限定。

なお、clone もとを http://hg.openjdk.java.net/hsx/hsx25 にすると、tool & libraries も落ちてきて、なおかつビルドできなかった。

出来上がった libjvm.so を上で作っておいた jdk8 の jdk-image の中にあるものと置き換える。
そうして、heap generation の一覧を出すような操作 (-XX:+PringGCDetails をつけておいて Ctrl-\) すると generation の部分の出力は次のようになっていた。

Heap
 def new generation   total 7488K, used 407K [0x00000000e7600000, 0x00000000e7e20000, 0x00000000ef8a0000)
  eden space 6656K,   6% used [0x00000000e7600000, 0x00000000e7665df8, 0x00000000e7c80000)
  from space 832K,   0% used [0x00000000e7c80000, 0x00000000e7c80000, 0x00000000e7d50000)
  to   space 832K,   0% used [0x00000000e7d50000, 0x00000000e7d50000, 0x00000000e7e20000)
 tenured generation   total 16704K, used 0K [0x00000000ef8a0000, 0x00000000f08f0000, 0x00000000ffe00000)
   the space 16704K,   0% used [0x00000000ef8a0000, 0x00000000ef8a0000, 0x00000000ef8a0200, 0x00000000f08f0000)
 Metaspace total 3400K, used 2851K, reserved 8192K
  data space     3076K, used 2588K, reserved 6144K
  class space    324K, used 263K, reserved 2048K

Perm の代わりに Metaspace として表示されている。不具合はそれなりに出るかもしれないけど、これで Perm Gen 削除の主な作業は一段落したのかな。

Playing with OSX OpenJDK

4月頃の jdk7u-dev に fastdebug が作ってあったので、少しデバッグを入れてみる。昔のオブジェクトがあればビルドも早いだろう。

次のようなスクリプトを用意しておく。make に fastdebug_build を渡して後は、環境変数

_JAVA_OPTIONS=-Dfile.encoding=ASCII
export _JAVA_OPTIONS
make fastdebug_build ALLOW_DOWNLOADS=true SA_APPLE_BOOT_JAVA=true ALWAYS_PASS_TEST_GAMMA=true ALT_BOOTDIR=`/usr/libexec/java_home -v 1.6` HOTSPOT_BUILD_JOBS=`sysctl -n hw.ncpu`

これを実行する事で、5分ぐらいで出来上がった。 hotspot の部分は10数秒でできていて、なぜか触っていない jdk 部分に大半の時間がかかっている。

ビルドの最後の出力

#-- Build times ----------
Target fastdebug_build
Start 2012-08-08 02:10:43
End   2012-08-08 02:14:58
00:00:08 corba
00:00:14 hotspot
00:00:03 jaxp
00:00:05 jaxws
00:03:39 jdk
00:00:05 langtools
00:04:15 TOTAL
                                                • -

hotspot/make の下で、target を fastdebug にするともっと速いかもと思ったけれど、全てをコンパイルしに行ってる様で、時間がかかりそうだったので、トップでのビルドに戻る。

一見ビルドできているようだが build/macosx-amd64-fastdebug/bin/java を実行しても、変更が反映されていないように見える。find で JVM の実体である libjvm.dylib を探すとふるい物が結構見つかる。

結局、これら全てを削除してからビルドしないと、なぜか少し前の libjvm が動いたりする。この辺は最近のコードではなおってるのかもしれないが、今回は試さなかった。

Tarjan 作られる過程を見てみたかったのだけど、-XX:+PrintDominators は完成した後でないと使えないので、別途関数を作った。debug build では Resoure Area (NTarjan などが取られるところ)は ZapResourceArea のため 0xABABABAB で埋まっているので、Node pointer はそのパターンに関してもチェックが必要。

変更したのは domgraph.cpp .

#ifdef _LP64
static bool NotANode(const Node* n) {
  if (n == NULL)                return true;
  if (((intptr_t)n & 1) != 0)   return true;
  if (n == (Node*)0xababababababababL) return true;
}
#else
static bool NotANode(const Node* n) {
  if (n == NULL)                return true;
  if (((intptr_t)n & 1) != 0)   return true;
  if (n == (Node*)0xabababab)          return true;
}

#endif // _LP64

void dumpTarjanDfsorder(NTarjan* ntj, uint* dfsorder, int size)
{
  tty->print("D: NTarjan dfsorder\n");
  for (int i = 0; i < size; i++) {
    NTarjan* tp = &ntj[i];
    tty->print("%3d: %#lx ", i, tp);
    if (!NotANode(tp->_control)) {
      tty->print("ctrl %#lx ", tp->_control);
      tp->_control->dump();
    }
    tty->print("\n");
    tty->print("  par %#lx lbl %#lx anc %#lx chld %#lx dom %#lx bkt %#lx domcld %#lx domn %#lx\n",
        tp->_parent, tp->_label, tp->_ancestor, tp->_child,
        tp->_dom, tp->_bucket, tp->_dom_child, tp->_dom_next);
    tty->print("  semi %d size %d dfsorder %d\n", tp->_semi, tp->_size, dfsorder[i]);
  }
}

あと、gdb がうまく動いてくれない。これも新しい物があるのかもしれない…

$ gdb /Library/Java/JavaVirtualMachines/1.7.0.jdk/Contents/Home/bin/java /cores/core.60921 
GNU gdb 6.3.50-20050815 (Apple version gdb-1752) (Sat Jan 28 03:02:46 UTC 2012)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin"...
Reading symbols for shared libraries ........ done

Reading symbols for shared libraries . done
Reading symbols for shared libraries .unable to load symbol file: [memory object "/System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa" at 0x7fff97c9b000]: Undefined error: 0
.unable to load symbol file: [memory object "/System/Library/Frameworks/Security.framework/Versions/A/Security" at 0x7fff98cb1000]: Undefined error: 0
.unable to load symbol file: [memory object "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices" at 0x7fff8fbf1000]: Undefined error: 0
.unable to load symbol file: [memory object "/usr/lib/libz.1.2.5.dylib" at 0x7fff99fd2000]: Undefined error: 0
.unable to load symbol file: [memory object "/usr/lib/libSystem.B.dylib" at 0x7fff92294000]: Undefined error: 0
.unable to load symbol file: [memory object "/usr/lib/libobjc.A.dylib" at 0x7fff90291000]: Undefined error: 0
.unable to load symbol file: [memory object "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation" at 0x7fff9ae1d000]: Undefined error: 0
.unable to load symbol file: [memory object "/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit" at 0x7fff903b6000]: Undefined error: 0
:
.unable to load symbol file: [memory object "/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/OpenScripting.framework/Versions/A/OpenScripting" at 0x7fff95f2d000]: Undefined error: 0
.unable to load symbol file: [memory object "/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/Print.framework/Versions/A/Print" at 0x7fff8ef37000]: Undefined error: 0
.unable to load symbol file: [memory object "/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/SecurityHI.framework/Versions/A/SecurityHI" at 0x7fff99fca000]: Undefined error: 0
. done
#0  0x00007fff950eece2 in ?? ()
(gdb) where
#0  0x00007fff950eece2 in ?? ()
#1  0x00007fff95f8ca7a in ?? ()
#2  0x000000010ea3641b in os::abort ()
#3  0x000000010eb2458e in VMError::report_and_die ()
#4  0x000000010ea37af5 in JVM_handle_bsd_signal ()
#5  0x00007fff95fedcfa in ?? ()
#6  0x00007fff935b5570 in ?? ()
#7  0x00007fff9ae4d50c in ?? ()
#8  0x00007fff9ae55c74 in ?? ()
#9  0x00007fff9ae55486 in ?? ()
#10 0x000000010e49c4bc in CreateExecutionEnvironment ()
#11 0x000000010e496cac in JLI_Launch ()
#12 0x000000010e49c819 in main ()
(gdb) info thread
error on line 784 of "/SourceCache/gdb/gdb-1752/src/gdb/macosx/macosx-nat-infthread.c" in function "void print_thread_info(thread_t, int *)": (ipc/send) invalid destination port (0x10000003)

Linux kernel likely/unlikely and !!

久々に Linux kernel を見ていると、likely, unlikely が目についたので調べてみた。

定義は include/linux/compiler.h で次の様になっている。LXR で上に表示されるので、長い方の定義を見てしまっていたけど、通常は短い方の __builtin_expect を使ったものなんだろう。あと、!! に関しても、こんなオペレータあったっけとなってしまった。

#if defined(CONFIG_TRACE_BRANCH_PROFILING) \…
  ...
#  define likely(x)     (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 1))
#  define unlikely(x)   (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 0))
  ...
#else
  ...
# define likely(x)      __builtin_expect(!!(x), 1)
# define unlikely(x)    __builtin_expect(!!(x), 0)
  ...
#endif

定義内の !! に関しては、落ち着いてみれば、 boolean not operator の ! を2回書いてあるだけで 0 を 0, それ以外を 1 の bool 値に変えているだけなんだろう。正確な根拠はあやふやだけど、0 かそれ以外で条件としては十分なはずだけど、!! で bool に変換した方が気持ちがよいという事なのかな。perl, javascript など ! のある言語での !! に関する質問が Stack Overflow にあったけど、文法の詳細まで知り尽くしていない言語で出くわしたら戸惑うのかもしれない。

ちなみに kernel 2.5.0 では !! は付いてない。

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

* 訂正:nminoru さんのコメントにある様に __builtin_expect は2つの引数を一致を期待するので、likely/unlikely の中の使用の2番目の引数 1, 0 になる様に !! を付けているのだと思われます。

likely, unlikey, __builtin_expect に関しては、次の文書の 6.2.2 Optimizing Level 1 Instruction Cache Access が分かりやすかった。

What Every Programmer Should Know About Memory http://www.akkadia.org/drepper/cpumemory.pdf

稀にしか真にならないテストに付随するコードブロックは有効なコードの連続性、I-Cache, Pipeline の有効利用の観点から、よく実行されるコードの間にあると良くない。__builtin_expect でヒントを与えておくと、稀にしか実行されないブロックは良く実行されるブロックから遠くに配置される効果があって、I-Cache, Pipeline の有効利用に繋がるとの事(-O2 で on になる -freorder-blocks が必要)。

GCC のドキュメントには __builtin_XX の説明がある。
http://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html

__builtin_constant_p は引数の式がコンパイル時に定数なら 1, そうで無ければ 0 となる。
__branch_check__ は __builtin_expect と branch 情報の収集のコードに展開されるマクロだった。

http://www.ibm.com/developerworks/jp/linux/library/l-gcc-hacks/ "Linux カーネルのなかに入り込む GCC" という記事でもこのへんの GCC 関数は紹介されていたけど、まさにタイトル通り。llvm-clang には不利な点なのかもしれない。

Building OpenJDK on Windows7

hg clone と get_source.sh でソースをダウンロード。README-build.html を眺めながらトライ。

Windows7 64-bit を使用。VC++ Express 2010 を使用。DirectX9 SDK, cygwin も入れておく(binutils, make, m4, cpio, awk, file, zip, unzip, procps パッケージを追加インストール)。
gmake への依存があるということだったので、gnu make を cygwin 環境でビルドして C:\cygwin\home\yoshi\local\make-3.82 においておく。

FreeType のリンクのエラーも出る事があるので、FreeType 2.4.10 をソースから multithread 版の .lib, .dll にコンパイルしておいて、C:/PROGRA~2/FreeType/lib に freetype.lib, freetype.dll という名前にして置いておく。include も C:/PROGRA~2/FreeType/include にコピーしておく。(jvm.dll をビルドするだけであればこれは必要なさそう)

環境変数設定のための bat ファイル
cygwin\home\… などはユーザー固有)
$ cat set_envs2.bat

set ARCH_DATA_MODEL=32
set ANT_HOME=C:/Users/yoshi/local/apache-ant-1.8.2
set ALT_BOOTDIR=C:/PROGRA~2/Java/jdk1.7.0_03
set VSD=C:\PROGRA~2\MICROS~1.0
set WSDK=C:\PROGRA~2\MICROS~1\Windows\v7.0A
set PATH=C:\cygwin\home\yoshi\local\make-3.82;%VSD%\VC\bin;%WSDK%\bin;%VSD%\Common7\IDE;%ALT_BOOTDIR%\bin;%ANT_HOME%\bin;C:\cygwin\bin;%Path%

set INCLUDE=%VSD%\vc\include;%WSDK%\include

set LIB=%VSD%\vc\lib;%WSDK%\lib

set ALT_FREETYPE_HEADERS_PATH=C:/PROGRA~2/FreeType/include
set ALT_FREETYPE_LIB_PATH=C:/PROGRA~2/FreeType/lib

set CLASSPATH=
set _JAVA_OPTIONS=-Dfile.encoding=ASCII

この batch ファイルを走らせて、hotspot/make の下で make (or gmake) したところ、
hotspot/build// の下に product 版、fast debug 版の jvm.dll ができているようだ。

Windows のコマンドプロンプトから cygwin の make, shell その他のツール、VC++ を呼び出す構成のため、環境変数のパスの区切りが / を使ったり、\ を使ったり、ごちゃごちゃしてるなぁ。

C:\PROGRA~2\MICROS~1.0 のようなショートパスを使うのはディレクトリ名にスペースが入っているときに、シェルの解釈がおかしくならない対策。ショートファイルネームは長いファイル名と一意に対応するわけではなく、作られた順番に依存するようなので、どのような対応になっているかはいちいち dir /x で確認する必要があります。

Tuning Java SE for Throughput and Latency

今日の午後「Tuning Java SE for Throughput and Latency」というタイトルでセミナーをやらせていただきました。

元々は昨年出版された「Java Performance」という本を書かれた Charlie Hunt さんのセミナーです。内容的には Java Performance 本のサブセットのような感じで、それに少し最近の動向等も加えました。

Java Performance 本は Safari Online で入手して、斜め読みしましたが良い本です。HotSpot VM に特化していますが、VM の動作原理(Theory of Operation) 的な章では HotSpot VM の様々なサブシステムの概要が良く説明されています。HotSpot JVM の動作に興味がある方には、いきなり膨大なソースを読むより役立つでしょう。また、GC は種類やオプションも多く、デフォルトより踏み込んでチューニングしていくにはそれなりの基礎知識が必要ですが、それらもいろいろな観点で説明されています。また、後半では App Server など Java EE 環境でのパフォーマンスのアプローチも説明されています。トータルで 680 ページ程度、なかなかのボリュームです。

質問も多くいただいたのですが、きれいに答えが出ない物もありました。

HotSpot ではない VM を使っていて、おそらくネイティブメモリがリークしてそうだが、なかなか絞れないという事も聞きました。ネイティブメモリのリークとなると、良いツールが無いと、ネイティブのメモリ確保に繋がるような呼び出しのスタックトレースを集めて傾向をみると言ったような泥臭い方法しか最終的には無かったりします。再現方法が確実であればいろいろ試行錯誤も出来るのですが。古い OS であれば brk/sbrk system call を呼ぶスタックトレースを集めていくと傾向が見えたりします。最近の glibc はリニアに伸びる事しか出来ない brk の不便さを克服する為か、malloc でもほとんど mmap で領域を確保しているので、mmap にいたるトレースも調べる必要がありそうです。Valgrind とかツールもあるのですが、本番環境でしか減少が見られず、低負荷で情報収集というのは難しい面があります。glibc malloc 自身の fragmentation も起きる時は起きるので、気になります。気軽に jemalloc などと切り替えられるとよいのですが。

また、Java code のアロケーションパターンには余り変化がなさそうなのに、徐々にマイナーGCの所要時間がふえるという現象の話も聞きました。それが Full GC をするとマイナーGC時間は短くなると。そして、とある java program 側の変更でそれが解消されたそうです。マイナーGCの時間の内訳が分からないと難しい所です。大半の GC/Heap がコピーコレクタを使っているので、コピーの量が一定であれば他に要因がありそうです。Remembered Set/Write Barrier の話もしたのですが、Old から Young の参照がだんだん増えるようであれば、時間が徐々にかかる様になる原因かも知れません。ある程度のプロモーションはマイナーGC毎に起きているという事でしたので、可能性としてはあります。検証するには debug build でいいオプションがあればそれをつかって、無い場合には独自の instrumentation を加えて検証するというちょっと手間のかかる事になりそうです。

元のセミナーの中で、Performance test を積極的に、リアクティブなケースでも、プロアクティブなケースでも、デザインの段階から作成して、自動テストのサイクルに組み込むと色々役に立つという事を Charlie Hunt さんが提案されていました。それなりに手間はかかるのですが、うまく作り込めれば予想外のパフォーマンスに関する副作用を速く検知できたり、パフォーマンチューニングや、パフォーマンストラブルシュートで非常に役立つでしょう。このような取り組みの具体的な成果が出て来たら、是非聞いてみたい物です。

本の中でも出てきますが、プロファイルツールとして Solaris Studio Performance Analyzer という物があります。その中の collect -j on に続けて java program コマンドラインを指定するとサンプリングベースで cpu, lock, stack trace などを収集してくれます。これを analyzer で表示すると、実行時にどのスレッドがいつ何をしていたかが java stack, native stack で分かりますし、コンパイルされたコードの中身やインタープリタのどの辺とかも分かります。このスレッドの時系列の表示もカラフルできれい。Java のスレッドも VMThread, compiler thread, GC thread 等の様子も分かり、頭の中の理解が具体的に GUI で確認できて、これはとても面白いツールでした。Solaris, Linux で動きます。いろいろなプログラムのプロファイルとってみたいと考えてます。

ともあれ、終わってホッとしています。