はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】並べ替え可能なランダムIDジェネレーターである「Cysharp製Ulid」の利用法(Guid.NewGuidより高速,生成時刻によるソート可)

はじめに

今回はCysharp/Ulidという.NET CoreUnityのためのランダムIDジェネレーターを紹介したいと思います。

Fast C# Implementation of ULID for .NET Core and Unity. Ulid is sortable, random id generator. This project aims performance by fastest binary serializer(MessagePack-CSharp) technology. It achives faster generate than Guid.NewGuid.

// DeepL翻訳
.NET CoreとUnityのためのULIDの高速C#実装。ULIDはソート可能なランダムIDジェネレータです。このプロジェクトでは、最速のバイナリー・シリアライザー(MessagePack-CSharp)技術によるパフォーマンスを狙っています。Guid.NewGuidよりも高速に生成することができます。

https://github.com/neuecc/MessagePack-CSharp

ulidの仕様については以下のリポジトリに記載されています。
github.com

これをC#での利用を念頭に、パフォーマンスに気をつけながら作成されたライブラリが今回紹介するCysharp/Ulidです(と私は解釈しています)。
github.com

また他にもC#ライブラリは存在します。
GitHub - mcb2001/CSharp.Ulid: Universally Unique Lexicographically Sortable Identifier

UUIDとUlid

皆さんいつも使っているGuid.NewGuidUUIDの生成になります。

UUID(Universally Unique Identifier)とは、ソフトウェア上でオブジェクトを一意に識別するための識別子である。UUIDは128ビットの数値だが、16進法による550e8400-e29b-41d4-a716-446655440000というような文字列による表現が使われることが多い。元来は分散システム上で統制なしに作成できる識別子として設計されており、したがって将来にわたって重複や偶然の一致が起こらないという前提で用いることができる。マイクロソフトによるGUIDは、UUIDの実装の1つと見なせる。

UUID - Wikipedia

ULIDも方向性としては同じですが、ミリ秒単位で時系列ソートできるというメリットがあります。

  • 128-bit compatibility with UUID
  • 1.21e+24 unique ULIDs per millisecond
  • Lexicographically sortable
  • Canonically encoded as a 26 character string, as opposed to the 36 character UUID
  • Uses Crockford's base32 for better efficiency and readability (5 bits per character)
  • Case insensitive
  • No special characters (URL safe)
  • Monotonic sort order (correctly detects and handles the same millisecond)
  • UUIDと128ビットの互換性
  • 1.21e+24個のユニークなULIDを1ミリ秒間に生成可能
  • レキシログラフィックにソート可能
  • 36文字のUUIDに対して、26文字の文字列として正規にエンコードされている
  • 効率性と可読性を高めるためにCrockfordのbase32を使用(1文字あたり5ビット)
  • 大文字と小文字を区別しない
  • 特殊文字なし(URLセーフ)
  • 単調なソート順(同じミリ秒を正しく検出し処理する)

https://github.com/ulid/spec

Guid.NewGuid(UUID)と比較して今回のライブラリを導入することで以下のメリットが得られます。

  • 生成時刻によるソート可
  • 高速な生成可

どうやって生成時刻によるソートができているのかというと、TimestampRandomnessに分かれて構成されているからですね。

01AN4Z07BY 79KA1307SR9X4MV3
Timestamp Randomness
48bits 80bits

Timestamp

  • 48 bit integer
  • UNIX-time in milliseconds
  • Won't run out of space 'til the year 10889 AD.

Randomness

  • 80 bits
  • Cryptographically secure source of randomness, if possible

GitHub - ulid/spec: The canonical spec for ulid

TimeStampmsであることから分かるように、同一ミリ秒だと順番が保証されませんので注意です。

Within the same millisecond, sort order is not guaranteed

そこまでデメリットはないとは思うのですが、ユーザーにULIDから生成時刻がバレちゃうとかはデメリットちゃデメリットなんですかね。

導入

Releaseページから最新のunitypackageをダウンロード&インポートしてください。
github.com

依存先として以下がありますが、unitypackageに同梱されています。

  • System.Buffers.dll
  • System.Memory.dll
  • System.Runtime.CompilerServices.Unsafe.dll

使い方

  • Ulid.NewUlid()
  • Ulid.Parse()
  • Ulid.TryParse()
  • new Ulid()
  • .ToString()
  • .ToByteArray()
  • .TryWriteBytes()
  • .TryWriteStringify()
  • .ToBase64()
  • .Time
  • .Random
// 生成 : Ulid.NewUlid vs Guid.NewGuid
Ulid ulid = Ulid.NewUlid();
Guid guid = Guid.NewGuid();
        
// ToString : Ulid.ToString vs Guid.ToString
string ulidText = ulid.ToString();                  // 01GNE8CEWWNZNK23JBCN197DM1
string guidText = guid.ToString();                  // 8ec8aaca-ab96-4a0a-8f47-b77302567a6b

// Parse : Ulid.Parse(string) vs Guid.Parse(string)
Ulid ulid2 = Ulid.Parse(ulidText);
Guid guid2 = Guid.Parse(guidText);
        
// GetHashCode : Ulid.GetHashCode vs Guid.GetHashCode
_ = ulid.GetHashCode();
_ = guid.GetHashCode();
        
// Equal : Ulid.Equals vs Guid.Equals
Debug.Log(ulid.Equals(ulid2));                  // True
Debug.Log(guid.Equals(guid2));                  // True
        
// CompareTo : Ulid.CompareTo vs Guid.CompareTo
Debug.Log(ulid.CompareTo(Ulid.NewUlid()));      // -1 (後に生成された方が大きい, CompareToのイメージは「左-右」)
Debug.Log(guid.CompareTo(Guid.NewGuid()));      // ??? (一意でない)

Debug.Log(ulid.Time);                           // 12/29/2022 06:16:19 +00:00
Debug.Log(BitConverter.ToString(ulid.Random));  // AF-EB-31-0E-4B-65-42-93-B6-81

パフォーマンス

github.com

メモリアロケーション・処理速度共に全体的に性能が向上しているようです。

GetHashCodeEqualsはちょいGUIDに処理速度で負けているようですが、大した差ではないと思います。
それ以上にメリットの方が遥かに大きいような。