ByteBufferへのコピー速度比較

JavaでByteBufferへのコピーを行う際の速度を簡単に比較してみました。java.nio.ByteBuffer#put(byte)で1byteずつ書き込んでいくのでなければ大体同じですね。

Heap上のbyte arrayをコピー

java.nio.ByteBuffer#put(byte[], int, int)

        byte[] bs = new byte[256 * 1024 * 1024];
        ByteBuffer src = ByteBuffer.wrap(bs);
        ByteBuffer dst = ByteBuffer.allocate(bs.length);
        long start = System.currentTimeMillis();


        dst.put(src.array(), 0, src.remaining());
        System.out.println("heap.byte_array: ByteBuffer.put(byte[], int, int): " + (System.currentTimeMillis() - start));

=> heap.byte_array: ByteBuffer.put(byte[], int, int): 73

java.nio.ByteBuffer#put(java.nio.ByteBuffer)

        dst.put(src);
        System.out.println("heap.byte_array: ByteBuffer.put(java.nio.ByteBuffer): " + (System.currentTimeMillis() - start));

=> heap.byte_array: ByteBuffer.put(java.nio.ByteBuffer): 69

java.nio.ByteBuffer#put(byte) with loop

        for (int i = 0; i < src.remaining(); i++)
            dst.put(src.get());
        System.out.println("heap.byte_array: ByteBuffer.put(byte): " + (System.currentTimeMillis() - start));

=> heap.byte_array: ByteBuffer.put(byte): 380

Heap上のByteBufferをコピー

java.nio.ByteBuffer#put(byte[], int, int)

        ByteBuffer src = ByteBuffer.allocate(256 * 1024 * 1024);
        ByteBuffer dst = ByteBuffer.allocate(256 * 1024 * 1024);
        long start = System.currentTimeMillis();

        dst.put(src.array(), 0, src.remaining());
        System.out.println("heap.bb: ByteBuffer.put(byte[], int, int): " + (System.currentTimeMillis() - start));

=> heap.bb: ByteBuffer.put(byte[], int, int): 72

java.nio.ByteBuffer#put(java.nio.ByteBuffer)

        dst.put(src);
        System.out.println("heap.bb: ByteBuffer.put(java.nio.ByteBuffer): " + (System.currentTimeMillis() - start));

=> heap.bb: ByteBuffer.put(java.nio.ByteBuffer): 70

java.nio.ByteBuffer#put(byte) with loop

        for (int i = 0; i < src.remaining(); i++)
            dst.put(src.get());
        System.out.println("heap.bb: ByteBuffer.put(byte): " + (System.currentTimeMillis() - start));

Off heap上のByteBufferをコピー

java.nio.ByteBuffer#put(byte[], int, int)

        ByteBuffer src = ByteBuffer.allocateDirect(256 * 1024 * 1024);
        ByteBuffer dst = ByteBuffer.allocate(256 * 1024 * 1024);
        long start = System.currentTimeMillis();

        dst.put(src.array(), 0, src.remaining());
        System.out.println("off-heap.bb: ByteBuffer.put(byte[], int, int): " + (System.currentTimeMillis() - start));

=> java.lang.UnsupportedOperationException (it doesn't have array inside)

java.nio.ByteBuffer#put(java.nio.ByteBuffer)

        dst.put(src);
        System.out.println("off-heap.bb: ByteBuffer.put(java.nio.ByteBuffer): " + (System.currentTimeMillis() - start));

=> off-heap.bb: ByteBuffer.put(java.nio.ByteBuffer): 72

java.nio.ByteBuffer#put(byte) with loop

        for (int i = 0; i < src.remaining(); i++)
            dst.put(src.get());
        System.out.println("off-heap.bb: ByteBuffer.put(byte): " + (System.currentTimeMillis() - start));

=> off-heap.bb: ByteBuffer.put(byte): 358

TypeProfileが取得される場合、所得されない場合について

JavaではJITコンパイルに関連するTypeProfileという機構がありますが、これがどのような状況で取得されるのかちょっと興味があったので簡単に試してみました。


komamitsu/TypeProfileTest · GitHub

Childというinterfaceを継承するChildImplA/Bという二つのclassがあり、MainXxxxというclassからChild/ChildImplA/Bの扱い方を微妙に変えて、TypeProfileが取得される挙動を見てみました。

https://github.com/komamitsu/TypeProfileTest/tree/master/org/komamitsu/tptest にMainXxxx.java群が置いてありますが概要から言うと、"あるメソッド内で、あるオブジェクトが `new` によって生成されて、そのオブジェクトが当該メソッド内のlocal変数経由で呼ばれる場合のみTypeProfileが取得されない" ように見えました。具体的には以下です。

TypeProfileTest/MainWithoutFactoryMethodAndImportingB.java at master · komamitsu/TypeProfileTest · GitHub

package org.komamitsu.tptest;
import org.komamitsu.tptest.child.Child;
import org.komamitsu.tptest.child.ChildImplA;
import org.komamitsu.tptest.child.ChildImplB;

public class MainWithoutFactoryMethodAndImportingB {
    public static void main(String argv[]) {
        Child c = new ChildImplA();
        for (int i = 0; i < 10000000; i++) {
            c.exec();
        }
        System.out.println(c.getCount());
    }
}

TypeProfileTest/WithoutFactoryMethodAndImportingB at master · komamitsu/TypeProfileTest · GitHub

$ java -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining org.komamitsu.tptest.MainWithoutFactoryMethodAndImportingB
     62    1    b        java.lang.String::hashCode (55 bytes)
     72    2    b        java.lang.String::indexOf (70 bytes)
                            @ 66   java.lang.String::indexOfSupplementary (71 bytes)   too big
     82    3    b        org.komamitsu.tptest.child.ChildImplA::exec (11 bytes)
     83    4 %  b        org.komamitsu.tptest.MainWithoutFactoryMethodAndImportingB::main @ 10 (41 bytes)
                            @ 17   org.komamitsu.tptest.child.ChildImplA::exec (11 bytes)   inline (hot)
     85    4 %           org.komamitsu.tptest.MainWithoutFactoryMethodAndImportingB::main @ -2 (41 bytes)   made not entrant
10000000

もうちょっと色々なケースで試してみたけれど、ひとまずはここまでで。

JavaのEscape AnalysisでNoEscapeがGlobalEscapeになってしまうケースの検証

Java ™ HotSpot Virtual Machine Performance Enhancements

Escape analysis is supported and enabled by default in Java SE 6u23 and later.

ということで最近のJVMでは有効になっているEscape analysisですが、こんな記事を見つけました。


Richard Burnison - Escape Analysis & Stack Allocated Objects

NoEscapeについて説明しているくだりで

Important to note, however, is that objects requiring finalizer execution are considered GlobalEscape and will not be stack-bound.

という記述があり、ちょっと気になったので試してみました。

$ java -version
java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)

まずは普通に。

public class Main {
    void run() {
        int totalLen = 0;
        for (int i = 0; i < 100000000; i++) {
            String s = new String("hello");
            totalLen += s.length();
        }
        System.out.println(totalLen);
    }

    public static void main(String argv[]) {
        Main main = new Main();
        main.run();
    }
}
  • Escape analysis無し
$ java -Xms512M -Xmx512M -XX:+PrintGCDetails -verbose:gc -XX:-DoEscapeAnalysis Main
[GC [PSYoungGen: 132096K->432K(153600K)] 132096K->440K(503296K), 0.0030680 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
50000000
Heap
 PSYoungGen      total 153600K, used 111393K [0x00000007f5500000, 0x0000000800000000, 0x0000000800000000)
  eden space 132096K, 84% used [0x00000007f5500000,0x00000007fc15c698,0x00000007fd600000)
  from space 21504K, 2% used [0x00000007fd600000,0x00000007fd66c010,0x00000007feb00000)
  to   space 21504K, 0% used [0x00000007feb00000,0x00000007feb00000,0x0000000800000000)
 ParOldGen       total 349696K, used 8K [0x00000007dff80000, 0x00000007f5500000, 0x00000007f5500000)
  object space 349696K, 0% used [0x00000007dff80000,0x00000007dff82000,0x00000007f5500000)
 PSPermGen       total 21504K, used 2554K [0x00000007dad80000, 0x00000007dc280000, 0x00000007dff80000)
  object space 21504K, 11% used [0x00000007dad80000,0x00000007daffebf8,0x00000007dc280000)
  • Escape analysis有り
$ java -Xms512M -Xmx512M -XX:+PrintGCDetails -verbose:gc -XX:+DoEscapeAnalysis Main
50000000
Heap
 PSYoungGen      total 153600K, used 5284K [0x00000007f5500000, 0x0000000800000000, 0x0000000800000000)
  eden space 132096K, 4% used [0x00000007f5500000,0x00000007f5a29028,0x00000007fd600000)
  from space 21504K, 0% used [0x00000007feb00000,0x00000007feb00000,0x0000000800000000)
  to   space 21504K, 0% used [0x00000007fd600000,0x00000007fd600000,0x00000007feb00000)
 ParOldGen       total 349696K, used 0K [0x00000007dff80000, 0x00000007f5500000, 0x00000007f5500000)
  object space 349696K, 0% used [0x00000007dff80000,0x00000007dff80000,0x00000007f5500000)
 PSPermGen       total 21504K, used 2552K [0x00000007dad80000, 0x00000007dc280000, 0x00000007dff80000)
  object space 21504K, 11% used [0x00000007dad80000,0x00000007daffe060,0x00000007dc280000)

Escape analysis有りの場合、Eden領域の使用量が少なくHeapではなくStack allocationになっているものと推測。

では、finalize() 実装版

public class Main {
    static int finalizeCount = 0;

    void run() {
        int totalLen = 0;
        for (int i = 0; i < 10000000; i++) {
            String s = new String("hello");
            totalLen += s.length();
        }
        System.out.println(totalLen);
        System.out.println(finalizeCount);
    }

    @Override
    protected void finalize() throws Throwable {
        finalizeCount++;
        super.finalize();
    }

    public static void main(String argv[]) {
        Main main = new Main();
        main.run();
    }
}

これをEA有効にして実行してみると

$ java -Xms512M -Xmx512M -XX:+PrintGCDetails -verbose:gc -XX:+DoEscapeAnalysis Main
50000000
0
Heap
 PSYoungGen      total 153600K, used 5284K [0x00000007f5500000, 0x0000000800000000, 0x0000000800000000)
  eden space 132096K, 4% used [0x00000007f5500000,0x00000007f5a29028,0x00000007fd600000)
  from space 21504K, 0% used [0x00000007feb00000,0x00000007feb00000,0x0000000800000000)
  to   space 21504K, 0% used [0x00000007fd600000,0x00000007fd600000,0x00000007feb00000)
 ParOldGen       total 349696K, used 0K [0x00000007dff80000, 0x00000007f5500000, 0x00000007f5500000)
  object space 349696K, 0% used [0x00000007dff80000,0x00000007dff80000,0x00000007f5500000)
 PSPermGen       total 21504K, used 2552K [0x00000007dad80000, 0x00000007dc280000, 0x00000007dff80000)
  object space 21504K, 11% used [0x00000007dad80000,0x00000007daffe2f0,0x00000007dc280000)

と、finalize() 実装前と変わらないので、"objects requiring finalizer execution are considered GlobalEscape and will not be stack-bound" というのは(少なくとも1.7.0_45では)気にしなくて良さそうかなぁと思います。

jackson-dataformat-msgpack

数ヶ月前から
というのを細々と作ってました。

これは何かというと、
msgpack/msgpack-java at v07-develop · GitHub で開発が進められているMessagePack Javaのv0.7系の実装と
FasterXML/jackson-databind · GitHub の豊富なデータバインディング機能を連携させるライブラリです。

作った背景ですが、

  • 性能面およびメンテナンス性において大きく改善すべく開発が進められているmsgpack-java v07では、v06まで提供していたある程度のデータバインディング機能を自前で提供する予定が今のところ無いので何かしら必要
  • そもそものmsgpack-java v06までのデータバインディング機能の機能的な制限が結構辛い

といった感じです。

現状でもv06で扱えない色々なケースに対応しているっぽいので便利っぽい(ぽいぽい)。

本家jackson-databind (JSON format)と比較すると(
https://github.com/komamitsu/jackson-dataformat-msgpack/tree/master/src/test/java/org/msgpack/jackson/dataformat/msgpack/benchmark
)性能的には下記のような感じで 1.1 ~ 1.5 倍くらい遅いです。とりあえず手なりで書いて、msgpack-java v07自体で改善した方が良さそうなところを直してもらって、位しかまだやってないので、そろそろjackson-dataformat-msgpack自体の性能を改善する時期なのかもしれないです。

normal object mapper: deserialize(huge_data) => 354
msgpack object mapper: deserialize(huge_data) => 371

normal object mapper: serialize(huge_data) => 130
msgpack object mapper: serialize(huge_data) => 137

normal object mapper: serialize(pojo) => 1288
msgpack object mapper: serialize(pojo) => 1594

normal object mapper: deserialize(pojo) => 2075
msgpack object mapper: deserialize(pojo) => 2998

あと、msgpack-java v07は現状Android (Dalvik VM) では動かないので、結果的に jackson-dataformat-msgpack でも動かない状態なので、後で msgpack-java v07 自体のほうも対応しないとなぁとか思ってます。

とりあえず、割と使い勝手が良い感じがするので使って頂いて、フィードバック等あればissueに突っ込んで頂けると嬉しいです。

chef-client実行時のエラーをHipChatに通知する方法

TreasureDataではエンジニア間のコミュニケーションツールとしてHipChatをがっつり使っているのですが、同じくTDで使われているchefのchef-client実行時のエラーをHipChatの部屋に流すようにしてみました。という小ネタ。

Chefには About Handlers — Chef Docs という機構があって、chef-clientの起動、成功・失敗時に発動する処理を登録できます。特にexception handlerはchef-clientが正常に処理できなかった場合に何かしらの通知をする、といった使い方ができるので有用かと思います。

Chef::Handlerを継承して自作のHandlerを作ればぶっちゃけ何でもできてしまうのですが、今回はできるだけコードを書かずに目的を達成してみます。自分で色々なHandlerを作ってみたい場合は前述のリンクを一読してみることをお勧めします。

まずはじめに、cwjohnston/chef-hipchat · GitHub にHipChat用のcookbookがあるので、これを利用可能にしておきます。

この cwjohnston/chef-hipchat · GitHubmojotech/hipchat · GitHub で提供されているHipChat用Handlerを、error_handlersに簡単に登録する実装を持っているので、これを有効にします。これによってchef-clientの失敗時にHipChatの任意のroomに通知が飛ぶようになります。対象となるchefのroleに以下のような変更を加えることで、この機能が有効になるかと思います。

run_list(
    :
  "recipe[hipchat::handler]",
    :
)
override_attributes(
    :
  :hipchat => {
      'handler' => {
        'token' => '123a456b789c098d765e432f',
        'room' => 'ChefAlert',
        'enabled' => true,
      }
  },

ここで、'token' はHipChatのAPI Token, 'room' はHipChatの通知先を指定してください。

chef-clientの失敗をHipChat等に通知させるようにしておくと、たまに意外なエラーが発生しているのがわかって興味深かったり、致命的な問題を未然に防ぎやすくなるのでおすすめです。

コンパイル時に特定のAnnotationが付いているClassのmodifierをチェックする方法

先日、


という事例を聞いたので何とかコンパイル時に検知出来ないかぼーっと考えてて、プリプロセス的にチェックできないか思いついたので試してみました。

#ちなみにTweet上ではリフレクションとあったけど、多分実際はJavassistを使ってる気がする


今回サンプルとして動かしてみたのは、@Helloというannotationがついたクラスはstaticじゃないとエラーにするやつ。

org.komamitsu.pptest.annotation.Hello

package org.komamitsu.pptest.annotation;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
public @interface Hello {}

org.komamitsu.pptest.Main

package org.komamitsu.pptest;

import org.komamitsu.pptest.annotation.Hello;

public class Main {
    @Hello
    static class A {
    }
    
    @Hello
    class B {
    }

    public static void main(String argv[]) {
        System.out.println("helloworld");
    }
}

こんな感じになっている場合に、コンパイル時にclass Bをエラーとしたい。


で、プリプロセス的なタイミングでチェックするやつがこれ

org.komamitsu.pptest.preprocessor.Hello

package org.komamitsu.pptest.preprocessor;

import java.util.Set;

import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.annotation.processing.*;
import javax.tools.Diagnostic.*;

@SupportedAnnotationTypes(value= {"*"})
public class Hello extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element elm : roundEnv.getElementsAnnotatedWith(org.komamitsu.pptest.annotation.Hello.class)) {
            Set<Modifier> modifiers = elm.getModifiers();
            if (!modifiers.contains(Modifier.STATIC)) {
                processingEnv.getMessager().printMessage(Kind.ERROR, "@Hello: Only accepts static class. " + elm);
            }
        }
        return true;
    }
}

一応、これも。

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.komamitsu</groupId>
  <artifactId>pptest</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Main classとかをコンパイルするときにorg.komamitsu.pptest.preprocessor.Helloを挟んであげれば良さそうなのだけど、スマートな方法が思いつかず以下のような手順で...

$ pwd
/Users/komamitsu/Dev/workspace/pptest
$ mvn clean package
    :
$ cd src/main/java/
$ javac -cp ../../../target/pptest-0.0.1-SNAPSHOT.jar -processor 
error: @Hello: Only accepts static class. org.komamitsu.pptest.Main.B
1 error

本当はちょこっとwarningが出てるんだけど、見辛いし本筋と関係ないので省略。

とまあ、こんな感じでコンパイル時にチェックできそうなんだけども、これをどうmsgpackに組み込むか(ユーザーが特に何もせずに当該チェックが作動するか)が思いついてない...

Raspberry PiでFluentdを立ち上げてTDにデータを流してみた話

一ヶ月程前に同僚である @doryokujin 先生からRaspberry Pi(面倒なので以下Pi)を頂いたのだけど、Piに必要なSDカードが手元に無くてすっかり放置していたのですが、満を持してSDカードを買ってきたのでちょっとPiを立ち上げてみた。という話を備忘録的に。
 
必要なもの(というかこれで間に合った、的なもの)

  • USBケーブル
  • RCAケーブル
  • USBキーボード
  • LANケーブル
  • SDカード(SDHD 8GB)

 
startxとか叩かなければUSBマウスは要らない。
 

Piの立ち上げ

  • 準備に使った環境はMac OX 10.8.5
  • SDカードは挿してある状態
  • SDカードの/dev/disk番号を確認してunmount
$ diskutil list
/dev/disk0
     :
/dev/disk4
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *7.8 GB     disk4
   1:                 DOS_FAT_32 NO NAME                 7.8 GB     disk4s1
$ diskutil unmountDisk /dev/disk4
Unmount of all volumes on disk4 was successful

 

  • OSにはRaspbianを選択。2014-01-07-wheezy-raspbian.zip を落としてきて展開したimgファイルをddで書き込み。かなり時間がかかる
$ sudo dd bs=1m if=~/Downloads/2014-01-07-wheezy-raspbian.img of=/dev/disk4
Password:
2825+0 records in
2825+0 records out
2962227200 bytes transferred in 1071.833726 secs (2763700 bytes/sec)
  • ddでイメージが書き込まれたSDカードをPiに挿してMBPとか適当なやつからUSBケーブルをつなげて電源を供給して上げれば起動
  • 自宅にディスプレイが無いので、あまり使われていないアナログテレビを用いて動作確認。RCAケーブルの黄色いやつ(映像用)をつなげれば滲んだ雰囲気の画面で確認出来る
  • Setting画面が表示されてユーザーのパスワードの変更とか色々できるので、やりたい場合はやる感じで。SSHの設定に関しては失敗してたけどひとまず先に進む
  • Settingを終えるとlogin promptが表示されるのでlogin。sshdも起動していたのでログインしてみたら普通に入れた。
  • Rubyは1.9.3, Javaは1.7が入っていて良い感じ

 

Fluentdとかを入れてTDにデータを流し込んでみる

  • Fluentdの安定版であるtd-agent(http://docs.fluentd.org/articles/install-by-deb)入れようかと思ったけど、何となくgemから入れてみた。FluentdはC拡張のビルドが発生するので、ruby-dev的なものを入れておく
$ sudo aptitude install ruby-dev
$ sudo gem install fluentd
  • TreasureDataにデータを入れるのでfluent-flugin-tdも。というか事前にTDのアカウントが必要なので適当に作っておくのが前提
$ sudo fluent-gem install fluent-plugin-td
  • fluentdのconfファイルを作成(/home/pi/fluentd/fluent.confとか)。入力はin_forwardだけの超シンプルな設定。出力はout_td。
<match td.*.*>
  type tdlog
  apikey <replace this with your TD API key!>
 
  auto_create_table
  buffer_type file
  buffer_path /home/pi/fluentd/td
</match>
 
<source>
  type forward
</source>
  • fluentd起動
$ fluentd -c /home/pi/fluentd/fluent.conf

fluentdデーモンとして起動したい場合は以下のようにしても良いかも。

$ export FLUENTD_ROOT=/home/pi/fluentd
$ fluentd -c $FLUENTD_ROOT/fluent.conf --log $FLUENTD_ROOT/fluentd.log --daemon $FLUENTD_ROOT/fluentd.pid
  • fluent-catとかfluent-postコマンドなどでレコードを放り込んでみる
$ echo '{"name":"pi", "age":49}' | fluent-cat td.pidb.test
  • td tablesコマンドで件数を確認
$ td tables pidb
+----------+-------+------+-------+--------+---------------------------+---------------------------+-----------------------+
| Database | Table | Type | Count | Size   | Last import               | Last log timestamp        | Schema                |
+----------+-------+------+-------+--------+---------------------------+---------------------------+-----------------------+
| pidb     | test  | log  | 1     | 0.0 GB | 2014-02-15 23:17:20 +0900 | 2014-02-15 23:14:15 +0900 | age:long, name:string |
+----------+-------+------+-------+--------+---------------------------+---------------------------+-----------------------+
1 row in set
  • td query コマンドでクエリーを投げてみる
$ td query -w -d pidb 'select name, age from test'
   :
Status     : success
Result     :
+------+-----+
| name | age |
+------+-----+
| pi   | 49  |
+------+-----+
1 row in set

 
見返してみると、記事にするほどの内容でも無い気がしてきたけど、勢いで書いたのでまぁ良いのではと。
 
PiにはGPIOとか付いているのでセンサー的なものをくっつけて、そこからの情報をTDに送り続けるというのは割と簡単に出来そう。データ量をあまり気にせずどんどん放り込んで後で集計の切り口を試行錯誤しやすいのは、1TDユーザーとしても使っていてかなり楽なので相性が良さげ。