Array.Empty 最適化

.Net 4.6 optimizations breaking on 4.5.2 servers #4889

Roslyn(C# コンパイラー)の問題か、msbuild(ビルド ツール)の問題化、JIT レイヤーに持っていくべき問題かとかいろいろたらいまわしになりそうな話。

Roslyn がやったこと

まず、何をやろうとして問題が起きてるのか。

params 引数に対して何も引数を渡さなかったときに、C# コンパイラーが内部的に空の配列を作って引数に渡します。この時、Roslyn を使って .NET 4.6 以降をターゲットにすると、生成されるコードが少し変わります。

new StringBuilder().AppendFormat("") みたいなのを書くと、実際に生成されるコードは、

  • 旧コンパイラー/古い .NET: new StringBuilder().AppendFormat("", new object[0])
  • Roslyn かつ .NET 4.6: new StringBuilder().AppendFormat("", Array.Empty<object>())

もちろん、GC性能改善のために、無駄な配列インスタンスを作らないため。

起きた問題

Array.Emptyは .NET 4.6 で追加されたメソッドなので、当たり前ですが、これを使った最適化は .NET 4.6 以降をターゲットにする場合にしかできません。もちろんRoslyn は .NET 4.6 を参照しているときにしかこの最適化をしません。

で、ここで問題は、例えば以下のような場合。

  • .NET 4.5.2 をターゲットにした C# プロジェクトを作る
    • 実際に動かすのは .NET 4.5.2 が入ったサーバー上
  • .NET 4.5.1 と、.NET 4.6 しか入っていないビルド マシンでビルドする

ここで、ビルド マシン上でmsbuildは、「4.5.2 がないから代わりに 4.6を使う」という判定をするらしく、Roslynの方には .NET 4.6 がわたってしまう模様。その結果、4.5.2をターゲットにしているにも関わらず、Array.Empty最適化がかかってしまう。そうなると、サーバー上では動作しなくなる(「Array.Emptyは見つかりません」エラーが出る)という問題。

取れる対処

ということで、これはRoslynの問題か、msbuildの問題か、みたいな話に。

根本解決にはmsbuild側を直す必要があるということで、このissueはCloseされて、msbuild側に問題レポートが飛びました。要するに、

Roslyn 側でできそうな対処は、プロジェクトで指定したターゲットと実際にわたってきた.NETアセンブリのバージョンが違ったら警告なりエラーなり出すというのなんですが、現状、msbuild側から.NETのバージョンを受け取ってないので不可能とのこと。なので、msbuild側からその.NETバージョンを渡してもらうようにしないとダメかもという。

JIT レベル最適化しないのか

こんな最適化、コンパイラーのレベルでやるからビルド マシン上のインストール状況に左右されるんじゃないかなんて話も出ています。

要するに、new T[0] があったらそれをことごとく Array.Empty<T>() に置き換える最適化をJITレベルでやればこんな問題起きないし、より一層性能的に有利なんじゃない?という。ですが、実際のところ、この JIT レベルでの最適化は結構深刻な互換性問題を引き起こすので無理とのこと。

世の中、new T[0] で毎回新しいインスタンスが作られる前提でしか動かないコードが結構残っているはずで、これを Array.Empty<T>()(シングルトンな単一インスタンスを常に返す)に置き換えると不具合を起こす。

「そんなコードどのくらいあるの?」と思うかもしれませんが、CTP の頃に一度 Enumerable.Empty<T>() が返すインスタンスをシングルトンに変えたら不具合が出て互換性問題レポートが来まくったとかいう事実もあったりします。

ということで、過去のコードを壊さないようにするためには、new T[0]Array.Empty<T>() に置き換える処理は状況を見て限定的に行わないといけなくて、そういう状況判断はJITレベルでは無理。なので、Roslyn側でやるしかないという話だそうです。

match ステートメント

[Proposal]: match construct for pattern matching #5016

今 C# 7.0に向けて実装が進んでいるパターン マッチングと関連して、switch 的な複数条件分岐をどうしようという話。結構前々からこの議論は進んでるんですが、まとめ的な issue ページが立ってたので紹介。

とりあえず、背景にある要望としては、以下の2点。

  • パターン マッチングに対応した分岐構文が必要
  • ステートメントじゃなくて式にしてほしい

これに対して、提案できる文法は2方針があって、どうしようかという議論が出ています。

  • switch ステートメントを拡張する
  • switch ステートメントとは別に、match 式という新構文を導入する

switch を使いまわせば、キーワードを増やす必要はないし、「switchはオワコンだから使うな」みたいな黒歴史ができなくて済むかもしれない。

一方で、同じswitchなのに全然違う構文がかけたらそれはそれで混乱しそうだし、互換性のために残っている「旧switch構文」は結局のところ黒歴史化しそうだし、既存の構文が足かせになって(例えばbreak必須とか、case 1: case 2: ...みたいなラベル連続時だけ許されるフォールバック機構とか)いまいちな文法にしかできないかもしれない。

non-nullable 参照型

久々に C# Design Meeting の議事録の投稿がありました。7・8月あたりは non-nullable 参照型に関する話をしてたみたいです。

特に、7/7 はEric Lippert (元C#チーム、現在は静的解析ツール ベンダーの Coverity 社員)を「honored guest」(名誉あるお客様)に呼んで。

おおむね方向性固まってきた模様。

T! みたいな型は作らない

T で non-nullable、T? で nullable にする方向。

既存コードの意味が変わってしまうという問題に対しては、以下のように対処。

  • 規定動作ではエラーではなく警告にする
  • アセンブリ単位で「T が non-nullable」かどうかの動作を変えるオプションを用意する
    • アセンブリ レベルの属性指定で動作を切り替える方式になりそう

アセンブリ単位の動作変更

例えば、A, B という2つのライブラリを利用するコードを書く場合、以下のようなシナリオがあり得る。

  • まず、B だけが C# 7.0 に更新して、null チェックが入った
  • 利用側が「C# 7.0 化はしたいけど、null チェック入れて回るほどの工数とれず、とりあえずバグがあるかもしれないのは承知の上でとりあえずビルド通したい」みたいな状態になる
  • 余裕ができたので利用側にも null チェックを入れれるようになる
  • 今度は A にも null チェックが入った
  • 利用側はやっぱりすぐには更新作業できない

こういうのを考えると、アセンブリ単位で「null チェックが入ったかどうか」「null チェックが入ったライブラリを使っている際に、自身はどう振る舞うか」を切り替えれないとまずいとのこと。

警告

「警告なんだしそこまで細かいことしなくても…」と思うかもしれませんが。C# には「警告をエラーとして扱う」オプションがあって、このオプションを利用している人は結構います。なので「警告を足す」というのも、十分に破壊的変更。実際、過去に、警告を足したらユーザーから文句を言われたことがあるそうです。

non-nullable 型の配列

配列は、new T[] した時点で中身はすべて null のはずなので、non-nullable な T に対して new T[] を認めるかどうかは結構難しい問題。

とりあえず現状は認める方向で考えてるみたいです。コンストラクターの中での初期化を必須としたり、要素の get するまでに set を必須にしたりというのはできる範囲でチェックする予定。

ジェネリック

class X<T> があったときに、 new X<string>() とか new X<string?>() とかをどうするか。

class 定義側で T? を使っているかどうかによって、

  • 定義側で1つも T? を使っていない → 利用側は non-nullable でも nullable でも OK
  • 定義側で1つも T? がある → 利用側は non-nullable でないと警告
  • 定義側で T? しか使っていない → 利用側は non-nullable でも nullable でも OK

にするとのこと。

あと、ジェネリック型引数に限り、T! みたいな「non-nullable型」を認めるとのこと。T! は、

  • T が元々 non-nullable なら、T!T そのもの
  • T が nullable なら、T!Tnon-nullable

という意味。FirstOrDefaultみたいな、参照型にも値型にも使われて、かつ、non-nullable な戻り値を返したいメソッド用。

null チェック演算子

nullable 型から non-nullable 型に変えるには、

  • ?? で代替値を与える
  • null だったら例外を投げる

のどちらかをやればいいわけですが、この後者にも専用に演算子を作るかもしれないみたいです。今のところ出ている案は後置きの ! 演算子。

さらに、null を伝搬させる null 条件演算子 ?. みたいに、null だったら例外を起こしつつメンバー アクセスする !. 演算子も一緒に作るかもしれないとのこと。