Page List

Search on the blog

ラベル OSS の投稿を表示しています。 すべての投稿を表示
ラベル OSS の投稿を表示しています。 すべての投稿を表示

2013年9月30日月曜日

Javaのビット演算いろいろ(1)

Integerクラスにはビット演算の良質な使用例がたくさんあります。
そのうちのいくつかを簡単な解説と一緒に載せておきます。

highestOneBit
public static int highestOneBit(int i) {
    // HD, Figure 3-1
    i |= (i >>  1);
    i |= (i >>  2);
    i |= (i >>  4);
    i |= (i >>  8);
    i |= (i >> 16);
    return i - (i >>> 1);
}
最初の5行(コメントは抜かして)で、1になっている桁より右の桁をすべて1にします。
そして最後の行で1だけ右シフトしたものを引くと、hightest one bitがえられます。

lowestOneBit
public static int lowestOneBit(int i) {
    // HD, Section 2-1
    return i & -i;
}
これは有名。プログラミング(競技プログラミング?)をやっている人なら何度も見たことがあると思います。iを「negateする=ビット反転して1を足す」なので、lowestOneBitだけが1になります。

reverse
public static int reverse(int i) {
    // HD, Figure 7-1
    i = (i & 0x55555555) << 1 | (i >>> 1) & 0x55555555;
    i = (i & 0x33333333) << 2 | (i >>> 2) & 0x33333333;
    i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f;
    i = (i << 24) | ((i & 0xff00) << 8) |
        ((i >>> 8) & 0xff00) | (i >>> 24);
    return i;
}
ビット反転処理(順序の反転)です。FFTのプログラムを書くときに使うかもしれません。

これはDivide and Conquerです。
最初の3行(コメントは抜かして)は、以下のような1byte内の反転処理をしています。
次の行では、byte単位の反転を行っています。

2013年9月27日金曜日

Integer.getCharsが面白い

 JavaのIntegerクラスgetCharsメソッドがおもしろい。このメソッド自体はpackage privateなので直接呼び出すことはできないが、Integer.toStringのメイン処理が実装されている。

メソッドのシグネチャはgetChars(int i, int index, char[] buf)で整数iを表す文字列をbufに格納するというもの。このとき文字列はindexから0の方向にLSB -> MSBの順で埋められていく。
これだけ聞くと簡単そうに聞こえるが、高速化のためにおもしろい処理がされている。

とりあえず実装を載せておく。
static void getChars(int i, int index, char[] buf) {
        int q, r;
        int charPos = index;
        char sign = 0;

        if (i < 0) {
            sign = '-';
            i = -i;
        }

        // Generate two digits per iteration
        while (i >= 65536) {
            q = i / 100;
        // really: r = i - (q * 100);
            r = i - ((q << 6) + (q << 5) + (q << 2));
            i = q;
            buf [--charPos] = DigitOnes[r];
            buf [--charPos] = DigitTens[r];
        }

        // Fall thru to fast mode for smaller numbers
        // assert(i <= 65536, i);
        for (;;) {
            q = (i * 52429) >>> (16+3);
            r = i - ((q << 3) + (q << 1));  // r = i-(q*10) ...
            buf [--charPos] = digits [r];
            i = q;
            if (i == 0) break;
        }
        if (sign != 0) {
            buf [--charPos] = sign;
        }
    }

まず第一段の処理としてiが65536以上の場合は2桁(10進数で)ずつ処理していく。
コメントを見ると、
r = i - ((q << 6) + (q << 5) + (q << 2))

r = i - (q * 100)
が等価であることが分かる。確かに100 = 64 + 32 + 4なのでそのとおり。DigitOnesとDigitTensはそれぞれ2桁の数字の1桁目と2桁目をO(1)で取り出すためのテーブル。これはハードコーディングされている。

まあここまでは分かる。

問題は次の第二段目の処理。

q = (i * 52429) >>> (16+3);
r = i - ((q << 3) + (q << 1));

何ですか、これは。。
何ですか、これは。∑( ̄Д ̄;)

先ほどの100倍と同じように、
r = i - ((q << 3) + (q << 1));
は、r = i - (q * 10)のこと。

ということは逆算して考えると、
q = (i * 52429) >>> (16+3);
ってのは、
q = i / 10ですか?試してみると確かにそうなっている。

でも何で52429と(16+3)なの?他の数字じゃだめなの??

ということで、いろいろ試行錯誤して「52429」が唯一使える数字ということを確認したコードが以下。
package com.kenjih.test;

public class Clazz {
    public void run() {
        // 割り算をシフト演算にしたいので、2の冪乗のみ
        for (int i = 0 ; i < 32 ; i++) {
            int x = 1<<i;
            
            // 10 * y / x == 1となるようなyが候補。
            int y = x/10;
            if (y++ == 0)
                continue;
            
            // 65535を掛けたとき0xffffffffを越えないか?
            //(シフト演算が>>>なのでCでいうところのunsigned intの範囲までなら扱える)
            if (y * 65535L > 0x0ffffffffL)
                continue;
            
            // 60009 * y - 6000 * x < xか?
            // 上式が成り立たない場合は、60009 * y % x = 6001(以上)になってしまう。
            if (x * 6001 > 60009 * y)
                System.out.println(y);
        }
    }
    
    public static void main(String[] args) {
        new Clazz().run();
    }
}
以下が実行結果。
$ 52429

まさにシフト演算のマジックやぁ~~。(ノ ̄□ ̄)ノオオオォォォォ!
コイツはしびれますね。

で結局これってほんとに速いの??
ということで高速化を考えず書いた普通の処理と比べてみる。

package com.kenjih.test;

public class MyInteger {
    private static char[] digits = {
        '0', '1', '2', '3', '4',
        '5', '6', '7', '8', '9'
    };
    
    public static String toString(int i) {
        char[] buf = new char[16];
        boolean minus = i < 0;
        int pos = 16;
        while (i > 0) {
            buf[--pos] = digits[i % 10];
            i /= 10;
        }
        if (minus)
            buf[pos] = '-';
        return new String(buf, pos, (16 - pos));
    }
}
普通に書くとまあこんなもんでしょっ。
桁数が大きい方が高速化が生きて来そうなので、1,000,000,000 - 1,100,000,000までを変換する処理も用いて比べてみた。

比較結果。
Integer = 0m4.499s
MyInteger = 0m5.681s

2割くらい速い!(微妙っ。いや、この違いは結構大きいのか?)

2013年9月19日木曜日

OSS日記(3) Unit Testのソースを書いてみる

 org.apache.commons.lang3.ValidateクラスのinclusiveBetweenメソッドのテストを書いてみた。
まず、自分で書いてみたテスト。
@Test
public void testInclusiveBetween()
{
    try {
        Validate.inclusiveBetween(0, 10, -1);
        fail("An IllegalArgumentException was not thrown.");
    } catch (IllegalArgumentException e) {
        assertEquals("The value -1 is not in the specified inclusive range of 0 to 10", e.getMessage());
    }
    Validate.inclusiveBetween(0, 10, 0);
    Validate.inclusiveBetween(0, 10, 3);
    Validate.inclusiveBetween(0, 10, 10);        
    try {
        Validate.inclusiveBetween(0, 10, 11);
        fail("An IllegalArgumentException was not thrown.");
    } catch (IllegalArgumentException e) {
        assertEquals("The value 11 is not in the specified inclusive range of 0 to 10", e.getMessage());
    }
}
実際のテストコード。
@Test
public void testInclusiveBetween()
{
    Validate.inclusiveBetween("a", "c", "b");
    Validate.inclusiveBetween(0, 2, 1);
    Validate.inclusiveBetween(0, 2, 2);
    try {
        Validate.inclusiveBetween(0, 5, 6);
        fail("Expecting IllegalArgumentException");
    } catch (final IllegalArgumentException e) {
        assertEquals("The value 6 is not in the specified inclusive range of 0 to 5", e.getMessage());
    }
}

うーん、なるほど。inclusiveBetweenの内部ではcompareToを使って比較しているので数値だけじゃなくて文字列も確認しないといけないか。
区間の中にあるとき、区間の端にあるとき、区間の外にあるときの3種類でテストされているけど、始点と終点でやらなくてもいいんだろうか??経路網羅はされているからいいのだろうか。大したボリュームじゃないから書いとけばいいのにって気もする。


2013年9月15日日曜日

OSS日記(2) HashMapで最頻値を計算するときはIntegerよりMutableIntの方がいい

org.apache.commons.lang3.ObjectUtils#modeのソースを読んでいたら、MutableIntという見慣れないクラスが使われていた。このクラスもCommons Langが提供しているものらしい。
int型のラッパークラスIntegerはimmutableなので、副作用を伴う使い方をしたい場合はMutableIntを使った方がいいということだろうか。確かにそっちの方が速そうだ。

ということで実験してみた。

ソースコード
あるデータ集合内に現れるデータの頻度を数えるような簡単なサンプルを書いて比較。 まず普通のIntegerを使った場合。
package com.kenjih.test;

import java.util.HashMap;
import java.util.Map;

public class Clazz {
    public void run() {
        final String[] names = { "Alice", "Bob", "Carol", "Dave" };

        Map<String, Integer> frequence = new HashMap<String, Integer>();
        for (int i = 0; i < 100000000; i++) {
            String name = names[i % 4];
            if (!frequence.containsKey(name))
                frequence.put(name, 0);
            int cnt = frequence.get(name);
            frequence.put(name, ++cnt);
        }

        System.out.println(frequence);
    }

    public static void main(String[] args) {
        new Clazz().run();
    }
}

次にMutableIntを使った場合。
package com.kenjih.test;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.mutable.MutableInt;

public class Clazz {
    public void run() {
        final String[] names = { "Alice", "Bob", "Carol", "Dave" };

        Map<String, MutableInt> frequence = new HashMap<String, MutableInt>();
        for (int i = 0; i < 100000000; i++) {
            String name = names[i % 4];
            if (!frequence.containsKey(name))
                frequence.put(name, new MutableInt());
            frequence.get(name).increment();
        }

        System.out.println(frequence);
    }

    public static void main(String[] args) {
        new Clazz().run();
    }
}

比較結果
[比較環境]
CPU: Intel(R) Core(TM) i5-2450M CPU @ 2.50GHz(4コア)
メモリ: 4GB

 [コンパイル/実行コマンド]
$ javac -cp ${HOME}/bin/commons-lang3-3.1/commons-lang3-3.1.jar -d bin src/com/kenjih/test/Clazz.java
$ time java -cp bin:${HOME}/bin/commons-lang3-3.1/commons-lang3-3.1.jar com.kenjih.test.Clazz

 [比較結果]
# valueの型がIntegerの場合
real 0m2.204s
user 0m2.156s
sys 0m0.100s

# valueの型がMutableIntの場合
real 0m1.339s
user 0m1.356s
sys 0m0.004s

OSS日記(1) Commons Langの開発環境を作る



 Apache CommonsのCommons Langの開発環境をローカルマシン上で整えてみました。いくつかハマったところがあったのでメモを残しておきます。

1. EclipseからSVNを使えるようにする
1-1) 以下の2つのプラグインをインストール
  • Subversive Plug-In
  • SVN Connectors

2. SVNレポジトリをプロジェクトに取り込む
2-1) Window -> Open Perspective -> Java でJavaパースペクティブを開く
2-2) Pakage Explorerビューで右クリック -> New -> Other -> Project from SVNを選択する -> Next
2-3) URLに「http://svn.apache.org/repos/asf/commons/proper/lang/trunk」と入力
-> Advancedタブを選択し、Enable Structure Detectionをオフにする。 -> Next -> Finish
2-4) Check out as a project configured using the New Project Wizardを選択し
Finish
2-5) プロジェクト作成ウィザードが自動で立ち上がるので設定する
Java Projectを選択 -> Next
2-6) Project name: に「Commons-Lang」と入力 -> Finish
2-7) パスワードの設定
Secure Storageウィンドウが立ち上がる。適当にパスワードを設定する。

3. ビルドパスの設定
SVNから取り込んだだけだとEclipse上でビルドパスが正しく設定されないため、手動で設定が必要。
3-1) Package Explorer内の「Commons-Lang」プロジェクトを右クリック -> Properties -> Java Build Pathを選択 -> Sourceタブ
3-2) デフォルトで設定されているCommons-Lang/srcをRemove
3-3) Add Folder -> src/main/javaにチェック -> OK
3-4) Add Folder -> src/test/javaにチェック -> OK
3-5) OKを押してProperties for Commons-Langウィンドウを閉じる

4. 依存ライブラリのインポート
プロジェクト内でjunit, commons-ioなどが使用されているため、これらの外部ライブラリをインポートします。
pom.xmlが同梱されているので、Mavenを使って設定します。
4-1) Package Explorer内の「Commons-Lang」プロジェクトを右クリック -> Configure -> Convert to Maven Project

これだけ。Maven神だよっ!

5. Antの設定
5-1) /Commons-Lang/build.properties.sampleファイルをコピペして、/Commons-Lang/build.propertiesを作成
5-2) Ant関連ファイルを修正(REV 1523209の断面ではMavenで入れたバージョンと差異がある箇所や設定漏れがあるため)

■build.properties
repository=${user.home}/.m2/repository
junit.home=${repository}/junit/junit/4.11/ # 4.10を4.11に修正
easymock.home=${repository}/org/easymock/easymock/3.1/
commons-io.home=${repository}/commons-io/commons-io/2.4/
hamcrest.home=${repository}/org/hamcrest/hamcrest-core/1.3/ # 追記

■default.properties
repository=${user.home}/.m2/repository
# The location of the "junit.jar" JAR file
junit.jar = ${junit.home}/junit-4.11.jar # 4.10を4.11に修正

# The location of the Easymock jar
easymock.jar = ${easymock.home}/easymock-3.1.jar

# The location of the Commons-IO jar
commons-io.jar = ${commons-io.home}/commons-io-2.4.jar

# The location of the hamcrest Jar
hamcrest.jar = ${hamcrest.home}/hamcrest-core-1.3.jar # 追記

■build.xml
repository=${user.home}/.m2/repository
<!-- ========== Construct unit test classpath ============================= -->
<path id="test.classpath">
    <pathelement location="${build.home}/classes"/>
    <pathelement location="${build.home}/tests"/>
    <pathelement location="${junit.jar}"/>
    <pathelement location="${easymock.jar}"/>
    <pathelement location="${commons-io.jar}"/>
    <pathelement location="${hamcrest.jar}"/>   <!-- 追記 -->
</path>

6. ビルド&テスト実行
ようやく設定完了。試しにビルド&テストしてみます。
6-1)ビルド実行
build.xmlを選択 -> Outlineビュー -> compileを選択し右クリック -> Run As Ant Build

6-2)テスト実行
build.xmlを選択 -> Outlineビュー -> testを選択し右クリック -> Run As Ant Build

来たーーーーーーーーっ!