kubell Creator's Note

ビジネスチャット「Chatwork」のエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

ULID生成器をScalaで実装してみた

おはこんにちは、かとじゅん(@j5ik2o)です。 今回の記事は、IDフォーマットの一種であるULIDの実装についての記事です。

ULIDよーわからんという人は、以下の僕の記事を参照してみてください。 zenn.dev

ID生成をどうするか議論によくなりますが、最近はソート可能なUUIDとしてULIDが話題にあがります。128ビットでかつ文字列型のキーが許容できるのであれば、ULIDはよい選択肢になりそうです。

既存のScala実装

実際Scalaで使おうと思うと以下が代表的な選択肢になると思います。

既存実装の問題点

つい最近までhuxi/sulkyの実装を使っていたのですが、やっぱ使いにくいなと思っています。

理由としては、

  • ULID型が実質生成器でID型ではない。ID型はValueクラスになっている
  • ULIDの乱数部の生成の効率が悪い
    • Random#nextLongを2回コールしていて、上位48ビットの乱数生成が無駄です。コードにもコメントが書かれていますが、Random#nextBytes(2)などとして必要最低限の乱数部を生成したほうが効率的です。
    • sulky/ULID.java at master · huxi/sulky · GitHub

wvlet/airframe-controlのULIDも検討したのですが、Randdom#nextDoubleの呼び出し回数が多い(1IDあたり16回呼ばれてしまうようです)というのと、ULIDだけ欲しかったので独自実装を作りました。

というわけで、以下が新たに実装したULID型です。特徴はjava.util.UUIDと同じぐらいは速いというところです。ボトルネックになりやすい乱数部の生成で、Random#nextBytesが1度しかコールされないようになっています。ULIDの文字列表現だけではなく、バイナリ表現もサポートしました。

github.com

クラスはULIDクラスの1個しかありません。

scala-ulid/ULID.scala at main · chatwork/scala-ulid · GitHub

ベンチマーク結果

jmhを使って以下3つのライブラリでのULID生成のベンチマークを比較してみました1

実際の使い方に合わせて、ULID.generate().asStringのようにULID型を生成した後に文字列に変換するベンチマークを取りました。実際のベンチマークのコードはこちら参照。

レイテンシは速いことに越したことはないですが、いずれの実装でも大量にデータを登録するなどの要求がない限り、レイテンシが問題になることはほぼないと思います。

-airframesulkychatwork
乱数生成Random#nextDoubleを16回コールRandom#nextLongを2回コール(上位48ビット無駄な乱数生成)Random#nextBytesを1回のみコール
Latency 95%tile(nsec)1038
(文字列版=1024nsec)
524460
Latency Max(msec)1.329
(文字列版=0.755msec)
0.7210.790
  • airframeの文字列版とはULID#newULIDStringのことです。文字列表現のULID値を生成するメソッドです。
  • java.util.UUID#randomUUIDは95%tile = 327nsec, max = 0.703msec。速い…。

まとめ

今回はScala用途でULIDだけがほしかったので独自実装を作りました。ULIDを使いたいときに思い出していただければ幸いです。


  1. jmhはsbt-jmhを使用しました。起動コマンドは → sbt ";clean ;benchmark/jmh:compile ;benchmark/jmh:run -i 5 -wi 5 -f1 -t1 $@"