おはこんにちは、かとじゅん(@j5ik2o)です。 今回の記事は、IDフォーマットの一種であるULIDの実装についての記事です。
ULIDよーわからんという人は、以下の僕の記事を参照してみてください。 zenn.dev
ID生成をどうするか議論によくなりますが、最近はソート可能なUUIDとしてULIDが話題にあがります。128ビットでかつ文字列型のキーが許容できるのであれば、ULIDはよい選択肢になりそうです。
既存のScala実装
実際Scalaで使おうと思うと以下が代表的な選択肢になると思います。
- Scalaで実装されたものを使う
- Javaで実装されたものを使う
既存実装の問題点
つい最近までhuxi/sulky
の実装を使っていたのですが、やっぱ使いにくいなと思っています。
理由としては、
- ULID型が実質生成器でID型ではない。ID型はValueクラスになっている
- ULIDは生成器のことでで、Value型がULID値を意味していてわかりにくい
- sulky/ULID.java at master · huxi/sulky · GitHub
- 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の文字列表現だけではなく、バイナリ表現もサポートしました。
クラスはULIDクラスの1個しかありません。
scala-ulid/ULID.scala at main · chatwork/scala-ulid · GitHub
ベンチマーク結果
jmhを使って以下3つのライブラリでのULID生成のベンチマークを比較してみました1。
実際の使い方に合わせて、ULID.generate().asString
のようにULID型を生成した後に文字列に変換するベンチマークを取りました。実際のベンチマークのコードはこちら参照。
レイテンシは速いことに越したことはないですが、いずれの実装でも大量にデータを登録するなどの要求がない限り、レイテンシが問題になることはほぼないと思います。
- | airframe | sulky | chatwork |
---|---|---|---|
乱数生成 | Random#nextDoubleを16回コール | Random#nextLongを2回コール(上位48ビット無駄な乱数生成) | Random#nextBytesを1回のみコール |
Latency 95%tile(nsec) | 1038 (文字列版=1024nsec) | 524 | 460 |
Latency Max(msec) | 1.329 (文字列版=0.755msec) | 0.721 | 0.790 |
airframe
の文字列版とはULID#newULIDString
のことです。文字列表現のULID値を生成するメソッドです。java.util.UUID#randomUUID
は95%tile = 327nsec, max = 0.703msec。速い…。
まとめ
今回はScala用途でULIDだけがほしかったので独自実装を作りました。ULIDを使いたいときに思い出していただければ幸いです。
-
jmhはsbt-jmhを使用しました。起動コマンドは →
sbt ";clean ;benchmark/jmh:compile ;benchmark/jmh:run -i 5 -wi 5 -f1 -t1 $@"
↩