はじめに
先日、SourceGenerator
を利用したパフォーマンスの良いEnumユーティリティを提供するライブラリを公開しました。
github.com
.NET標準API や 世界最速のenumライブラリ FastEnum よりもパフォーマンスが良いライブラリ RapidEnum リリースしました!!
— はなちる@ゲーム制作 (@hanaaaaaachiru) August 18, 2024
手元で計測したところ、.NET APIより数十~数万倍良い計測結果がでてました
Unity Package Managerにも対応してるので、Unity勢も手軽に導入できますhttps://t.co/8miqpRUo0r pic.twitter.com/FOztfAYD5f
記事の後半で詳しく紹介しますが、パフォーマンスが良いと知られていたFastEnum
よりも高速に動作しゼロアロケーションを達成しています。
GitHub - xin9le/FastEnum: The world fastest enum utilities for C#/.NET
今回はこのライブラリの実装についてツラツラと書いていきたいと思います。
背景
まず前提として、.NETに用意されているAPIは遅いです。
public enum Weather { Sun, Cloud, Rain, Snow }
// Sun,Cloud,Rain,Snow Weather[] values = Enum.GetValues<Weather>(); // Sun,Cloud,Rain,Snow string[] names = Enum.GetNames<Weather>(); // Rain string? name = Enum.GetName(Weather.Rain); // Cloud string toString = Weather.Cloud.ToString(); // True bool defined = Enum.IsDefined(Weather.Sun); // Sun Weather parse = Enum.Parse<Weather>("Sun"); // True // Sun bool tryParse = Enum.TryParse<Weather>("Sun", out Weather value);
対してxin9le
さんのFastEnum
では、Static Type Caching
という手法を用いてキャッシュを行うことでパフォーマンス向上を狙っています。
// Sun,Cloud,Rain,Snow IReadOnlyList<Weather> values = FastEnum.GetValues<Weather>(); // Sun,Cloud,Rain,Snow IReadOnlyList<string> names = FastEnum.GetNames<Weather>(); // Rain string? name = FastEnum.GetName(Weather.Rain); // Cloud string toString = Weather.Cloud.FastToString(); // True bool defined = FastEnum.IsDefined(Weather.Sun); // Sun Weather parse = FastEnum.Parse<Weather>("Sun"); // True // Sun bool tryParse = FastEnum.TryParse<Weather>("Sun", out Weather value);
github.com
www.hanachiru-blog.com
以下はFastEnumのReadmeに記載されているパフォーマンス比較で、かなり処理速度が向上していることが伺えます。またゼロアロケーションです。
一応補足しておきますが、APIの返り値の型が完全に一致しているわけではないですし、Static Type Caching
の性質上一度メモリ上に確保されてしまうとアプリケーションを終了するまで占有しつづけてしまうといったデメリットも存在することはご留意ください。
ただ実用する上ではFastEnum
を使ったとしてもそこまで問題にならないケースが殆どだと思いますし、積極的にFastEnum
を利用していくと良いと思います。
RapidEnum
私が今回作成したRapidEnum
はIncremental SourceGenerator
を用いてコンパイルのタイミングでenum
の解析を行い、コード生成を行います。
// [RapidEnum]という属性をpublic or internalなenumに付与してあげると... [RapidEnum] public enum Weather { Sun, Cloud, Rain, Snow } // ↓のコードがコンパイルのタイミングで自動生成される public static partial class WeatherEnumExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ToStringFast(this global::Sample.Weather value) { return value switch { Sample.Weather.Sun => nameof(Sample.Weather.Sun), Sample.Weather.Cloud => nameof(Sample.Weather.Cloud), Sample.Weather.Rain => nameof(Sample.Weather.Rain), Sample.Weather.Snow => nameof(Sample.Weather.Snow), _ => value.ToString() }; } private static readonly ReadOnlyCollection<global::Sample.Weather> CacheValues = new ReadOnlyCollection<global::Sample.Weather>(new[] { Sample.Weather.Sun, Sample.Weather.Cloud, Sample.Weather.Rain, Sample.Weather.Snow, }); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IReadOnlyList<global::Sample.Weather> GetValues() => CacheValues; ... }
// 利用する時は生成されたコードを利用する // Sun,Cloud,Rain,Snow IReadOnlyList<Weather> values = WeatherEnumExtensions.GetValues();
生成されたコードを見ると高速な理由が分かると思いますが、ただのswitch
があるだけです。あとはReadOnlyCollection
をキャッシュしておいて、それを返すだけ。めっちゃシンプルです。
これは今までランタイムでenum
の解析を行っていたのを、コンパイルのタイミングに解析のタイミングをずらしたことによって得られた恩恵です。
利用する側に定義をする
enum
に[RapidEnum]
を利用すると定義側にユーティリティを作成するのに対し、[RapidEnumWithType]
を用いることで利用側にユーティリティを定義することができます。これによって、サードパーティ製のライブラリに定義されているenum
などの任意のenum
に対して利用可能です。
// System.DateTimeKind has Unspecified, Utc, Local // public static partial class or internal static partial classのどちらかに[RapidEnumWithType(対象enumの型)]を付与する [RapidEnumWithType(typeof(DateTimeKind))] public static partial class DateTimeKindEnumExtensions { }
// Unspecified,Utc,Local IReadOnlyList<DateTimeKind> values = DateTimeKindEnumExtensions.GetValues();
[RapidEnum]
と[RapidEnumWithType]
の違いは、定義側・利用者側のどちらにユーティリティを定義するかの違いです。パフォーマンスに差があるわけではありません。
結果
.NET APIよりも約十倍 ~ 数万倍
パフォーマンスが良いという結果が得られました。またFastEnum
より高速です。
考察
パフォーマンスが良いと書きましたが、RapidEnum
には色々と制約があるので一概に比較できるものではないことは補足させてください。
まずFastEnum
と異なり、public
かinternal
のenum
にしか適応できないです。というのもあくまでISGによりユーティリティクラスが生えてくるので、そのクラスから利用できない型にはアクセスできないのが理由です。
また属性をつけないといけないめんどくささがあるのと、生成されるクラス名([RapidEnumWithType]
は自分で設定したクラス名が適応)が分かりづらいという点もつらいです。
// クラス名は一応自由に設定できる [RapidEnumWithType(typeof(DateTimeKind))] public static partial class HogeHoge { }
// Unspecified,Utc,Local IReadOnlyList<DateTimeKind> values = HogeHoge.GetValues();
今後の展望
実際に使うとやはり属性を付与するのがめんどくさいですよね...。
SourceGeneratorなので一定仕方がない面があるのですが、CySharpさんのConsoleAppFramework
はメソッドのシグネチャを見てコード生成を行う大胆な手法を取り入れています。
neue.cc
つまりメソッドが定義されているからそれを呼ぶのではなく、メソッドを利用しようとするとそのメソッドが生えてくるという世界観です。
// Genericを見てコード生成を行う案 public enum Hoge { A, B } _ = RapidEnum<Hoge>.Hoge.GetNames(); // ↓が生成される public static partial class RapidEnum<T> { public static partial class Hoge { public static IReadOnlyList<string> GetNames() { // 処理を行う } } }
generic
を利用するのでバイナリサイズに気をつけないといけないのは勿論ですが、Nested Classes
を利用することでパフォーマンスを保ちます。
一応static type caching
を利用すれば一つのclass
に集約できそうな気もしているのですが、うまくいけるかは自信がないです...(チャレンジしてみたいですが)
構想自体はあるのですが、結局はインテリセンスが快適に動作するのかあたりが焦点だと思います。また勝手にコード生成されるのは如何なものかという意見もあるのは重々承知してます。(それは絶対やめておけみたいな意見もちょこちょこ聞いています)
ひとまずR&D的な意味合いで、experimental機能として実装してみたいという温度感ですね。