2016年を振り返る

振り返る、のも五回目。今年は、ものすごくC#を書く技量が向上した気がします。いやほんと。私も結構歳とった感があるのですが(昨日誕生日で33歳でした!)、まだグッと成長できる切り口が残ってたんだなぁと思うと大変嬉しい話です。正直今年はあまり良いニュースはなかったのですが、自分のメインの軸で自己成長を実現できたというのは、次のステップ頑張ろうって気になれます。

C#

プログラミングって、ある程度はパターンがあって、このシチュエーションにはこれを当てはめて、こういう風に組み立てていけば勝てる、みたいな手札の多さが強さ(?)みたいなところがあると思ってるんですが、ここ2年ほど私自身のデッキは割と安定していたんですよね。言語やフレームワークのアップデートに従って組み替えたり、他のライブラリを見て手札を、アイディアを増やすというのは随時やっていってましたが、大きく変わるようなことはなかったなあ、と。言語がアップデートされると、そりゃ当然手法も大きく変わるんですが、良くも悪くもC#は安定期に入っていて、ぶっちけそんな変わってないし、次のC#も大して変わらないですしね。

って状況だったんですが、今年はガラッと書き方、考え方が変わりました。もちろん、使い続けている手札もいっぱいありますが、新規に入ってきた要素もとても多くて。そのお陰で、APIの表現力も大幅に上がりました。組み合わせの問題でもあるので、手札が多いと、やれることの幅やAPIの表現力が爆発的に上がっていくので非常に良いことです(逆に手札が少ない人の作るAPIは窮屈だったりするというのはありますね、そういうのみると慢心してる感じだなあ、とか思ったりはします)

変わった要因は2つあって、一つは、今年はパフォーマンスを極限まで追い求めたコードを色々書いたから。ブログを漁るとUnityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映UniRx 5.4.0 - Unity 5.4対応とまだまだ最適化と、UniRxの継続アップデートはいつも新しいことを考えたり、導入したりするきっかけになっています。UniRxも今年はGitHubで1000Star越えを果たしたり、スーパーマリオラン(5000万ダウンロード!)に採用されていたりと、一つの山を超えた感じはあります。

個人的にブレークスルーだったのはLINQ to GameObject 2.1 - 手書き列挙子による性能向上と追加系をより使いやすくで、改めてLINQ、そしてパフォーマンスとは、に関して見直すきっかけになりました。そしてZeroFormatter - C#の最速かつ無限大高速な .NET, .NET Core, Unity用シリアライザーで、集大成として結実しました。いやぁ、大変だった。ほんと大変だった、終わってみればあっさりって気もしなくもないんですが、いやぁ、大変だった……。シリアライザなんて枯れた群雄割拠な代物と思ってましたが、性能面でもまだまだ全然追求できる幅あったんだというのは驚きで。意外と世の中まだやれることは無限にある。C#もまだまだ限界は迎えてない。

性能は最大の機能だ、というのは勿論なのですけれど、究極的にそれを実現するためには新しいアイディアを大量に投下しなきゃいけなかった。今まで自分はいかにヌルいコードを書いてたんだ、と痛感させられました。また、そんな性能追求ギプスのお陰で沢山の手札を手に入れられて、それは視野の広がりをもたらして、ただたんに性能のために、というだけじゃなく書き方の広がりを手に入れられたと思ってます。

突き詰めてやることにはとても意義がある。逆に、そこまでしなければ手に入れられないものもある。手札を増やすのに他の言語に浮気するってのも悪いことではないですが、その前に目の前のことを突き詰めてみるってのもいいんじゃないのってのはとっても思います。nullがどうこうとか言ってる前にC#どんだけ書けるのよ、みたいな。みたいな?

技術的負債との付き合い方

技術的負債って、優秀なエンジニアがしっかり考えれば発生しない。わけではないんですよね。コードなんて誰が書いても、書いた瞬間から腐敗は始まっていて、アプリケーションとしてローンチする前から負債になっている場合すらある。そして、出来ないエンジニアの作る負債よりも、むしろ出来るエンジニアの作る負債のほうが痛かったりする。JavaScript界隈でよく聞くような、新しい技術をいっぱい取り入れました、でももう時代遅れです!みたいなのは典型ですが(これも普通よりちょっと出来るエンジニアぐらいのほうがハマりやすい罠)、そんなんじゃなくても、大なり小なり腐敗を抱えて生きてるわけです。

永遠に輝くコードなんて存在しないからこそ、むしろいかに捨てるかに腐心するほうが良い。もちろん、私の書くものだって例外じゃあなくて、ゴミは作ってしまうのね。別にゴミだと思って作るわけじゃなくても!ダメだと気づいたら、しょうがないので焼却する。これがね、自分の作ったコードなら躊躇なく捨てられる。捨てた際のカバーもなんとかできる、こともある(できないこともある、ひどぅぃ)。けれど他人の作ったものの扱いはとても難しい。そもそも他人の書いたものをジャッジするのが難しい!自分の書いたものを、あぁ、アイディア自体がゴミでダメですね、と切り捨てれても、他人のものを正しく判定するのはむつかしいんだなあ。いや、現在にたいしてダメか否かの判定は簡単ですけれど、未来の判定をするのがむつかしい。

自分の書いたものだと未来も見えるんですよね、このアイディアの延長線上に何があるか想像がつく、未来がないことが見えた時、やめましょう、投げ捨てましょう、になる。けれど、他人の未来はわからなくて、今はまだまだだけど、もう少しやってりゃあなんとかなるかもしれない……。とか思っちゃうわけです。期待して。或いは目をつむって。実際大抵はそんなことはなくて、ダメなもんはダメだったりするわけですが。

損切りするのが難しいのと一緒で、そりゃうまくできりゃあ良いんですが。というかうまくできなきゃあダメなんですが。傷口は 消毒で誤魔化してないで、腐食が進む前に切り落とさなきゃ本当にダメで。腐った土台のうえでいくら技巧を凝らしても、醜い延命策で、なんの解決にもなってないというか、むしろただの時間の浪費なんですよね。いやはや。

何れにせよ、奢った気持ちで書かれたものはダメですねぇ。「よくできているのにどうしょうもなくダメなプログラム」とは何ぞやか、というのを考え直すきっかけになりましたし、そうして考え直すことは自分の書き方の変化にも繋がりました。自分自身ね、そういうの書いちゃってたりやっぱしてしまうわけで。

お仕事

というわけで技術的負債の返却、じゃないですが、今年の後半は、意識的に、問題を技術で解決するというところにフォーカスしていました。結構ね、状況は余裕じゃないんですが、なんとかして解消しなければならない!

ZeroFormatterを起点に、まだ未完成のものでMagicOnion - gRPC based HTTP/2 RPC Streaming FrameworkMasterMemory - Embedded Readonly In-Memory Document Databaseというのを用意しています。

現状をクソだというのはイージーなんですが、なんとか維持しつつも解決させるってのは結構難しくある。アイデアというのは複数の問題を一気に解決するものであるとはよく言ったものですが、実際、これらの導入によって抱えている問題をそれなりに解決できる。といいなぁ。

技術で技術を返却するってのは、良くも悪くもですね。特に、私自身がCTOという立場でそれやってるのは、結構キワキワだとは思ってます。意識して脳みその9割をコードに割くようにしてるのは、逆に他のことはあまり考えてないってことですからねぇ。正直、あんまいいことじゃあないし、来年も同じようにしたいとは思わないというか、すべきではないと思ってますが、現在の状況からすればこれが最善、かな。と選んでやってます。この辺はしゃーない。もう少しうまくやれりゃあいいのですけれど。

損切りのタイミングを逸したとか、自分で返却しなきゃいけないものを返却できなかったりとか、前期であまり良い決断ができてなかったというのはうーむ、といったところも多々ありつつ。対外的なプレゼンスに関してはよくやれたと思ってますし、その辺の人にはできないことをやってるとは思いますが、それだけでいいと言い切れない程度には歯切れの悪い年でした。

ゲームとか音楽とか

とんかつDJアゲ太郎だけはアニメ全部見ました:) それ以外はアニメもドラマも何もかも完走できてないというかロクに見ちゃいない。本も読んでなければ漫画も見てないんですが、うーん、何が良かったかなあ。本日のバーガーはテーマ的には良かった!色々なハンバーガーがあるし、あっていんだよ、という当たり前のような当たり前を認識できて。

ゲームは、うーん、オーディンスフィア レイヴスラシルは今年でしたか、良かった。あとスーパーマリオランはレート3000、ブラックコインコンプぐらいにはやりました。レートカンストはちょっと不毛感あるので、いったんそろそろいいかな感もありますが。

音楽は、水曜日のカンパネラをよく聴いてましたねー、ジパング私を鬼ヶ島に連れてってが傑作で。あと、つい先日出た戸川純 with Vampillia / わたしが鳴こうホトトギスが良くてホクホク。

来年

年始暫くはひたすらシステムプログラミングですねー。好きでやってていいってことにも、限度が、頻度というものがあって、大げさ大掛かりなものを連続して作らなきゃいけないってのは正直シンドイ。ゆうて神経めっちゃ使うのよ。やるにしても、もう少し間隔あけながらやりたいよぅ、というのも自業自得なんでしょーがない。

というわけかで、去年の目標であったグラフィックプログラミングはちっとも前進しませんでした。今年はVRにもしっかり手を出したかったんですが、あまりやれてないですね、まぁそうしたグラフィックプログラミングも、VRも、あと最近興味あるのはディープラーニングも、ゲームをリリースするまではお預け。

というわけで、リリースしましょう、ってことですね!

ASP.NET Coreを利用してASP.NET Coreを利用しないMiddlewareの作り方

今回の記事はASP.NET Advent Calendar 2016向けのものとなります。最終日!特に書くつもりもなかったのですが、たまたま表題のような機能を持つMiddlewareを作ったので、せっかくなので書いておくか、みたいなみたいな。

.NET 4.6でASP.NET Core

まぁ普通に.NET 4.6でASP.NET Coreのパッケージ入れるだけなんですが。別にASP.NET Coreは.NET Coreでしか動かせないわけではなくて、ちゃんと(?).NET 4.6でも動きます。如何せん.NET Coreがまだ環境として成熟してはいないので、強くLinuxで動かしたいという欲求がなければ、まだまだWindows/.NET 4.6で動かすほうが無難でしょう。Visual Studioのサポートも2015だとちょっとイマイチだとも思っていて、私的には本格的に作り出していくのはVisual Studio 2017待ちです。脱Windowsとして、Linuxでホスティングするというシナリオ自体にはかなり魅力的に思っていますし、ライブラリを作るのだったら今だと.NET Core対応は必須だと思いますけれど。

Hello Middleware

Middlewareとはなんぞやか、というと、ASP.NET公式のMiddlewareのドキュメントが見れば良いですね。

image

Httpのリクエストを受けつけて、レスポンスを返す。ASP.NET Core MVCなどのフレームワークも、Middlewareの一種(図で言うところのMiddleware3にあたる、パイプラインの終点に位置する)と見なせます。このパイプラインのチェーンによって、事前に認証を挟んだりロギングを仕込んだりルーティングしたりなど、機能をアプリケーションに足していくことができます。

考え方も、実質的なメソッドシグネチャもASP.NET Coreの前身のOWINと同一です。今ではOWIN自体の機能や周辺フレームワークは完全に整っていて、ASP.NET Coreで全て賄えるようになっているので、新しく作る場合はASP.NET Coreのことだけを考えればいいでしょう。逆に、OWINで構築したものをASP.NET Coreへ移行することはそう難しくないです

ASP.NET Coreのパッケージはいろいろあって、どれを参照すべきか悩ましいのですが、最小のコア部分となるのはMicrosoft.AspNetCore.Http.Abstractionsです。これさえあればMiddlewareが作れます。

では、パイプラインの各部にフックするだけの単純なMiddlewareを作りましょう!

public class HelloMiddleware
{
    // RequestDelegate = Func<HttpContext, Task>
    readonly RequestDelegate next;

    public HelloMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            Console.WriteLine("Before Next");
            
            // パイプラインの「次」のミドルウェアを呼ぶ
            // 条件を判定して「呼ばない」という選択を取ることもできる
            await next.Invoke(context);

            Console.WriteLine("After Next");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception" + ex.ToString());
        }
        finally
        {
            Console.WriteLine("Finally");
        }
    }
}

注意点としては、完全に「規約ベース」です。コンストラクタの第一引数はRequestDelegateを持ち(その他のパラメータが必要な場合は第二引数以降に書く)、public Task Invoke(HttpContext context)メソッドを持つ必要があります。逆に、それを満たしていればどのような形になっていても構いません。

この規約ベースなところは賛否あるかなぁ、というところですが(私はどちらかというと否)、C#の言語機能としてはしょうがない面もあります。(自分でもこの手のフレームワークを何個か作った経験があるところから理解している上で)実装面の話をすると、この規約で最も大事なところは、コンストラクタの第一引数でRequestDelegateを受け入れるところにあります。そして、C#は具象型のコンストラクタの型の制約は入れられないんですよね。なので、MiddlewareBaseとか作ってもあんま意味がなくて、ならもう全部規約ベースで処理しちゃおうって気持ちは分かります。

Invokeのメソッドシグネチャをpublic Task Invoke(HttpContext context, RequestDelegate next)にすることで、そうしたコンストラクタの制約を受ける必要がなくなって、メソッドに対するインターフェイスでC#として綺麗な制約をかけることは可能になるんですが(私も、なので以前はそういうデザインを取っていた)、そうなるとパフォーマンス上の問題を抱えることになります。Invoke(HttpContext context, RequestDelegate next)というメソッドシグネチャだと実行時に"next"を解決していくことになるのですが、これやるとどうしても、nextを解決するための余計なオブジェクト(クロージャを作るかそれ用の管理オブジェクトを新しく作るか)が必要になりますし、呼び出し階層もその中間層を挟むため、どうしても一個深くなってしまいます。

ミドルウェアパイプラインは構築時にnextを解決することができるわけで、そうした実行時のコストを構築時に抑え込むことが原理上可能です。それが、コンストラクタでnextを受け入れることです。C#を活かした設計の美しさ vs パフォーマンス。このMiddlewareチェーンはASP.NET Coreにおける最も最下層のレイヤー。この局面ではパフォーマンスを選ぶべきでしょう。実に良いチョイスだと思います。

最後に、使いやすいように拡張メソッドを用意しましょう。拡張メソッドなのでnamespaceは浅めのところにおいておくと使いやすいので、その辺は適当に気をつけましょう:)

public static class HelloMiddlewareExtensions
{
    public static IApplicationBuilder UseHello(this IApplicationBuilder builder)
    {
        // 規約ベースで実行時にnewされる。パラメータがある場合はparams object[] argsで。
        return builder.UseMiddleware<HelloMiddleware>();
    }
}

Middlewareを使う

作ったら使わないと動作確認もできません!というわけでホスティングなのですが、これもAspNetCoreのパッケージはいっぱいありすぎてよくわからなかったりしますが、「Microsoft.AspNetCore.Server.*」がサーバーを立てるためのライブラリになってます。IISならIISIntegration、Linuxで動かすならKestrel、コンソールアプリなどでのセルフホストならWebListenerを選べばOK。今回はMicrosoft.AspNetCore.Server.WebListenerで行きましょう。

class Program
{
    static void Main(string[] args)
    {
        var webHost = new WebHostBuilder()
            .UseWebListener()      // ホスティングサーバーを決める
            .UseStartup<Startup>() // サーバー起動時に呼ばれるクラスを指定
            .UseUrls("http://localhost:54321") // 立ち上げるアドレスを指定
            .Build();

        webHost.Run();
    }
}

public class Startup
{
    // Configure(IApplicationBuilder app)というのも規約ベースで名前固定
    public void Configure(IApplicationBuilder app)
    {
        // さっき作ったMiddlewareを使う
        app.UseHello();

        // この場で最下層の匿名Middleware(nextがない)を作る
        app.Run(async ctx =>
        {
            var now = DateTime.Now.ToString();
            Console.WriteLine("---------" + now + "----------");
            await ctx.Response.WriteAsync(DateTime.Now.ToString());
        });
    }
}

例によって規約ベースなところが多いので、まぁ最初はコピペで行きましょう、しょーがない。これでブラウザでlocalhost:54321を叩いてもらえば、現在時刻が出力されるのと、コンソールにはパイプライン通ってますよーのログが出ます。

image

基本のHello Worldはこんなところでしょう、後は全部これの応用に過ぎません。

ASP.NET Coreを利用してASP.NET Coreを利用しない

さて、本題(?)。現在、私はMagicOnionというフレームワークを作っていて(まぁまぁ動いてますが、一応alpha段階)、謳い文句は「gRPC based HTTP/2 RPC Streaming Framework for .NET, .NET Core and Unity」。つまり……?gRPCというGoogleの作っている「A high performance, open-source universal RPC framework」を下回りで使います。つまり、ASP.NET Coreは使いません。さよならASP.NET Core……。

gRPCは(.NET以外では)非常に盛り上がりを見せていて、ググればいっぱい日本語でもお話が見つかるので、知らない方は適当に検索を。非常に良いものです。

gRPCはHTTP/2ベースで、しかもデータは基本的にはProtocol Buffersでやり取りされているので、従来のエコシステム(HTTP/1 + JSON)からのアクセスが使えません。そこでgrpc-gatewayというプロキシを間に挟むことで HTTP/1 + JSONで受けてHTTP/2 + Protobuf にルーティングします。それによりSwaggerなどの便利UIも使えて大変捗るという図式です。素晴らしい!

grpc-gatewayは素晴らしいんですが、Pure Windows環境で使うのは恐らく無理があるのと、MagicOnionではデータをZeroFormatterでやり取りするようにしているので、そのまま使えません。残念ながら。しかし、特にSwaggerが使いたいんで絶対にgrpc-gateway的なものは欲しい。と、いうわけで、用意しました。ASP.NET Coreを利用して(HTTP/1 + JSON)、ASP.NET Coreを利用しない(HTTP/2 + gRPC/MagicOnion/ZeroFormatter)。

public class MagicOnionHttpGatewayMiddleware
{
    readonly RequestDelegate next;
    // MagicOnionのHandler(キニシナイ)
    readonly IDictionary<string, MethodHandler> handlers;
    // gRPCのコネクション
    readonly Channel channel;

    public MagicOnionHttpGatewayMiddleware(RequestDelegate next, IReadOnlyList<MethodHandler> handlers, Channel channel)
    {
        this.next = next;
        this.handlers = handlers.ToDictionary(x => "/" + x.ToString());
        this.channel = channel;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            var path = httpContext.Request.Path.Value;

            // HttpContextのパスをgRPCのパスと適当に照合する
            MethodHandler handler;
            if (!handlers.TryGetValue(path, out handler))
            {
                await next(httpContext);
                return;
            }

            // BodyにJSONがやってきてるということにする(実際はFormからの場合など分岐がいっぱいでもっと複雑ですが!)
            string body;
            using (var sr = new StreamReader(httpContext.Request.Body, Encoding.UTF8))
            {
                body = sr.ReadToEnd();
            }

            // JSON -> C# Object
            var deserializedObject = Newtonsoft.Json.JsonConvert.DeserializeObject(body, handler.RequestType);

            // C# Object -> ZeroFormatter
            var requestObject = handler.BoxedSerialize(deserializedObject);

            // gRPCのMethodをリクエストを動的に作る
            var method = new Method<byte[], byte[]>(MethodType.Unary, handler.ServiceName, handler.MethodInfo.Name, MagicOnionMarshallers.ByteArrayMarshaller, MagicOnionMarshallers.ByteArrayMarshaller);
            
            // gRPCで通信、レスポンスを受け取る(ZeroFormatter)
            var rawResponse = await new DefaultCallInvoker(channel)
                .AsyncUnaryCall(method, null, default(CallOptions), requestObject);

            // ZeroFormatter -> C# Object
            var obj = handler.BoxedDeserialize(rawResponse);

            // C# Object -> JSON
            var v = JsonConvert.SerializeObject(obj, new[] { new Newtonsoft.Json.Converters.StringEnumConverter() });

            // で、HttpContext.Responseに書く。
            httpContext.Response.ContentType = "application/json";
            await httpContext.Response.WriteAsync(v);
        }
        catch (Exception ex)
        {
            // とりあえず例外はそのまんまドバーッと出しておいてみる
            httpContext.Response.StatusCode = 500;
            await httpContext.Response.WriteAsync(ex.ToString());
        }
    }
}

細かいところはどうでもいいんですが(あと一部端折ってます、実際はもう少し複雑なので)、基本的な流れはJSONをZeroFormatterに変換→内部で動いてるgRPCと通信→ZeroFormatterをJSONに変換。です。見事に左から右にデータを流すだけー、のお仕事、ですね!

MagicOnion本体は限界までボクシングが発生しないように、ラムダのキャプチャなどにも気を使って、ギチギチにパフォーマンスチューニングしてあるんですが、このGatewayはそんなに気を使ってません:) まぁ、もとより複数回の変換が走ってる、パフォーマンス最優先のレイヤーではないから、いっかな、という。どっちかというとデバッグ用途でSwaggerを使いたいがために用意したようなものです。本流の通信はこのレイヤーを通ることはないので。

image

ちゃんとgRPCでもSwagger使えてめっちゃ捗る。

What is MagicOnion?

gRPCは.protoを記述してサーバーコードの雛形とクライアントコードを生成します。私はこのIDL(Interface Definition Language)の層があまり好きじゃないんですね。そもそも、クライアントもサーバーも、ありとあらゆる層をC#で統一しているので、C#以外を考慮する必要がないというのもあるので。なので、C#自体をIDLとして使えるように調整したり、MVCフレームワークでいうフィルターが標準でないので、それを差し込めるようにしたり、gRPCは(int x, int y, int z)のような引数に並べるような書き方ができない(必ずRequestクラスを要求する!)ので、動的にそれを生成するようにしたりして、より自然にC#で使えるように、かつ、パフォーマンスも一切犠牲にしない(中間層が入ってるからオーバーヘッドと思いきや、むしろプリミティブ型が使えるようになったのでむしろ素のgRPCより速くなる)ようにしています。そもそもそしてUnityでも動作出来るような調整/カスタマイズなどなども込みで、ですね。

それ以外の話はZeroFormatterと謎RPCについて発表してきました。にて少し書いてあります。もう少し詳細な話は、完成した時に……。

まとめ

.NET Coreを本格的に(プロダクション環境で)使うということは、特に開発環境という点でまだ足りないところが多くて(project.json廃止とかゴタついたところもあるし)、VS2017待ちだと判断しています。しかし、ASP.NET Coreのフレームワーク面では十分完成していて、問題ないですね。なので、そちらから随時移行していきたいという気持ちでいます。

まぁ、とはいえ↑で書いたとおり、ほとんどASP.NET Core自体すら使わないんですが。うーん、そうですね、やっぱスタンダードな作り(JSON API)をクロスプラットフォームを紳士に取り組んでます、みたいなことやってる間に、世界は凄いスピードで回ってるんですよね。Microsoftは常に一歩遅いと思っていて、まぁ今回もやっぱそうですよね、という感じで、世間が成熟した頃にやっと乗り出すようなスピード感だと思ってます。ナデラでOSSでスピーディーなのかといったら、別に私はそう思ってないですね、スピードという点では相変わらずだなぁ、と。むしろ「正しくやろうとする」圧力の高さに自分で縛られてしまっている気すらします。スタンダードだからとJSONでコンフィグ頑張ろうとしてやっぱダメでした撤回、みたいな。そういうのあんま良くないし、その辺の束縛から自由になれた時が真のスタートなんじゃないかな。

ともあれ、私はgRPCにベットしてるんで、ASP.NET Core自体は割とどうでも良く思ってます、今のところ。でもそれはそれとして、当然(補助的に)使ってく必要はあるんで、そういう時にちょいちょいと出番はあるでしょう。

C#に置ける日付のシリアライズ、DateTimeとDateTimeOffsetの裏側について

C# Advent Calendar 2016の記事になります。何気に毎年書いてるんですよねー。今年は、つい最近ZeroFormatterというC#で最速の(本当にね!)シリアライザを書いたので、その動的コード生成部分にフォーカスして、ILGenerator入門、にしようかと思ってました。ILGeneratorでIL手書き、というと、黒魔術!難しい!と思ってしまうけなのですが、実のところ別に、分かるとそれほど難しくはなくて(面倒くさい&デバッグしんどいというのはある)、しかし同時にILGeneratorで書いたから速かったり役に立ったり、というのもなかったりします。大事なのは、どういうコードを生成するのかと、全体でどう使わせるようなシステムに組み上げるのか、だったり。とはいえ、その理想的なシステムを組むための道具としてILGeneratorによるIL手書きが手元にあると、表現力の幅は広がるでしょう。

シリアライザ作って思ったのは、Jilは(実装が)大変良くできているし、SigilはIL生成において大いに役立つ素晴らしいライブラリだと思ってます。まぁ、そういうの作る時って依存避けたいので使わなかったけれどね……。

みたいなイイ話をしようと思っていたんですが、ちょっと路線変更でDateTimeについてということにします。えー。まぁいいじゃないですか、DateTimeだって深いですし、IL手書きなんかよりずっと馴染み深いではないですか。役立ち役立ち。

DateTimeとはなんぞやか

シリアライズの観点から言うと、ulongです。DateTimeとはulongのラッパー構造体というのが実体です。ulongとは、Ticksプロパティのことを指していて、なので例えばDayを取ろうとすれば内部的にはTicksから算出、AddHoursとすればhoursをTicksに変換した後に内部的なulongを足して、新しい構造体を返す。といった形に内部的にはなっています。それぞれのオペレーションは除算をちょっとやる程度なので、かなり軽量といってもいいでしょう。

つまり、DateTimeとはなんぞやかというのは、Ticksってなんやねん、という話でもある。

Ticksとは、100ナノセカンド精度での、0が0001/01/01 00:00:00から、最大が9999/12/31 23:59:59.999999までを指す。ほほー。

// 0001-01-01T00:00:00.0000000
new DateTime(ticks: 0).ToString("o");
// 0001-01-01T00:00:00.0000001
new DateTime(ticks: 1).ToString("o");

// 3155378975999999999
DateTime.MaxValue.Ticks

DateTimeにはもう一つ、Kindという情報も保持しています。KindはUtcかLocalか謎か(Unspecified)の三択。

public enum DateTimeKind
{
    Unspecified = 0,
    Utc = 1,
    Local = 2
}

ふつーにDateTime.Nowで取得する値は、Localになっています、ので日本時間である+9:00された値が取れます。さて、このKindは内部的にはどこに保持されているかというと、Ticksと相乗りです!ulong、つまり8バイト、つまり64ビットのうち62ビットをTicksの表現に、残りの2ビットでKindを表現しています。なんでそういう構造になっているかといえば、まぁ節約ですね、メモリの節約。まー、コアライブラリなのでそういう気の使い方します、的な何か。

Ticksプロパティ、Kindプロパティはそれぞれ内部データを脱臭した値が出てくるので、そうしたTicks, Kindが相乗りした内部データを取りたい場合はToBinaryメソッドを使います。復元する場合は、FromBinaryです。

// Ticks + Kind(long, 8byte)
var dateData = DateTime.Now.ToBinary();
var now = DateTime.FromBinary(dateData);

これで8バイトでDateTimeの全てを表現できるので、これが最小かつ最速な手法になります。あまり使うこともないと思いますが。

さて、当然ZeroFormatterはそうしたToBinaryで保持してるんだよね!?というと、違います!seconds:long + nanos:intという12バイト使った表現(秒+ナノ秒)にしています。これはProtocol Buffersの表現を流用していて、うーん、一応クロスプラットフォーム的にはそのほうがいいかな、みたいな(でも今考えると別にTicksで何が悪い、って気はする……失敗した……)。そして、Kindは捨てています。シリアライズ時にToUniversalTimeでUTCに変換し、そのUTCの値のみシリアライズしています。

で、Kindは、私は捨てていいと思ってます。一応MSDNのDateTime、DateTimeOffset、TimeSpan、および TimeZoneInfo の使い分けというドキュメントにもありますが

DateTime データを保存または共有する際、UTC を使用する必要があり、DateTime 値の Kind プロパティを DateTimeKind.Utc に設定する必要があります。

UTCかLocalか、なんていうだけの二値はシリアライズに全く向いてないです。それだったらTimeZoneも保存しないと意味がない。アメリカで復元したらどうなんねん、みたいな。なのでシリアライズという観点で見るとKindはナンセンス極まりないです。これはDateTimeの設計が悪いって話でもあるんですが(後述するDateTimeOffsetがDateTimeのラッパーみたいな感じになってますけれど、本質的にはその逆であるべきだと思う)、その辺(初期の.NETのクラスはどうしても微妙にしょっぱいところがある)はshoganaiんで、受け入れるんだったらKindは無視。これが鉄板。

DateTimeOffset

Kindを無視するのはいいけれど、時差の保存は欲しいよね、という時の出番がDateTimeOffset。これは内部的には ulong(DateTime) + short(オフセット分) の2つの値で保持しています。まんま、DateTimeとOffset。DateTime.NowとDateTimeOffset.Nowって同じような値が帰ってくるし違いはなんなんやねん、というと、DateTimeOffsetはローカル時間といったKindじゃなくて、明確に内部的に+9時間というオフセットを持っているということです。

ZeroFormatterでシリアライズする際は、こちらはオフセットも保存していて、 seconds:long + nanos:int + minutes:short の14バイトの構成です。

ZeroFormatter上では明確にDateTimeとDateTimeOffsetは違うものとして取り扱ってるわけですが、よくあるDateTimeをToString("o")した場合って(んで、JSONなんかに乗せる場合って)

// 2016-12-07T03:19:23.7683110+09:00
DateTime.Now.ToString("o");
// 2016-12-07T03:19:23.7713117+09:00
DateTimeOffset.Now.ToString("o");

と、いうふうに、完全に一緒なわけです。というか、むしろこれはDateTimeの(文字列への)シリアライズをDateTimeOffsetとして表現している、とも言えます。まぁ、そのほうが実用上は親切ではある。が、これはDateTimeもDateTimeOffsetも区別してない(stringで表現)からっていうことであって、決してKindもシリアライズしているということではないということには注意。そして明確にDateTimeとしてDateTimeOffsetを違うものとして扱うなら(ZeroFormatterの場合)、良くも悪くもこういう表現はできないんだなぁ。不便だけどね。

基本的にDateTimeOffset、のほうが使われるべき正しい表現だと思うんですが、.NETのクラス設計上、DateTimeのほうが簡潔(だし内部構造的にもDateTimeOffsetはDateTime+αという形)で短い名前(名前超大事!)である以上、DateTimeの天下は揺るがないでしょう。残念なことにDateTimeOffsetの登場が.NET 2.0 SP1からだということもあるし。DateTimeOffsetがDateTimeで、DateTimeがLocalDateTimeだったら話は変わってくるでしょうけれど(そしてそんな構造だったらきっとLocalDateTimeは使われない)、まぁ変わらないものは変わらないです。まぁ保存用途ならUTCが良いと思うんで、現代的な意味では逆にDateTimeOffsetの出番はより減ってきたとも言える。データがクラウドに保存されて世界各国で共有されるとか当たり前なので、保存はUTC、表示時にToLocalTimeのほうが合理的。Kindって何やねん、と同じぐらいOffsetって何やねん、みたいな。

まぁLocalDateTime, ZonedDateTime, OffsetDateTimeという3種で表現というJava8方式が良いですよねということになる。

NodaTime

日付と時間に関しては、TimeZoneやCalendarなど、真面目に扱うとより泥沼街道を突っ走らなければならないわけですが、いっそ.NET標準のクラスを「使わない」という手もあります。NodaTimeは良い代替で、Javaの実質標準のJodaTime(後にJava8 Date API)の移植ではありますが、製作者がJon Skeet(Stackoverflow回答ランキング世界一位, Microsoft MVP, google, C# in Depth著者)なので、ありがちなJava移植おえー、みたいなのでは決してないのが一安心。

こういった標準クラスを置き換える野良ライブラリはシリアライズ出来ないのが難点で、そうしたシリアライズの表象でだけDateTime/DateTimeOffsetに置き換えるというのはよくあるパターンですが面倒くさくはある。シリアライザの拡張ポイントを利用してネイティブシリアライズ出来るようにするのが良い対応かなー、というのはあります。NodaTimeは標準でJson.NETに対応した拡張ライブラリが用意されているというところも、(当たり前ですが)わかってるなー度が高くていいですね。ZeroFormatterも拡張ポイントを持っているので、必要な分だけ手書きして対応させれば、まぁ、まぁ:)

まとめ

DateTimeOffsetも可愛い子ではある。時に使ってあげてください。というわけで、次のアドベントカレンダーは@Marimoiroさんです!

ZeroFormatterに見るC#で最速のシリアライザを作成する方法

というタイトルで発表してきました。連続してZeroFormatterネタなのですが、今回はC#実装のほうにフォーカスして紹介しています。

ZeroFormatterに見るC#で最速のシリアライザを作成する100億の方法 from Yoshifumi Kawai

intをシリアライズするところにフォーカスして、何故、既存のシリアライザは遅くて、何故ZeroFormatterは速いのかというところを解説しました。読んでもらえれば、理屈でパフォーマンスについて納得してもらえるんじゃないかと思います。

以下、会場であったFAQなどなぞ。

エンディアン違いは?

現在はリトルエンディアンしかサポートしてません。C#の動く環境ってほとんどリトルエンディアンなのでそこまで大きな問題ではないかな、と(Xboxはダメらしいですが)。対応しようと思えば当然できるんですが、Buffer.BlockCopyを多用しているので、そこの部分をバラさなきゃいけないので若干手間なのですよね(あと、性能面では低下します)。というわけで、要望があって困った、というレポートが来てから対応を考えます。一応、ビッグエンディアン下では例外を吐くようになっていて、そこの例外メッセージの中で、issueに自分の環境を書いていってください、みたいなメッセージを乗せています。

LINQ使っちゃダメなの?

んなこたぁないです。場所によりけりで、ZeroFormatter内部でも、コード動的生成する部分の型情報を舐めてどうこうするところでは使っています。それは「アプリケーションの寿命の中で最初の一回だけだから」「動的に作ったILをコンパイルする時間のほうが比較にならないぐらいに長いので、その程度を節約するのは無意味」だからです。

基本的には使おうよ、ってのは変わりはしないのですけれど、とはいえ、今まで良いとされてきた領域が、必ずしもそうなの?実はそうじゃないんじゃないの?というのを頭に入れて、都度都度考える必要は出てきているんじゃないかな、と思ってます。以前よりも。ゲームなんかでは今も昔も当然そうなのですけれど、ふつーのアプリケーションでも、今まで、単体のコンピューターで動くものは、まぁ限界もあるし、コンピューターの性能は上昇し続けるで、気にする必要はそんななかった。サーバーアプリケーションも。でも、今、サーバーアプリケーションって数十台、数百台のクラスタで動かすことも少なくなくて、それらの場合って少し性能を上げるだけで、数百台の見返りがあるんですよね。塵も積もれば山となる、今まではチリはつもらなかったけれど、今はつもりやすい環境になってきた。ってことを考えると、まぁ、特にライブラリや基盤部分のフレームワークなんかはどこでどう使われるか分からないので、気合入れてこう!っと。

2番じゃダメなんですか

まぁ、ダメですね!

せっかくライブラリ公開するなら多くの人に使ってもらいたいんですよね。これは、単純に使ってもらって嬉しいっていうのと、多くの人に使われることによって、バグが減る、機能のためのアイディアがもらえる、コントリビュートしてもらえてより強力なライブラリになれる、などなどもあります。そういうのって、会社にとってもメリットなんですよね。大きめの規模だったり独自性の高いライブラリは、社内だけで抱えたくないんです。まず、未来がない。未来がないものなんて使いたくない。というわけで、出来る限り、最初から公開を意識して作って、実際公開するわけですが、別に公開したからって未来があるわけでもない。多くの人に使われて、ある程度メジャー感が出て、はじめて未来が生まれる。なので、やるからには精一杯頑張ろうって感じですね。少なくとも何らかのインパクトは残したいと思ってやってます、いつも。

んで、2番ってヒキが全くないわけですよ。1番と2番があったら、そりゃ1番選ぶでしょ。 "Second Place is the First Loser"なわけです(ちょうど勉強会の時に聞いたので早速使ってみた)。というわけで、ヒキのある要素は色々必要で、一点目が「無限大に高速」で、これは勿論、非常に差別化要素になりうる目玉機能です。でも、それだけだとキワモノ臭さが抜けない。やっぱパフォーマンスが最大の機能なんですよね、この手のものは。だから、最速。最初はそれを目指してたわけじゃなかったんですが、スライド中に書いたように、初期設計段階でStreamを排除していたりステートを抜いてたりしたのが功を奏して、ある程度出来た段階で、最速が現実的に狙えると分かったので、そっから先はギアを切り替えてガチガチに書きました。そのせいで完成が若干遅れはしたんですが、結果としては非常に良かったと思ってます。

名前の由来

ゼロ速度のシリアライザということで。ZeroSerializerよりZeroFormatterのほうが格好良いと思います、語感が。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive