xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

FastEnum - 世界最速の enum ライブラリ

というのを、この数日をかけて勢いで作りました。ずーーっと昔から「enum は遅い」と言われ続けていたので何か手を入れたいと思っていたのですが、突然やる気になりました。久々にプライベートのコーディング意欲が爆上げしたー。勢いが強過ぎて毎日寝不足のまま明け方までのコーディングを繰り返し、しかも家庭を顧みませんでした...(超ヨクナイ

どのくらい速いかというと、.NET Core 3.0 よりも何倍も速いです。もう狂ったくらい速い。全てのメソッドがゼロ・アロケーションです。プログラミングする上で enum はかなり使うので、多少なり性能改善に寄与できるのではないかと思います。ぜひ使ってみた感想をいただければなーと思います :)

f:id:xin9le:20190906201828p:plain

簡単に使い方解説

.NET 標準の System.Enum に近い使い方ができるように API を設計しています。以下のような感じで、とてもシンプル!

//--- FastEnum
var values = FastEnum<Fruits>.Values;
var names = FastEnum<Fruits>.Names;
var name = Fruits.Apple.ToName();
var defined = FastEnum<Fruits>.IsDefined(123);
var parse = FastEnum<Fruits>.Parse("Apple");
var tryParse = FastEnum<Fruits>.TryParse("Apple", out var value);
//--- .NET
var values = Enum.GetValues(typeof(Fruits)) as Fruits[];
var names = Enum.GetNames(typeof(Fruits));
var name = Enum.GetName(typeof(Fruits), Fruits.Apple);
var defined = Enum.IsDefined(typeof(Fruits), 123);
var parse = Enum.Parse<Fruits>("Apple");
var tryParse = Enum.TryParse<Fruits>("Apple", out var value);

おまけ機能

enum 周りでよく使う機能があって、それらも一緒に組み込むことでより便利に使えるようにしています。

1. メンバー情報を丸ッと取得

たまに enum の値と名前のペアが欲しくなることがあります。そういうケースに耐えるために Member<TEnum> というのを用意しています。FieldInfo なども一緒に入っているので、リフレクションなどのお供にご利用ください。

class Member<TEnum>
{
    public TEnum Value { get; }
    public string Name { get; }
    public FieldInfo FieldInfo { get; }
    // etc...
}

//--- こんな感じで取得できるので、煮るなり焼くなり
var member = Fruits.Apple.ToMember();

2. EnumMemberAttribute.Value の取得

EnumMemberAttribute をフィールド名の別名として利用している方をちょくちょく見かけます。このケースに対応するため、Value プロパティからサッと値を取得できるようにしました。

enum Company
{
    [EnumMember(Value = "Apple, Inc.")]
    Apple = 0,
}

var value = Company.Apple.GetEnumMemberValue();  // Apple, Inc.

3. 複数のラベルを付与

EnumMemberAttributeAllowMultiple = false になっているため、同一のフィールドに対して複数の属性を付けられません。それが不便で個人的には好んでおらず、その代替として LabelAttribute というのを作って使っています。FastEnum にこの機能を追加することで、以下のような感じで便利に利用できます。

enum Company
{
    [Label("Apple, Inc.")]
    [Label("AAPL", 1)]
    Apple = 0,
}

var x1 = Company.Apple.GetLabel();   // Apple, Inc.
var x2 = Company.Apple.GetLabel(1);  // AAPL

制限

System.Enum に対して完全に互換があるかというと、実はそうではないです。標準で提供されているいくつかの機能を削ることで速さを得ている部分があります。

1. Generics な API のみを提供

標準の System.Enumtypeof(TEnum) を用いたオーバーロードが用意されているのですが、その場合、引数か戻り値が object になります。そこで box 化が走って非常に遅くなってしまうので、機能として削ることで速度を稼ぐようにしています。

2. カンマ区切りの文字列を Parse できない

実は System.Enum.Parse は以下のような感じの文字も parse できます。あまり知られていないんじゃないかと思うくらい、ひっそりと存在する仕様です。

//--- こんなのがあるとして
[Flags]
enum Fruits
{
    Apple = 1,
    Lemon = 2,
    Melon = 4,
    Banana = 8,
}

//--- カンマ区切りの文字列を与える
var value = Enum.Parse<Fruits>("Apple, Melon");
Console.WriteLine((int)value);  // 5

フラグ処理をするときに便利な機能のようですが、これまで 10 年以上一度も使ったことがありませんでした。このカンマ区切りの解析を加えようとするとそのオーバーヘッドが出てしまうので、思い切って機能を削ることで速度を稼いでいます。ほとんど使われない (少なくとも自分は使わない) 機能のために速度を落とすのが嫌だったのでそうしています。そのあたりは本家に譲ればいいかな、と...。

速さの秘訣

言わずもがなでお察しかもしれませんが、内部でキャッシュしているからです。Static Type Caching というアプローチを取っていて、そのおかげで読み込み速度が限りなく 0 になっています*1。これを基礎としつつ、アロケーション回避のためのテクニックを交えたり、内部で利用する辞書を特定のキーに特化させるなどしています*2

このあたりのテクニックはのいえ先生 (@neuecc) の講演スライドCysharp 印の GitHub リポジトリが大変参考になります。

*1:BenchmarkDotNet にも「計測不可能」と言われる速度

*2:保守性の観点では好ましくないけれど、速度を稼ぐためにやむを得ず...