マルチプロセスで使用可能なロックフリーキュー

タイトルの通り、マルチプロセスで使用可能なロックフリーのFIFOキューを実装したので、その簡単な紹介。

作成物

github: ipc-msgque (0.0.4)

  • ロックフリーなFIFOキュー
    • 再入可能 かつ SIGKILLに対して安全*1
  • C++
  • 共有メモリ(mmap)を使用
  • マルチプロセス(and マルチスレッド)間の通信に使用可能
  • gcc(ver4.1以上)*2 かつ POSIX準拠環境*3でのみ使用可能

単機能な割に、内部では「まず(割合)汎用的な可変長ブロックメモリアロケータを作って、その上に固定長ブロックアロケータ、さらにその上にFIFOキューを実装」と地味に凝ったことをしている。

使用例

fork()と併用した例。

/**
 * 親子プロセスで共有するFIFOキューを作成し、子から親へメッセージを送信するサンプルプログラム
 * 
 * ファイル名: msgque-sample.cc
 * コンパイル: g++ -o msgque-sample msgque-sample.cc
 */
#include <imque/queue.hh>  // インクルードパスを通しておく

#include <unistd.h>    // fork, getpid
#include <sys/types.h>
#include <stdio.h>     // sprintf
#include <string.h>    // strlen
#include <iostream>
#include <string>

#define CHILD_COUNT 10        // 子プロセスの数
#define QUEUE_ENTRY_COUNT 32  // キューの最大要素数
#define SHM_SIZE 4096         // キューが使用可能な共有メモリのバイト数

int main(int argc, char** argv) {
  // 要素数と共有メモリサイズを指定してキューを作成
  imque::Queue que(QUEUE_ENTRY_COUNT, SHM_SIZE);  
  if(! que) {
    return 1;
  } 

  for(int i=0; i < CHILD_COUNT; i++) {
    if(fork() == 0) {
      // 子プロセスの処理
      char buf[1024]; 
      sprintf(buf, "Hello: %d", getpid());

      // enqueue
      que.enq(buf, strlen(buf));
      return 0;
    }
  }

  // 親プロセスの処理
  for(int i=0; i < CHILD_COUNT; i++) {
    std::string buf;

    // dequeue
    while(que.deq(buf) == false);  // キューが空の間はビジーループ
    std::cout << "[receive] " << buf << std::endl;
  }

  return 0;
}

実行結果:

$ ./msgque-sample 
[receive] Hello: 12736
[receive] Hello: 12737
[receive] Hello: 12738
[receive] Hello: 12740
[receive] Hello: 12739
[receive] Hello: 12742
[receive] Hello: 12744
[receive] Hello: 12743
[receive] Hello: 12745
[receive] Hello: 12741

気が向けば、内部で使用しているメモリアロケータのコードなども載せていくかもしれない。

*1:ただし、メモリを確保してから解放するまでの間にSIGKILL等でプロセスがダウンした場合は、その分のメモリはリークする

*2:__sync_bool_compare_and_swap 等の各種アトミック関数を使用しているため。

*3:共有メモリの仕組みとしてmmapを使用しているため。

ループ処理を関数型っぽく書いてみる(1)

今週は、common lispでループ処理を関数型っぽく、かつ効率良く実装できるかどうかを試していたので、その結果を載せておく。
結論から云えば、処理系の十分な最適化を期待できれば、関数型っぽく書いても、手続き型的に書いた場合と比肩しえる性能が得られそうな感じだった。

成果物

作ったもの: loop
今回はこれの使用例とベンチマーク結果を、次回は実装方法を載せる予定。

使用例

シーケンス(or 無限シーケンス)とmapとかfilterとかを組み合わせてループを表現する。

;; 1から5までの数値を表示する
> (loop:each (lambda (n) (print n))
             (loop:from 1 :to 5))
1
2
3
4
5
=> NIL

> (loop:from 1 :to 5)
=> #<CLOSURE (LAMBDA () :IN LOOP:FROM) {10030E0E0B}> ; 実態はクロージャー

;; マッピング
> (loop:collect (loop:map (lambda (c) (list c (char-code c)))
                          (loop:for-string "mapping")))
=> ((#\m 109) (#\a 97) (#\p 112) (#\p 112) (#\i 105) (#\n 110) (#\g 103))

;; フィルター
> (loop:collect (loop:filter #'oddp (loop:from 1 :to 10)))
-> (1 3 5 7 9)

;;; fizzbuzz
> (defun fizzbuzz-seq ()
    (loop:filter #'consp
      (loop:map (lambda (n) (cond ((zerop (mod n 15)) (cons n :fizzbuzz))
                                  ((zerop (mod n  5)) (cons n :buzz))
                                  ((zerop (mod n  3)) (cons n :fizz))
                                  (t nil)))
                (loop:from 1))))

;; 先頭三つ
> (loop:collect (loop:take 3 (fizzbuzz-seq)))
=> ((3 . :FIZZ) (5 . :BUZZ) (6 . :FIZZ))

;; 10から12番目
> (loop:collect (loop:take 2 (loop:drop 10 (fizzbuzz-seq))))
=> ((24 . :FIZZ) (25 . :BUZZ))

;; 100以下かつ偶数のものだけ
> (loop:collect 
   (loop:take-while (lambda (x) (<= (car x) 100)) 
                    (loop:filter (lambda (x) (evenp (car x)))
                                 (fizzbuzz-seq))))
=> ((6 . :FIZZ) (10 . :BUZZ) (12 . :FIZZ) (18 . :FIZZ) (20 . :BUZZ) (24 . :FIZZ)
    (30 . :FIZZBUZZ) (36 . :FIZZ) (40 . :BUZZ) (42 . :FIZZ) (48 . :FIZZ)
    (50 . :BUZZ) (54 . :FIZZ) (60 . :FIZZBUZZ) (66 . :FIZZ) (70 . :BUZZ)
    (72 . :FIZZ) (78 . :FIZZ) (80 . :BUZZ) (84 . :FIZZ) (90 . :FIZZBUZZ)

;; zip
> (loop:collect 
    (loop:take 3
      (loop:map-n 3 (lambda (x y z) (list x y z))  ; zipと組み合わせる場合は XXX-n 系のマクロを使用して、引数の数を指定する
        (loop:zip (loop:filter #'oddp (loop:from 1))
                  (loop:down-from 100 :by 3)
                  (loop:map #'sqrt (loop:repeat (lambda () (random 1000))))))))
=> ((1 100 19.078785) 
    (3 97 19.052559) 
    (5 94 16.309507))

;; フィボナッチ数列を定義
(defun fib-seq () 
  (let ((n+1 1))
    (declare (fixnum n+1))
    (loop:make-generator 
     :init (lambda () 0)                             ; 初期値生成関数
     :next (lambda (n) (prog1 n+1 (incf n+1 n)))     ; 値更新関数
     :end? (lambda (n) (declare (ignore n)) nil))))  ; 終端判定関数

> (loop:collect (loop:take 20 (fib-seq)))
=> (0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181)

速度比較

loopマクロやdoを使って書いた場合との速度比較。
処理系はSBCL(1.0.54)

比較1: sum関数 (単純なループ)
;; 準備
(defparameter *fastest* '(optimize (speed 3) (safety 0) (debug 0) (compilation-speed 0)))
(defparameter *note* '(sb-ext:unmuffle-conditions sb-ext:compiler-note))

;; loopパッケージ版
(defun sum1 (start end)
  (declare (fixnum start end) #.*fastest* #.*note*)
  (loop:reduce (lambda (acc x) (the fixnum (+ acc x)))
               0
               (loop:from start :to end)))

;; loopマクロ版
(defun sum2 (start end)
  (declare (fixnum start end) #.*fastest* #.*note*)
  (loop WITH total fixnum = 0
        FOR i FROM start TO end
        DO (incf total i)
        FINALLY (return total)))  ; (loop ... SUM i) では最適化されない部分があるので、少し長くなるけどこちらを採用

;; do版
(defun sum3 (start end)
  (declare (fixnum start end) #.*fastest* #.*note*)
  (let ((total 0))
    (declare (fixnum total))
    (do ((i start (1+ i)))
        ((> i end) total)
      (incf total i))))

;; 実行
> (time (sum1 1 100000000))  ; loopパッケージ版: 0.084秒
Evaluation took:
  0.084 seconds of real time
  0.084006 seconds of total run time (0.084006 user, 0.000000 system)
  100.00% CPU
  167,231,593 processor cycles
  0 bytes consed
=> 5000000050000000

> (time (sum2 1 100000000)) ; loopマクロ版: 0.086秒
Evaluation took:
  0.086 seconds of real time
  0.088005 seconds of total run time (0.088005 user, 0.000000 system)
  102.33% CPU
  171,410,077 processor cycles
  0 bytes consed
=> 5000000050000000

> (time (sum3 1 100000000)) ; do版: 0.083秒
Evaluation took:
  0.083 seconds of real time
  0.084005 seconds of total run time (0.084005 user, 0.000000 system)
  101.20% CPU
  166,793,649 processor cycles
  0 bytes consed
=> 5000000050000000

単純なループ処理なら、どれも速度は同じくらい。

比較2: 数値リストの奇数番目の要素の平均値を求める (zipを使ったループ)
;; データ準備: 1000万要素のリスト
(defparameter *list* (loop REPEAT 10000000 COLLECT (random 100000)))

;; loopパッケージ版
(defun avg1 (list)
  (declare #.*fastest* #.*note*)
  (flet ((average (sequence)  ; シーケンスの生成と平均値を求める処理を分離することが可能
           (let ((total 0)
                 (count 0))
             (declare (fixnum total count))
             (loop:each (lambda (n)
                          (incf total n)
                          (incf count))
                        sequence)
             (float (/ total count)))))

    (let ((seq (loop:map-n 2 (lambda (_ n) n)
                 (loop:filter-n 2 (lambda (i _) (oddp i))
                   (loop:zip (loop:from 0 :to most-positive-fixnum)
                             (loop:for-list list :element-type fixnum))))))
      (average seq))))

;; loopマクロ版
(defun avg2 (list)
  (declare #.*fastest* #.*note*)
  (loop WITH total fixnum = 0
        WITH count fixnum = 0
        FOR i fixnum FROM 0
        FOR n fixnum IN list
        WHEN (oddp i)
    DO
    (incf total n)
    (incf count)
    FINALLY
    (return (float (/ total count)))))

;; do版
(defun avg3 (list)
  (declare #.*fastest* #.*note*)
  (let ((total 0)
        (count 0))
    (declare (fixnum total count))
    (do ((i 0 (1+ i))
         (head list (cdr head)))
        ((endp head))
      (declare (fixnum i))
      (when (oddp i)
        (incf count)
        (incf total (the fixnum (car head)))))
    (float (/ total count))))

;; 実行
> (time (avg1 *list*))  ; loopパッケージ版: 0.084秒
Evaluation took:
  0.084 seconds of real time
  0.084005 seconds of total run time (0.084005 user, 0.000000 system)
  100.00% CPU
  166,739,958 processor cycles
  0 bytes consed
=> 50003.64

> (time (avg2 *list*))  ; loopマクロ版: 0.036秒
Evaluation took:
  0.036 seconds of real time
  0.036002 seconds of total run time (0.036002 user, 0.000000 system)
  100.00% CPU
  72,645,764 processor cycles
  0 bytes consed
=> 50003.64

> (time (avg3 *list*))  ; do版: 0.037秒
Evaluation took:
  0.037 seconds of real time
  0.040003 seconds of total run time (0.040003 user, 0.000000 system)
  108.11% CPU
  75,246,648 processor cycles
  0 bytes consed 
=> 50003.64

loopパッケージ版はzipを通すと、loopマクロやdoを使った場合の半分以下になってしまう。

比較3: 比較2での、平均値算出部分と奇数番要素のフィルタ部分を、別関数に分けた場合
;; loopマクロで、平均値算出部分と奇数番要素のフィルタ部分を、別関数に分けた場合
(declaim (inline average-list))
(defun average-list (list)
  (declare #.*fastest* #.*note*)
  (loop WITH total fixnum = 0
        WITH count fixnum = 0
        FOR n fixnum IN list
    DO
    (incf total n)
    (incf count)
    FINALLY
    (return (float (/ total count)))))

(declaim (inline filter-list))
(defun filter-list (list)
  (declare #.*fastest* #.*note*)
  (loop FOR i fixnum FROM 0
        FOR x IN list
        WHEN (oddp i)
        COLLECT x))

> (time (average-list (filter-list *list*)))
Evaluation took:
  0.122 seconds of real time              ; 全体で0.122秒、GC抜きなら0.081秒
  0.120008 seconds of total run time (0.120008 user, 0.000000 system)
  [ Run times consist of 0.040 seconds GC time, and 0.081 seconds non-GC time. ]
  98.36% CPU
  244,986,221 processor cycles
  79,986,688 bytes consed
=> 50003.64

;; do版
;; ※ loopマクロ版とほとんど変わらないので省略

;; loopパッケージで、平均値算出部分と奇数番要素のフィルタ部分を、別関数に分けた場合
(declaim (inline average-loop))
(defun average-loop (sequence)
  (declare #.*fastest* #.*note*)
  (let ((total 0)
        (count 0))
    (declare (fixnum total count))
    (loop:each (lambda (n)
                 (incf total (the fixnum n))
                 (incf count))
               sequence)
    (float (/ total count))))

(declaim (inline filter-loop))
(defun filter-loop (list)
  (declare #.*fastest* #.*note*)
  (loop:map-n 2 (lambda (_ n) n)
    (loop:filter-n 2 (lambda (i _) (oddp i))
      (loop:zip (loop:from 0 :to most-positive-fixnum)
                (loop:for-list list :element-type fixnum)))))

> (time (average-loop (filter-loop *list*)))
Evaluation took:
  0.070 seconds of real time   ; 0.070秒
  0.072005 seconds of total run time (0.072005 user, 0.000000 system)
  102.86% CPU
  139,856,631 processor cycles
  0 bytes consed
=> 50003.64

;; loopパッケージでinline宣言を外した場合
(declaim (notinline average-loop))
(declaim (notinline filter-loop))
> (time (average-loop (filter-loop *list*)))
Evaluation took:
  0.378 seconds of real time  ; 0.378秒
  0.376024 seconds of total run time (0.376024 user, 0.000000 system)
  99.47% CPU
  754,498,049 processor cycles
  0 bytes consed
=> 50003.64

loopマクロやdoマクロでは、ループ処理の一部を自然な形で効率よく外だしするのが困難なので、そういった用途ではloopパッケージの方が性能が良い。
ただし、現状のloopパッケージは、inline展開による最適化に過度に依存しているので、展開が効かないケースでは、いっきに処理速度が遅くなってしまう。

Gomokuの形態素解析部をScalaで実装してみた

ここ数日はScalaのコップ本を読んでいて、何かまとまったプログラムをScalaで書いてみたくなったのでGomoku(Java形態素解析器。ver0.0.6)Scalaで実装してみた*1
github: scala-gomoku(ver0.0.1)

以下、使用例とJava版/Scala版の簡単な比較メモ。

使用例

$ scala -cp scala-gomoku-0.0.1.jar

// インタプリタ起動 & パッケージインポート
scala> import net.reduls.scala.gomoku._

// 分かち書き
scala> Tagger.wakati("Scalaはオブジェクト指向言語と関数型言語の特徴を統合したマルチパラダイムのプログラミング言語である。")
res0: List[String] = 
        List(Scala, は, オブジェクト, 指向, 言語, と, 関数, 型, 言語, の, 特徴, を, 統合, し, た, マルチパラダイム, の, プログラミング, 言語, で, ある, 。)

// 形態素解析
scala> Tagger.parse("Scalaはオブジェクト指向言語と関数型言語の特徴を統合したマルチパラダイムのプログラミング言語である。")
res1: List[net.reduls.scala.gomoku.Morpheme] =
         List(Morpheme(Scala,名詞,固有名詞,組織,*,*,*,0), Morpheme(は,助詞,係助詞,*,*,*,*,5), Morpheme(オブジェクト,名詞,一般,*,*,*,*,6), Morpheme(指向,名詞,サ変接続,*,*,*,*,12), Morpheme(言語,名詞,一般,*,*,*,*,14), 
              Morpheme(と,助詞,並立助詞,*,*,*,*,16), Morpheme(関数,名詞,一般,*,*,*,*,17), Morpheme(型,名詞,接尾,一般,*,*,*,19), Morpheme(言語,名詞,一般,*,*,*,*,20), Morpheme(の,助詞,連体化,*,*,*,*,22), Morpheme(特徴,名詞,一般,*,*,*,*,23), 
              Morpheme(を,助詞,格助詞,一般,*,*,*,25), Morpheme(統合,名詞,サ変接続,*,*,*,*,26), Morpheme(し,動詞,自立,*,*,サ変・スル,連用形,28), Morpheme(た,助動詞,*,*,*,特殊・タ,基本形,29), Morpheme(マルチパラダイム,名詞,一般,*,*,*,*,30), 
              Morpheme(の,助詞,連体化,*,*,*,*,38), Morpheme(プログラミング,名詞,サ変接続,*,*,*,*,39), Morpheme(言語,名詞,一般,*,*,*,*,46), Morpheme(で,助動詞,*,*,*,特殊・ダ,連用形,48), Morpheme(ある,助動詞,*,*,*,五段・ラ行アル,基本形,49), Morpheme(。,記号,句点,*,*,*,*,51))

// 名詞のみ取り出し
scala> for(m <- res1 if m.feature.startsWith("名詞")) yield m.surface
res2: List[String] = 
        List(Scala, オブジェクト, 指向, 言語, 関数, 型, 言語, 特徴, 統合, マルチパラダイム, プログラミング, 言語)

ソースコード行数

形態素解析部のソースコードの行数比較。

Java:

$ cd gomoku-0.0.6-src
$ wc -l `find . -name '*.java'`
  117 ./analyzer/src/net/reduls/gomoku/Tagger.java
   12 ./analyzer/src/net/reduls/gomoku/Morpheme.java
   23 ./analyzer/src/net/reduls/gomoku/util/ReadLine.java
   83 ./analyzer/src/net/reduls/gomoku/util/Misc.java
   38 ./analyzer/src/net/reduls/gomoku/bin/Gomoku.java
   32 ./analyzer/src/net/reduls/gomoku/dic/Unknown.java
   72 ./analyzer/src/net/reduls/gomoku/dic/Char.java
   23 ./analyzer/src/net/reduls/gomoku/dic/WordDic.java
   61 ./analyzer/src/net/reduls/gomoku/dic/SurfaceId.java
   43 ./analyzer/src/net/reduls/gomoku/dic/Morpheme.java
   26 ./analyzer/src/net/reduls/gomoku/dic/PartsOfSpeech.java
   23 ./analyzer/src/net/reduls/gomoku/dic/ViterbiNode.java
   26 ./analyzer/src/net/reduls/gomoku/dic/Matrix.java
  579 合計

Scala:

$ cd scala-gomoku-0.0.1-src
$ wc -l `find . -name '*.scala'`
   3 ./src/net/reduls/scala/gomoku/Morpheme.scala
  27 ./src/net/reduls/scala/gomoku/bin/Gomoku.scala
  15 ./src/net/reduls/scala/gomoku/dic/Matrix.scala
  13 ./src/net/reduls/scala/gomoku/dic/PartsOfSpeech.scala
  18 ./src/net/reduls/scala/gomoku/dic/Morpheme.scala
  22 ./src/net/reduls/scala/gomoku/dic/Char.scala
  32 ./src/net/reduls/scala/gomoku/dic/Util.scala
   9 ./src/net/reduls/scala/gomoku/dic/ViterbiNode.scala
  39 ./src/net/reduls/scala/gomoku/dic/SurfaceId.scala
  30 ./src/net/reduls/scala/gomoku/dic/Unknown.scala
  15 ./src/net/reduls/scala/gomoku/dic/WordDic.scala
  56 ./src/net/reduls/scala/gomoku/Tagger.scala
 279 合計

Scala版はJava版に対して、おおよそ半分程度の行数。

処理速度

以下のようなベンチマークスクリプトを書いて、両者の処理速度を比較してみた。

// ファイル名: Benchmark.scala

import scala.testing.Benchmark
import net.reduls.scala.gomoku.{Tagger=>ScalaTagger}
import net.reduls.gomoku.{Tagger=>JavaTagger}
import scala.io.Source

// ベンチマーク用データ: 使用したのは約17MBの日本語テキストデータ
object BenchmarkData {
  val lines = Source.fromFile("/path/to/testdata").getLines.toArray
}

// Scala用のベンチマークオブジェクト
object ScalaGomokuBenchmark extends Benchmark {
  // BenchmarkData.linesの各行を分かち書き
  override def run() { BenchmarkData.lines.foreach(ScalaTagger.wakati _) } 
}

// Scala用のベンチマークオブジェクト
object JavaGomokuBenchmark extends Benchmark {
  override def run() { BenchmarkData.lines.foreach(JavaTagger.wakati _) }
}

// ベンチマーク実行
println("[Data]")
println("  lines: " + BenchmarkData.lines.length)
println("")

val scalaRlt = ScalaGomokuBenchmark.runBenchmark(11).tail
println("[Scala]")
println("  result : " + scalaRlt.mkString(", "))
println("  average: " + (scalaRlt.sum / scalaRlt.length))
println("")

val javaRlt = JavaGomokuBenchmark.runBenchmark(11).tail
println("[Java]")
println("  result : " + javaRlt.mkString(", "))
println("  average: " + (javaRlt.sum / javaRlt.length))
println("")

実行結果:

# Scala: version 2.9.0.1 (OpenJDK 64-Bit Server VM, Java 1.6.0_23).
$ scala -cp scala-gomoku-0.0.1.jar:gomoku-0.0.6.jar Benchmark.scala
[Data]
  lines: 172088  # データの行数(約17万行)

[Scala]
  result : 4529, 4574, 4568, 4540, 4503, 4510, 4523, 4515, 4551, 4531  
  average: 4534  # 平均: 4.534秒

[Java]
  result : 3153, 3111, 3118, 3112, 3102, 3098, 3118, 3130, 3117, 3133
  average: 3119  # 平均: 3.119秒

自分の環境では、Scala版はJava版よりも1.5倍程度遅かった。
※ まだScalaでの効率の良い書き方とかが全然分かっていないので、その辺りを踏まえてちゃんと最適化を行えばもっと差は縮まるかもしれない


書きやすさはScalaの方が全然上だけど、(今回のケースでは)まだJavaに比べると若干遅い感じはする。

*1:形態素解析部のみ、バイナリ辞書データ構築部は未実装

erl-creole: ユニコード文字列とマルチバイト列の変換ライブラリ

少し必要になったのでErlangユニコード文字列と各種エンコーディングのマルチバイト列(バイナリ)の相互変換を行うモジュールを作成した。
github: erl-creole-0.0.1


現状、対応しているエンコーディングは以下の通り*1:

使用例

%% 入力文字列
> S = "Unicode (ユニコード) とは、世界中の多くのコンピュータ上の文字列を一貫した方法で符号化し、表現し、扱うためのコンピュータ業界の標準である。".

%% EUC-JPに変換
> creole:from_string(S, eucjp).
<<"Unicode (\&#230;\&#203;\&#179;&#161;&#188;\&#201;) &#164;&#200;&#164;&#207;&#161;¢&#192;&#164;&#179;釗&#195;&#230;&#164;&#206;&#194;&#191;&#164;&#175;&#164;&#206;\&#179;\&#243;\&#212;\&#229;&#161;&#188;\&#191;&#190;&#229;&#164;&#206;&#202;&#184;&#187;&#250;&#206;&#243;&#164;&#242;°&#236;´&#211;&#164;&#183;&#164;&#191;&#202;&#253;&#203;&#161;&#164;&#199;&#201;&#228;&#185;&#230;&#178;&#189;&#164;&#183;&#161;¢&#201;&#189;&#184;&#189;&#164;&#183;&#161;¢°&#183;&#164;釗&#164;&#191;&#164;&#225;&#164;&#206;\&#179;\&#243;\&#212;\&#229;&#161;"...>>

%% JIS(ISO-2022-JP)に変換
> Bin = creole:from_string(S, jis).
<<"Unicode (\e$B%f%K%3!<%I\e(B) \e$B$H$O!\"@$3&Cf$NB?$/$N%3%s%T%e!<%?>e$NJ8;zNs$r0l4S$7$?J}K!$GId9f2=$7!\"I=8=$7!\"07$&$?$a$N"...>>

%% バイト列からユニコード文字列に変換
> creole:to_string(Bin, jis).
[85,110,105,99,111,100,101,32,40,12518,12491,12467,12540,
 12489,41,32,12392,12399,12289,19990,30028,20013,12398,22810,
 12367,12398,12467,12531,12500|...]

> io:format("~ts~n", [creole:to_string(Bin, jis)]).
Unicode (ユニコード) とは、世界中の多くのコンピュータ上の文字列を一貫した方法で符号化し、表現し、扱うためのコンピュータ業界の標準である。
ok

%% 変換不能なバイト列がある場合は、デフォルトでは "?" が代わりに使用される
> io:format("~ts~n", [creole:to_string(<<"Unicode (\e$B%~^s^sjaf*(asf7aK%3!<%I">>, jis)]).
Unicode (??潁潁裃罟?癈羞疔コード
ok

%% "?"の代わりに"_"を使用
> io:format("~ts~n", [creole:to_string(<<"Unicode (\e$B%~^s^sjaf*(asf7aK%3!<%I">>, jis, creole:replace($_))]).
Unicode (__潁潁裃罟_癈羞疔コード
ok

*1:ユニコードと他のエンコーディングのコードポイントの対応は主に http://source.icu-project.org/repos/icu/data/trunk/charset/data/ucm/ を参考にさせてもらった。

文字列/バイト列用のハッシュ関数ライブラリ

A Hash Function for Hash Table Lookupに載っているハッシュ関数(Jenkins Hash)Common Lispで実装した。
github: jenkins-hash(0.0.2)


作成の主な動機は以下の二つ:

おそらくSBCL以外の処理系でも動くと思うけど、動作は未確認。

使用例

以下、使用例。

;;;; SBCL-1.0.51-64bit

;;; 【文字列】
;; 文字列からハッシュ値を算出
(jenkins-hash:hash-string "ハッシュ関数")
--> 3188986421   ; 一つのキーに対して二つのハッシュ値(32bit)を返す
    2167986557

;; パラメータ(seed1,seed2)を替えて別のハッシュ値を算出
(jenkins-hash:hash-string "ハッシュ関数" :seed1 13 :seed2 44444)
--> 2402597428
    3323692532

;; 範囲指定
(jenkins-hash:hash-string "ハッシュ関数" :start 2 :end 4)
--> 58741211
    888923469

;;; 【バイト列】
;; バイト列からハッシュ値を算出
(jenkins-hash:hash-octets (sb-ext:string-to-octets "ハッシュ関数"))
--> 1523938354
    936250363

;; sxhash関数だと配列を与えた場合、全て同じハッシュ値になってしまう
(sxhash (sb-ext:string-to-octets "ハッシュ関数"))
--> 518591303

(sxhash (sb-ext:string-to-octets "別の値"))
--> 518591303

;;; 【複数のハッシュ値】
;; nth-hash関数を使って、任意個のハッシュ値を安価に生成可能
;; ※ 内部的にはDoubleHashingを用いて生成している => 可算一つと乗算一つで算出可能

;; 一つのキーに対する10個の異なるハッシュ値を取得する
(defun hash10 (key)
  (multiple-value-bind (h1 h2) (jenkins-hash:hash-string key)
    ;; 最初の二つはそのまま使って、残りはnth-hash関数で生成する
    `(,h1 ,h2 . ,(loop FOR i FROM 2 BELOW 10 COLLECT (jenkins-hash:nth-hash i h1 h2)))))
       
(hash10 "ハッシュ関数")
--> (3188986421 2167986557 3229992239 1103011500 3270998057 1144017318 3312003875
     1185023136 3353009693 1226028954)

*1:sxhash関数を配列に適用すると常に同じ値が返ってきてしまう。sbcl-1.0.51-64bit

cc-dict: ハッシュテーブル

ハッシュテーブルを実装してみた。
cc-dict-0.0.3
チェイン法を用いたハッシュテーブルで、リンクリスト(チェイン)のノードを割り当てる際に自前のアロケータを使っている点以外は、特に変わったところもないと思う。

ベンチマーク

一応、ベンチマークも取ったので載せておく。
比較対象はg++のunordered_mapとGoogleのsparse_hash_map及びdense_hash_map。
ベンチマークプログラムにはHash Table Benchmarksに記載のものを使用させてもらった*1
※ 実行環境は Ubuntu-11.11-64bit / Intel(R) Core(TM) i7 CPU L 620 @ 2.00GHz。

このベンチマーク結果を見る限りは、特別性能が悪い、ということはなさそう*2

*1:ただし以下の追加・修正を施した。
 1: 検索処理時間の計測の追加
 2: もともとは文字列のベンチマークではキーの型const char*が使用されていたのをstd::stringに変更(関連)
 3: 処理時間にキー配列を生成する時間も含まれていたので、キー配列は最初にまとめて生成しておき、その後から計時を開始するように修正

*2:ベンチマークデータが結構恣意的(連番 or 乱数)なので、必ずしも実データで同様の性能がでるかは分からないけど・・・

Sanmoku(0.0.5): 原型や読みの情報取得に対応

Sanmoku(0.0.4): 辞書データサイズ縮小のコメントにて要望があったのでSanmoku形態素の原型や読みの情報取得に対応させてみた。
Sanmoku本体のインターフェースは以前の同様*1で、原型・読み・発音の取得を行うためのFeatureExクラス(sanmoku-feature-ex-x.x.x.jar)を新しく追加した。

使用例:

import net.reduls.sanmoku.Tagger;
import net.reduls.sanmoku.Morpheme;
import net.reduls.sanmoku.FeatureEx; // 追加

for(Morpheme m : Tagger.parse("...")) {  // 形態素解析
  FeatureEx fe = new FeatureEx(m);       // 解析結果の形態素インスタンスから、追加の素性データを取得

  // 結果表示
  System.out.println(m.surface+"\t"+m.feature+","+fe.baseform+","+fe.reading+","+fe.pronunciation);
}
$ export CLASSPATH=sanmoku-0.0.5.jar:sanmoku-feature-ex-0.0.1.jar
$ echo '招待制でリリースしました' | java net.reduls.sanmoku.bin.SanmokuFeatureEx
招待	名詞,サ変接続,*,*,*,*,招待,ショウタイ,ショータイ
制	名詞,接尾,一般,*,*,*,制,セイ,セイ
で	助詞,格助詞,一般,*,*,*,で,デ,デ
リリース	名詞,サ変接続,*,*,*,*,リリース,リリース,リリース
し	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
まし	助動詞,*,*,*,特殊・マス,連用形,ます,マシ,マシ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS

比較

以前の表にSanmoku-0.0.5(+ FeatureEx-0.0.1)を追加したもの。

  辞書データサイズ(IPADIC) 最小所要メモリ(-Xmx) 起動(≒辞書ロード)時間*2 10MBテキストの解析時間
Igo-0.4.3 40MB 73MB 0.058秒 2.729秒
Gomoku-0.0.4 8.2MB 23MB 0.371秒 2.621秒
Sanmoku-0.0.4 4.8MB 2MB 0.057秒 5.807秒
Sanmoku-0.0.5(+ FeatureEx-0.0.1) 10MB 11MB 0.098秒 6.814秒

読み等の情報を取得した場合、性能は全体的に劣化しているけど、これくらいなら十分許容範囲内のような気がする。

*1:内部的にはMorphemeクラスが形態素IDを保持するように修正されている

*2:JVM自体の起動時間は除いた数値