xin9le.net

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

FastEnum - 更なる高速化を果たした v1.1.0 リリース

先の記事FastEnum という超高速な Enum Utility ライブラリを作ってリリースしてから 3 日。v1.1.0 をリリースしました。パフォーマンスはさらに磨きがかかっていて、他の追随を許さないレベルに昇華されました。最早グラフにしても棒が表示されてない...(

f:id:xin9le:20190909221347p:plain

今回は主に以下のような変更を行っています。早速思いっきり Breaking Chages!!

  • API Surface が大きく変更に
  • 更なるパフォーマンスの改善
  • API の追加
  • etc...

API Surface の変更

のいえ先生 (@neuecc) から「Generics Class な API は使い心地が悪いからヤだ」と指摘を受け、全くそのとーりだったので改善しました。思いっきり Breaking Changes!! なんですが、リリース数日なので許してください!(泣

概ね以下のように変更されています。Generics クラスだったものを Generics メソッドに変更した感じです。大変自然というか、System.Enum にかなり近づいた API になったと思います。

//--- Before
var values = FastEnum<Fruits>.Values;
var names = FastEnum<Fruits>.Names;
var defined = FastEnum<Fruits>.IsDefined(123);
var parse = FastEnum<Fruits>.Parse("Apple");
var tryParse = FastEnum<Fruits>.TryParse("Apple", out var value);

//--- After
var values = FastEnum.GetValues<Fruits>();
var names = FastEnum.GetNames<Fruits>();
var defined = FastEnum.IsDefined<Fruits>(123);
var parse = FastEnum.Parse<Fruits>("Apple");
var tryParse = FastEnum.TryParse<Fruits>("Apple", out var value);

更なるパフォーマンスの改善

.NET Core に勝っただけで世界最速!などと大きなことを言ってしまったことを反省しています。世の中には Enums.NET というライブラリがあって、同様に high-performance を謳っていました。それとの比較なしに最速を勝手に語るのは思い上がりなので、ちゃんと比較しました。

結果として IsDefined メソッドで Enums.NET より 4 倍ほど遅い結果が出てしまって大変がっかり...。

ということで必死に改善しました。改善した結果は記事冒頭のグラフの通りで、Enums.NET と比べて 6 倍 (v1.0 の実装と比べると 25 倍) 程速くなっています。どのメソッドにおいても Enums.NET よりも System.Enum よりも速い!今度こそ最速!(たぶん

まとめ

1 [ns] レベルの改善するために数日かけるという非効率の極み...。このチューニングによる時間は回収できるものなのか分かりませんが、大変勉強にはなりました。もうこれ以上の速度は出せない気がする (気がするだけかも) ので、ぜひ速いものに巻かれる (?) つもりで利用してみてください。バグ報告や PR などもお待ちしております :)

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:保守性の観点では好ましくないけれど、速度を稼ぐためにやむを得ず...

C# でインストールされている Windows Store App の一覧を取得する

とある先輩に「Windows Store のアプリ一覧を取得したいんだけど、やり方知らない?」と聞かれたのでやってみました。今回は、その方法と簡単な実装のメモの回。

最初から答えを授かりました

2 分ほど iPhone で調べて PowerShell コマンドで最低限できそうというところまでは当たりをつけていたのですが、C# から PowerShell コマンドを呼び出すのではない方法がいいなーと思っていました。と思っていたら C# MVP の大先輩である @matarillo さんがパッと答えを教えてくださいました!本当に感謝!

どうやら WMI (= Windows Management Instrumentation) という OS 管理の機能/技術を使えばできるみたいです。

実装

LINQPad で書いてみたのですが、ザッと以下のような感じで取得できました。WQL なるクエリ言語で書いて投げるだけの簡単なお仕事でした。ちなみに実行には管理者権限が必要です。

void Main()
{
    WindowsStoreApp.GetInstalled().Dump();
}

public sealed class WindowsStoreApp
{
    public string Architecture { get; }
    public string Language { get; }
    public string Name { get; }
    public string ProgramId { get; }
    public string Vendor { get; }
    public Version Version { get; }

    private WindowsStoreApp(PropertyDataCollection props)
    {
        this.Architecture = (string)props[nameof(Architecture)].Value;
        this.Language = (string)props[nameof(Language)].Value;
        this.Name = (string)props[nameof(Name)].Value;
        this.ProgramId = (string)props[nameof(ProgramId)].Value;
        this.Vendor = (string)props[nameof(Vendor)].Value;
        this.Version = Version.Parse((string)props[nameof(Version)].Value);
    }

    public static WindowsStoreApp[] GetInstalled()
    {
        // これで取り出せちゃう!簡単!
        const string wql = "select * from Win32_InstalledStoreProgram";
        using (var searcher = new ManagementObjectSearcher(wql))
        {
            return searcher
                .Get()
                .Cast<ManagementBaseObject>()
                .Select(x => x.Properties)
                .Select(x => new WindowsStoreApp(x))
                .ToArray();
        }
    }
}

手元の環境では結果は以下のようになっていて、この NameProgramId から所望のアプリがインストール済みかを判定できそうです。

f:id:xin9le:20190828023330p:plain

Microsoft Office の Application ID

例えば Microsoft Office の Windows Store 版が入っているかは、以下のサイトにある Application ID を参考にすると判定できるような気がしています。「気がしている」というのは、僕が Office 365 を契約していないので Windows Store 版をインストールできていないから、なだけなのです...(悪しからず

LINQPad.Controls でお手軽 GUI 操作

今頃気が付いたのですが、超カジュアル IDE の LINQPadLINQPad.Controls なる GUI 部品の名前空間が追加されていたので遊んでみました。以下のようなインタラクティブなことがお手軽にできるようになります。

f:id:xin9le:20190825152552g:plain

使い方

使い方はかなり簡単で、C# で WinForms / WPF / UWP などのデスクトップアプリを作ったことがある方にとってはお馴染みの方法です。TextBoxButton などのインスタンスを new して、イベント処理などをコードで表現するだけです。唯一違うのは GUI 部品を .Dump() すること。これで結果画面に GUI 部品が表示されるようになります。

var textBox = new TextBox("This is initial text.").Dump();  // TextBox 表示
var button = new Button("Execute").Dump();  // Button 表示
button.Click += (sender, e) =>
{
    $"Hello, my name is {textBox.Text}.".Dump();
};

サンプル

LINQPad をインストールすると、LINQPad.Controls のサンプルが結構含まれているので参考になります。その中に正規表現評価のサンプルがありました。以下のようなこともチョイチョイっとコードを書くとできちゃうみたいです。便利!

f:id:xin9le:20190825160839g:plain

LinkGenerator : ASP.NET Core の DI で利用可能な URL 生成機構

ASP.NET Core でリダイレクト機能を持つ Action Filter を作っていたときのこと。Controller や View で利用できる IUrlHelper を Action Filter で利用できないことに気が付きました。Controller だと以下のように Url プロパティで IUrlHelper にアクセスできるので簡単ですが、Action Filter には IUrlHelper にアクセスするためのプロパティなどがどこにもありません。これでは特定の Action に対するルーティングを考慮した URL の生成ができない!うーん、困った。

// Controller だとこう書けるから簡単だけど
public class ApplicationController
{
    [HttpGet("{market}/app/{id}")]
    public IActionResult Index([FromQuery]string id)
    {
        var action = "Index";
        var controller = "Application";
        var routeValues = new { market = "jp", id = "abc123" };
        var url = this.Url.Action(action, controller, routeValues);

        // url : "/jp/app/abc123"
    }
}

ドキュメントを読んでみる

IUrlHelper がない環境下でルーティングを考慮した URL の生成ができないかと調べていたら、公式ドキュメントにヒントがありました。

  • URL generation is based on addresses, which support arbitrary extensibility:

この英語の雰囲気的には以下のような感じかと思います。なるほど LinkGenerator を DI すれば良い、と。

  • LinkGenerator なる機能があり、それを DI を介して取得できる
  • LinkGenerator を使えないところには IUrlHelper が提供されている

使ってみる

簡単な認証フィルターを例に LinkGenerator 型を使ってみましょう。アカウントのサインインが必要なページに来たら認証画面にリダイレクトし、認証が終わったらページに戻す実装です。

public class AccountController
{
    [AllowAnonymous]
    public IActionResult SignIn(string returnUrl = "/")
    {
        // 認証済みなら即リダイレクト
        if (this.User.Identity.IsAuthenticated)
            return this.LocalRedirect(returnUrl);

        // リダイレクト先を決定して認証チャレンジ
        var props = new AuthenticationProperties { RedirectUri = returnUrl };
        return this.Challenge(props);
    }
}
public class AuthorizationFilter : IAuthorizationFilter
{
    private LinkGenerator LinkGenerator { get; }

    public AuthorizationFilter(LinkGenerator linkGenerator)
        => this.LinkGenerator = linkGenerator;

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // AllowAnonymous 属性が付いている場合はスキップ
        if (context.Filters.Any(x => x is IAllowAnonymousFilter))
            return;

        // 認証済みなら何もしない
        if (context.HttpContext.User.Identity.IsAuthenticated)
            return;

        // 認証ページにリダイレクト
        var returnUrl = context.HttpContext.Request.GetEncodedPathAndQuery();
        var routeValues = new { returnUrl };
        var url = this.LinkGenerator.GetPathByAction(context.HttpContext, "SignIn", "Account", routeValues);
        context.Result = new LocalRedirectResult(url);
    }
}

IUrlHelper と使い方が若干違いますが、概ね同じような感覚で使えますね。LinkGenerator のこと、ときどきでいいから...思い出してください。