ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) 小ネタ $ 文字列中の三項演算子

小ネタです。

はてなブログC# の linter が古いのか、シンタックスハイライトがめちゃくちゃになってますが気にしないでください。

string Foo(int? num)
{
    // これはコンパイルエラー
    return $"The num is {num == null ? "NULL" : num.Value.ToString()}.";
}

string interpolation ($文字列) の中に三項演算子は書けないです。もしこれを書けるように認めると、コンパイラ構文解析のコストが高くなりすぎるらしいです、たしか。

悲しいですね。

なのでコンパイラの気持ちになってあげましょう。

string Foo(int? num)
{
    // 全体を () で包むとコンパイルが通る
    return $"The num is {(num == null ? "NULL" : num.Value.ToString())}.";
}

コンパイラにここまでが値だよ~と教えてあげるために全体をカッコで包んであげましょう。はい、優しさに包まれましたね。

(C#) interface/core パターンによるポリモーフィズム

継承の濫用による設計の矛盾

コード共通化のためだけの継承の濫用が設計の矛盾を生むというのを分かっている方は、下の問題設定だけ見て、次の項まで読み飛ばしてください。

問題設定

  • 人間・犬・猫には「歩く」という共通の振る舞いがあり、現在の「位置」を持っている。
  • 全ての生き物はデフォルトでは速度1で歩くが、犬と猫は速度2で歩く。
  • 猫は昼寝をし、昼寝を始めると歩かない。

C#er 的には自然な考えとして、まずは継承で実装してみましょう。

public abstract class Walkable
{
    public int X { get; protected set; }
    public virtual void Walk() => X += 1;
}

public class Human : Walkable{ }

public class Dog : Walkable
{
    public override void Walk() => X += 2;
}

public class Cat : Walkable
{
    public bool IsSleeping { get; private set; }
    public override void Walk()
    {
        if(IsSleeping == false) {
            X += 2;
        }
    }
    public void Sleep() => IsSleeping = true;
}

普通ですね。ここで犬と猫でX += 2の部分が同じです。現実的なコードではもっと複雑な実装だとすると、二か所に同じことを書くのはメンテナンス性に欠けるため、共通化したくなります。この時、何も考えていないと継承を使ってしまいがちで、これは継承の濫用です。以下、継承を濫用したよくない実装です。

// Walkable と Human は先ほどと同じ

public class Dog : Walkable
{
    public override void Walk() => X += 2;
}

public class Cat : Dog
{
    public bool IsSleeping { get; private set; }
    public override void Walk()
    {
        if(IsSleeping == false) {
            base.Walk();    // Dog の Walk() メソッドを呼ぶ
        }
    }
    public void Sleep() => IsSleeping = true;
}

X += 2という犬と猫に共通な処理は犬の中に実装されて共通化されています。そして「犬を継承した猫」という自然界の冒涜、超生命体が爆誕しています。ありえません。もし逆に猫を犬が継承したとしても、今度は犬も昼寝をできることになってしまいます。ついでに猫を継承したキメラ犬が降臨するのも解消されません。

もし継承でコードを共通化したいのであれば、速度2で歩く抽象クラスを間に挟んで実装すると正しく実装することができます。しかし、ここでは別の方法として interface を使い、かつコードを共通化する方法を示したいと思います。

interface と実装の分離

継承ではなく interface を使って、かつコードを共通化する方法を示します。

// 共通の振る舞いを表す interface
public interface IWalkable
{
    int X { get; }
    void Walk();
}

// 生き物のデフォルトの IWalkable 実装
public struct DefaultWalkableCore
{
    public int X { get; set; }
    public void Walk() => X += 1;
}

// 犬と猫用の IWalkable 実装
public struct DogCatWalkableCore
{
    private DefaultWalkableCore _baseCore;
    public int X { get => _baseCore.X; set => _baseCore.X = value; }
    public void Walk() => X += 2;
}

// 人間
public class Human : IWalkable
{
    private DefaultWalkableCore _core;

    public int X => _core.X;
    public void Walk() => _core.Walk();
}

// 犬
public class Dog : IWalkable
{
    private DogCatWalkableCore _core;

    public int X => _core.X;
    public void Walk() => _core.Walk();
}

// 猫
public class Cat : IWalkable
{
    private DogCatWalkableCore _core;

    public bool IsSleeping { get; private set; }
    public int X => _core.X;
    public void Walk()
    {
        if(IsSleeping == false) {
            _core.Walk();
        }
    }
    public void Sleep() => IsSleeping = true;
}

振る舞いは interface で定義し、その具体的な処理 (Walk()) は各クラスには書かず、実装用の別の構造体に書いておきます。そして、各クラスからはその実装を呼びます。犬と猫が速度2で歩くというコードも共通化されており、猫は昼寝をするという要件もきちんと満たしています。

例ではDogCatWalkableCoreDefaultWalkableCoreを持たせていますが、メモリレイアウト的に同じなので

public struct DogCatWalkableCore
{
    public int X { get; set; }
    public void Walk() => X += 2;
}

のようにして、ベースになる実装を持たせずに書いてもよいです。

このパターンによる実装は継承よりも柔軟で、継承の場合 Human, Dog, Cat は class である必要がありますが、構造体にすることもできます。また、別の interface と core を追加で持たせることもできるため、多重継承のようなことも実現できます。大規模なプログラムの場合、そもそもクラス間に継承関係を持たせること自体かなり慎重に設計しないと負債になりがちですが、それを避けられます。

この interface/core パターンは私が勝手に考えたわけではありません。現在の C# の非同期処理の標準実装であるValueTaskを動かすためのIValueTaskSourceという interface があるのですが、そこに inteface/core パターンが使われています。(逆に言うとそれ以外で典型的にこのパターンが使われているのを見たことがありません、知っていたら教えてください。) このIValueTaskSourceの実装オブジェクトとしてManualResetValueTaskSourceCoreという構造体があり、core という名前もこれにならってつけています。

継承をすることなく別の型から機能だけを取り入れているという意味では mixin と言ったほうが近いかもしれません。

多段に呼び出しを経由しているのでオーバーヘッドが発生して遅いのではないかと一見思いますが、core のメソッドの呼び出しはメソッドのサイズが小さいのでほぼ間違いなくインライン展開されて、実行時には消えます。また、core を構造体実装しているため、メモリレイアウトも継承で書いた場合と同一になり無駄がありません。(メモリレイアウトについては普通は気にする必要はありません)

他言語でのポリモーフィズム

C# 以外の言語に目を向けると、Rust にもポリモーフィズムの概念はあります。ところがオブジェクト指向言語ではないのでオブジェクト指向言語的な継承はありません。つまり、「ポリモーフィズム ≠ 継承」です。これは重要なことなので明確にしておきます。実装を提供するxxxCoreという構造体と、振る舞いを表す interface を組み合わせた前述の実装は、実は Rust の trait/impl と全く同じです。Rust の trait/impl にちなんで、私が interface/core パターンと勝手に呼んでいますが、名前はないので特に何でもいいです。

OOP 言語などメインで使っている人には「継承なんてバグの温床、オブジェクト指向負の遺産」という過激派な人もいますが、この意見には私は否定的です。目的はポリモーフィズムの実現であって、継承やオブジェクト指向はその道具にすぎません。正しく使えば継承は強力な機能であり、正しく使えていないから壊れるのです。

とはいえ Rust の trait/impl は合理的で、良いものは C# にだって取り入れられます。しかし、これが C# において継承を駆逐するとは思いません。継承には継承の価値があり、どちらを使うかは設計と実装者の腕次第だと思います。

おまけ

前述の例でHuman, Dog, Cat を構造体にすることもできると書きましたが、構造体に interface 経由でアクセスすると boxing によって不要なコストが発生します。これは、interface で受け取らずに型制約付きジェネリクスで受け取ることで回避できます。

// interface で構造体を受け取ると boxing が起こる
public void Foo(IWalkable walkable)
{
    // 何か処理をする
}

// 型制約付きジェネリクスで受け取ると boxing が起こらない
public void Foo<TWalkable>(TWalkable walkable) where TWalkable : IWalkable
{
    // 何か処理をする
}

使っていきましょう。

(C#) Source Generator で typeof の型名を取得する

C# 9.0 から使える Source Generator、良いですね。アプリケーションを作る側の人が Source Generator を作ることは少ないと思いますが、ライブラリを作る側の人には便利ですよね。

基本的な使い方・作り方はここでは解説しません。公式の説明公式サンプルを見たり、調べたりすれば出てきます。少ないのが残念なのですけれども。

Source Generator で属性を作って、属性の情報からコードを生成したりするというのは典型的な使い方ですね。

// ユーザーコード
[GenerateProperty(typeof(SomeType), "Prop1")]
partial class TargetType
{
}
// ------------------------
// 生成されたコード
partial class TargetType
{
    public SomeType Prop1 { get; set; }
}

上記のサンプルコードに特に意味はありません、ただの説明用の例です。GeneratePropertyという属性を使ってプロパティを自動生成してくれるソースジェネレーターを想定してください。(GeneratePropertyという属性自体もソースジェネレーターが生成した属性です。BCL にこんな属性があるわけではないです。)

シンタックスの読み取りはISourceGeneratorから提供される Roslyn の API を使うしかないのですが、ネットで検索しても情報が少なすぎて非常に苦労します。日本語で検索してもほとんど出てきません。英語でもかなり情報は少ないです。

このソースジェネレータ―を作るためには、typeof(SomeType)の部分から"SomeType"を読み取ってくる必要があるのですが、その方法の情報がなさ過ぎて苦労したので覚書です。

以下、ソースジェネレータ―の実装の一部。(例なので雑です。実際はもうちょっとちゃんと書いてください)

[2021/02/01 追記] サンプルコードと説明が色々と間違っていました。完全に別のものと勘違いしていました。以下、正しく書きなおしました。

private const string AttributeDef = 
@"namespace Sample
{
    internal sealed class GeneratePropertyAttribute : System.Attribute
    {

    }
}
";

private readonly Regex _attrRegex =
    new Regex(@"^(global::)?(Sample\.)?GenerateProperty(Attribute)?$");

public void Execute(GeneratorExecutionContext context)
{
    // ここで最初に属性自体を出力する (略)

    // シンタックスを総なめして属性を探す
    var compilation = context.Compilation;
    var attrs = compilation
        .SyntaxTrees
        .SelectMany(s => s.GetRoot().DescendantNodes())
        .Where(s => s.IsKind(SyntaxKind.Attribute))
        .OfType<AttributeSyntax>()
        .Where(s => _attrRegex.IsMatch(s.Name.ToString()))
        .ToArray();
    foreach(AttributeSyntax attr in attrs) {
        var attrSemantic = compilation.GetSemanticModel(attr.SyntaxTree);
        var expr = attr.ArgumentList.Arguments[0].Expression;

        // これが typeof の中身の型のフルネーム "SomeNamespace.SomeType"
        string typeName = attrSemantic
            .GetSymbolInfo((expr as TypeOfExpressionSyntax).Type)
            .Symbol!.ToString();

        // ↓ 間違い
        // string typeName = attrSemantic.GetConstantValue(expr).ToString();

        // 以下略 (生成コードの出力)
    }
}

SyntaxReceiverを使う方法もありますが、私はサクッと LINQ で syntax tree を全部なめるのが好きなので LINQ を使いました。どっちが良いのかはわかりませんが、特に問題はないでしょう。

ここで取得できる型名の文字列は、書いたソースコードの expression がそのまま取得できるので、typeof(SomeNamespace.SomeType)と書いた場合は"SomeNamespace.SomeType"となります。

セマンティクスをきちんと考慮してくれるため、typeof(SomeType)と書かれていた場合でも、きちんと名前空間付きのフルネーム"SomeNamespace.SomeType"を取得できます。

属性引数にtypeofを指定するのはけっこう頻出パターンだと思うので、日本語で情報があると助かる人が世の中に1人ぐらいはいるんじゃないかな。(いてほしいなぁ)

(C#) 型の外部から疑似的に interface を追加する

interface を後付けしたい

interface は C#ポリモーフィズムを実現するために、メソッド・プロパティの宣言だけを決めておき、実際の挙動は型の実装者に任せるというものです。今更説明するまでもないですね、普段みなさんが使ってるやつです。

普通は型定義の段階で interface を継承させて使うのですが、例えば外部ライブラリで定義されている型に自作の interface を継承させることはできません。

public class Apple  // 外部ライブラリのクラス
{
    public string Name => "apple";
}

public class Orange  // 外部ライブラリのクラス
{
    public string Name => "orange";
}

public interface IName  // 自作の interface
{
    string Name { get; }
}

上記のようなAppleOrangeクラスが外部ライブラリにあって、これらを統一的に扱うためにINameという自作の interface をつけたいとしても、当然外部ライブラリ内にあるクラスに勝手に継承を追加させることはできません。

 

今回は、これをローカル関数と関数ポインタ (と必要ならジェネリクス) を使って疑似的に実現させます。

public unsafe readonly struct IName
{
    private readonly object _obj;
    private readonly delegate*<object, string> _func;

    public string Name => _func(_obj);

    private IName(Apple apple)
    {
        _obj = apple;
        _func = &GetName;
        static string GetName(object obj) => Unsafe.As<Apple>(obj).Name;
    }
    
    private IName(Orange orange)
    {
        _obj = orange;
        _func = &GetName;
        static string GetName(object obj) => Unsafe.As<Orange>(obj).Name;
    }

    public static implicit operator IName(Apple apple) => new IName(apple);
    public static implicit operator IName(Orange orange) => new IName(orange);
    public static explicit operator Apple(in IName iName) => (Apple)iName._obj;
    public static explicit operator Orange(in IName iName) => (Orange)iName._obj;
    // サンプルなので Equals とか GetHashCode とかの override は省略
}

コンストラクタ内で、元になるオブジェクトと実装を呼ぶローカル関数の関数ポインタをフィールドに格納しておき、疑似インターフェースの定義したプロパティの呼び出し時に、関数ポインタ経由で処理を呼びます。ついでに、対象となる型とのキャストを作っておきます。

ローカル関数内で使っているUnsafe.As<Apple>(obj)(Apple)objと同義ですが、一切の安全チェックを省いた最速のキャストです。本来は危険なのですが、今回の場合はobjは絶対に対象の型であることが静的に保証できているため問題ありません。

一見意味不明ですが、短いですのでよーくコードを眺めてください。理解できるはずです。(というかこれ以上説明することがない)

 

原理としては、元になったオブジェクトの型によって関数ポインタ経由で処理を変えています。ここでよく考えてみてください、これって仮想関数テーブルから間接参照で処理に飛ぶというインターフェースの原理そのものですよね。

あたかも本物の interface のように振舞い、しかも原理まで同じなのでパフォーマンスもほぼ同等です。

Apple apple = new Apple();
IName iName = apple;    // 暗黙的キャスト

一応、INameという型名にしましたが、これは interface のプレフィックスである Iを冠していいのかは不明です。何しろ interface ではなく構造体ですので。というか、こんな実装方法はたぶん想定されていないため、命名方法の慣習も何もありません、無敵です。

ジェネリクス型にジェネリクス挙動をさせる

ここからは、先ほどまでとは別のことをします。記事のタイトルとして「型の外部から interface を追加する」というタイトルをつけましたが、本来この記事で言いたかった内容は「ローカル関数×関数ポインタ×ジェネリクスによるハックな実装」です。型の外部から interface を追加するのはその一例です。

では、先ほどと似た原理を使って、非ジェネリクス型にジェネリクス (のような) 挙動をさせてみましょう。

public unsafe class MyClass
{
    private readonly delegate*<void> _say;

    private MyClass(delegate*<void> say)
    {
        _say = say;
    }

    public void SayGenericsType() => _say();

    public static MyClass Create<T>()
    {
        if(typeof(T) == typeof(int))
            return new MyClass(&SayInt);
        else if(typeof(T) == typeof(double))
            return new MyClass(&SayDouble);
        else
            return new MyClass(&SayOtherTypes);

        static void SayInt() => Console.WriteLine("I am int");
        static void SayDouble() => Console.WriteLine("I am double");
        static void SayOtherTypes() => Console.WriteLine($"I am {typeof(T).FullName}");
    }
}

上記のMyClassジェネリクス型ではないですが、SayGenericsType()メソッドを呼ぶとジェネリクス挙動をします。

var obj1 = MyClass.Create<int>();
var obj2 = MyClass.Create<double>();
var obj3 = MyClass.Create<string>();

obj1.SayGenericsType();   // "I am int"
obj2.SayGenericsType();   // "I am double"
obj3.SayGenericsType();   // "I am System.String"

コンストラクタに型引数<T>を宣言することはできないため、Createという静的メソッドをファクトリーメソッドとしています。ファクトリーメソッド内で型によって関数ポインタを変えることで、挙動を操っています。そして、このたくさん並んだif分岐は JIT コンパイル時に分岐が消えます。これについては以前の記事 (3種類の定数と JIT コンパイル時最適化) で解説しているため、気になった方は読んでください。

これのすごい所は実行時型Typeを保存しているわけではなく、ジェネリクス<T>を静的型制約として復元できる点です。上記のobj3は生成時にstringを指定していますが、後でメソッドを呼ぶときにはどこにもstringと指定していません。にもかかわらず、SayGenericsType()メソッドの呼び出し時にジェネリクス型が復元されます。状態をインスタンスに保持するという点で、これはラムダ式の変数キャプチャに似ています。

実際、これはCreateメソッドの<T>がローカル関数のSayOtherTypesに入り込んでいる (これをキャプチャと呼んでいいのかどうかはわかりません) のですが、<T>のキャプチャがラムダ式の変数キャプチャと違う点は、パフォーマンス上のコストがないことです。変数キャプチャが動的状態 (値) をインスタンスに保存するのに対し、<T>のキャプチャは静的状態 (型制約) をインスタンスに保存するのです。

ローカル関数にジェネリクス<T>をキャプチャしつつ関数ポインタに保持し、オブジェクト生成時の<T>インスタンスに保存する、という C# の言語機能をパズルのように組み合わせた何とも珍妙な実装です。記事タイトルを「ローカル関数×関数ポインタ×ジェネリクスによるハックな実装」と題したかった意味が分かっていただけますでしょうか……?

ISpan<T>を作る

この記事の本命です。配列T[]やリストList<T>Span<T>を生成することができます。(公式にList<T>からSpan<T>を生成できるのは .NET5 以降からですが、ちょっとUnsafeを使うとそれ以前のバージョンでもできます。詳しくは過去記事を参照)

もし、C# 1.0 からSpan<T>が存在していたら、おそらく以下のようなISpan<T>というインターフェイスが存在したことでしょう。(少なくとも、私が C# の言語設計者なら作ります。)

public interface ISpan<T>
{
    Span<T> AsSpan();
}

ところが、Span<T>自体が割と最近登場した型で、残念ながらそんなものは存在しません。そこで、ここまで解説してきた外付け疑似インターフェースを作ります。

public unsafe readonly struct ISpan<T>
{
    private readonly object _obj;
    private readonly delegate*<object, Span<T>> _asSpan;

    public Span<T> AsSpan() => _asSpan(_obj);

    private ISpan(T[] array)
    {
        _obj = array;
        _asSpan = &AsSpan;
        static Span<T> AsSpan(object obj) => Unsafe.As<T[]>(obj).AsSpan();
    }

    private ISpan(List<T> list)
    {
        _obj = list;
        _asSpan = &AsSpan;
        static Span<T> AsSpan(object obj)
        {
            return CollectionsMarshal.AsSpan(Unsafe.As<List<T>>(obj));
        }
    }

    public static implicit operator ISpan<T>(T[] array) => new ISpan<T>(array);
    public static implicit operator ISpan<T>(List<T> list) => new ISpan<T>(list);
    public static explicit operator T[](in ISpan<T> iSpan) => (T[])iSpan._obj;
    public static explicit operator List<T>(in ISpan<T> iSpan) => (List<T>)iSpan._obj;
    // サンプルなので Equals とか GetHashCode とかの override は省略
}

原理は先ほどまでと同じなので分かるかと思います。これで、配列とリストを「Span<T>を生成することができる」という意味のインターフェースで共通に扱えますね。やったね。

 

そして、今回のオチです。このコード、本記事執筆現在の最新のコンパイラ (Visual Studio 2019 16.8.2) でコンパイルエラーになります。文法、実装内容ともに完全に正しい C# のコードです。まさかのコンパイラのバグを踏みました。

エラー内容は驚きの「CS8751 C# コンパイラで内部エラーが発生しました。」です。初めて見た。

おそらく、関数ポインタdelegate*の戻り値としてSpan<T>を指定しているのがまずいようです。(現状、言語仕様としては禁止されていません。) 関数ポインタもref structもどちらも最近 C# に入った新しめの機能なので枯れておらず、言語設計の未検討の仕様を突いてしまったと思われます。腐ってやがる。早すぎたんだ。

一応 roslyn のリポジトリに issue を出しました。

github.com

[2021/7/24 追記] バグは修正されて、最新のコンパイラでは正常にコンパイルできます。

以上、外付けできる疑似 interface の作り方と、ローカル関数×関数ポインタ×ジェネリクスなミラクル実装でISpan<T>を作ろうとしたらコンパイラのバグを踏んだ話でした。。。

[追記] Memory<T>について

ご存知かと思いますが「Span<T>を生成するもの」という意味において、似たような機能のものとしてSystem.Memory<T>が公式で存在しています。しかし、私はあれが嫌いです。オーバーヘッドが大きくて遅い。独自型をMemory<T>対応させるためにはMemoryManager<T>を使うのですが実装がイケてなさすぎる。

私のライブラリで UnmanagedArray というライブラリがあるのですが、あれがMemory<T>に対応していないのもそんな理由です。

なら、私ならどう実装するのか?と言われると、目的に応じてケースバイケースなので回答を1つには出せませんが、答えの一つが先程の疑似ISpan<T>です。アンマネージドメモリの生ポインタとかも考慮に入れて考えると、あれでは boxing を挟んでしまうのでやや不十分なのですが、そこはケースバイケースという便利な単語でお茶を濁します。

(C#) 3種類の定数と JIT コンパイル時最適化

この記事は「C# Advent Calendar 2020」の24日目の記事です。

3種類の定数と分岐削除

C# には3種類の定数があります。とはいえ、実際に C#ソースコードとして目に見えるのは1種類だけでconstとして書くものです。それぞれ順に見ていきましょう。

C#定数 (const)

最も一般的な定数はconstキーワードで定義するものです。

void Hoge()
{
    const int a = 5;
    if(a == 5)
        Console.WriteLine("a is 5");
    else
        Console.WriteLine("a is not 5");
}

constC# の静的なソースコードの状態で定数であることを表現します。上の例ではconstは変数として定義されますが実際にはコンパイルすると変数はなくなり、aリテラル5が直接埋め込まれます。静的に5であることをコンパイラが認識できるため、上記のifの分岐はなくなり、Console.WriteLine("a is 5");だけが残ります。

IL 定数

次に IL 定数です。const キーワードをつけることはできませんが、コンパイル時に定数であることが確定しているものです。

string Foo()
{
    const string a = nameof(Foo);
    string b = $"{a}{a}";  // not 'const' but IL const
    return b;
}

一番わかりやすい IL 定数は string interpolation ($文字列) です。まずnameof演算子は右辺値constですので、上記のaC#定数です。そしてbconstではありません。(constにするとコンパイルエラー)

しかし、bコンパイル時に"FooFoo"であることが確定しています。したがって、コンパイル後の IL を見ると、直接定数で"FooFoo"が埋め込まれていることが確認できます。コンパイル後の IL は以下の通りです。

// IL
.method private hidebysig instance string Foo () cil managed 
{
    .maxstack 8
    IL_0000: ldstr "FooFoo"
    IL_0005: ret
}

みごとに string interpolation が消えて"FooFoo"が IL 内で定数となっているのがわかります。

では、C# 定数の時と同じように分岐が消えるのかどうか確認してみましょう。以下のコードを見てください。

int Bar()
{
    return $"{nameof(Bar)}{nameof(Bar)}" == "BarBar" ? 1 : 0;
}

このメソッドは必ず1を返すことは見て分かるかと思います。 ではこれをコンパイルした IL を見てましょう。

// IL
.method private hidebysig instance int32 Bar () cil managed
{
    .maxstack 8
    IL_0000: ldstr "BarBar"
    IL_0005: ldstr "BarBar"
    IL_000a: call bool [System.Private.CoreLib]System.String::op_Equality(string, string)
    IL_000f: brtrue.s IL_0013    // if 分岐

    IL_0011: ldc.i4.0
    IL_0012: ret

    IL_0013: ldc.i4.1
    IL_0014: ret
    }

あれ……?分岐が消えていませんね。上記の IL_000f のbrtrue.s命令が C#ifに相当します。一応 JIT コンパイル結果の x86-64 アセンブリまで見ておきましょう。

アセンブリを見てスラスラ理解できる宇宙人の方は良いですが、人間には厳しいので隣に高級言語的な疑似コードをつけておきました。

; x86-64 asm (on 64 bits)
Bar()
    L0000: sub rsp, 0x28
    L0004: mov rcx, 0x287a79f8938
    L000e: mov rdx, [rcx]
    L0011: mov rcx, rdx
    L0014: call System.String.Equals(System.String, System.String)
    L0019: test eax, eax            ; if("BarBar" == "BarBar")
    L001b: jne short L0024          ;     goto L0024 ┐
    L001d: xor eax, eax             ;                |
    L001f: add rsp, 0x28            ;                |
    L0023: ret                      ; return 0       |
    L0024: mov eax, 1               ;             <--┘
    L0029: add rsp, 0x28            ;
    L002d: ret                      ; return 1;

やっぱり分岐が消えていませんね。理想的にはただ1を返すだけのアセンブリになっていてほしいのですが、そうなっていません。これはおそらく単にC#コンパイラ、あるいは JIT コンパイラの最適化性能が不十分なだけです。どちらのレイヤーでこの最適化を解決するかは議論の余地がありますが、次期C#バージョン (少なくとも C# 10 以降) では C# コンパイラのレイヤーで解決するようです。

次期バージョンでは const のみを含む string interpolation はconstにできるようです。つまり、上記のBar()メソッドを JIT コンパイルすると

; x86-64 asm (on 64 bits)
Bar()
    L0000: mov eax, 1
    L0005: ret

となり、完全にリテラルの1を返すメソッドに最適化されます。

余談

現状の C# では string interpolation を使わずに+演算子const stringを繋ぐと、完全な最適化がされて1だけを返すメソッドになります。

int Bar()
{
    return nameof(Bar) + nameof(Bar) == "BarBar" ? 1 : 0;
}

つまり、これは string interpolation の最適化がただ甘いというだけの話です。

JIT 定数

JIT 定数は C# 定数や IL 定数よりもさらに制約が緩いもので、静的には定数として扱われません。実行時に JIT コンパイラが解釈し、実行環境の機械語になって初めて定数扱いされます。実際に見て見ましょう。

int Get3PointerSize()
{
    return IntPtr.Size * 3;
}
// IL
.method private hidebysig instance int32 Get3PointerSize () cil managed 
{
    .maxstack 8
    IL_0000: call int32 [System.Private.CoreLib]System.IntPtr::get_Size()
    IL_0005: ldc.i4.3        // 3
    IL_0006: mul             // IntPtr.Size * 3
    IL_0007: ret             // return
}
; x86-64 asm (on 64 bits)
Get3PointerSize()
    L0000: mov eax, 0x18
    L0005: ret                    ; return 24

上記のGet3PointerSize()メソッドはポインタ3つ分のバイトサイズを返すメソッドです。 IntPtr.Sizeは 64 bit 環境では8、32 bit 環境では4です。 つまり、64 bit 環境では24を、32 bit 環境では12を返します。

しかし、これは実行時に初めてその実行環境の値が決まるため、静的にはその値はわかりません。 したがって、IL 上では値を取得して乗算が行われているのがわかります。JIT コンパイルが行われて初めて定数が確定し、乗算を省略してリテラル値を返すメソッドになります。

では、JIT 定数についても分岐省略の最適化を見てみます。

void DumpRuntimeBits()
{
    if(IntPtr.Size == 4)
        Console.WriteLine("This is 32 bits runtime");
    else if(IntPtr.Size == 8)
        Console.WriteLine("This is 64 bits runtime");
    else
        Console.WriteLine("Unknown runtime");
}
; x86-64 asm (on 64 bits)
DumpRuntimeBits()
    L0000: mov rcx, 0x287a79f8970
    L000a: mov rcx, [rcx]        ; "This is 64 bits runtime"
    L000d: jmp System.Console.WriteLine(System.String)

JIT 後のアセンブリでは分岐が綺麗に消えていますね。 これは 64 bit 環境の JIT 結果なので、32 bit 環境 JIT の場合はif(IntPtr.Size == 4)の処理だけが残ります。

型と JIT と最適化

C# の実行時最適化を制御してこそ、JIT コンパイルを採用している C# の真の力を発揮できます。 JIT 定数としてIntPtr.Size以外に代表的な物はtypeof演算子です。 JIT 定数に焦点を当ててジェネリクスと組み合わせて使う方法について見ていきます。

 

ジェネリクス<T>は IL の状態では型引数<T>が抽象的なままメタな状態です。これが、実行時に実際に使われた型に合わせて機械語が生成されます。

例えば、List<T>の場合、List<int>.AddList<double>.AddJIT 後は完全に別のメソッドで、その関数ポインタの値は異なります。つまり、実行時にはあたかもList_intList_doubleという別の型が存在しているかのように解釈されます。

これと JIT 定数である typeofと組み合わせてみます。

public class Sample<T>
{
    public void DumpType()
    {
        if(typeof(T) == typeof(int))
            Console.WriteLine("int");
        else if(typeof(T) == typeof(double))
            Console.WriteLine("double");
        else
            Console.WriteLine("other types");
    }
}

// ------------------------
var intSample = new Sample<int>();
intSample.DumpType();        // >> int

JIT 後のアセンブリは省略しますが、この分岐は実行時には消え、一致する部分の処理だけが残ります。 したがって、ジェネリクスクラスあるいはジェネリクスメソッドの中で、特定の型に対してだけ処理を行う場合、typeofで if 文を大量に列挙するのが最適解です。

 

このように、JIT の仕組みと JIT 時最適化読みで施せる C# のパフォーマンスハックはいろいろとあります。 特に、ジェネリクスJIT は相性がいいため、いろいろとできます。また機会があれば記事を書こうかなと思います。

(例えば、分岐削除という観点ではないですが、ジェネリクスJIT の組み合わせとしては Static Type Caching なんかが有名ですね)

速すぎる最適化に意味はない、などとよく言われますが、そもそも知らない最適化は書けません。 より速い最適化方法を知っていてこそ、それを使うか使わないかの選択肢を選べます。

たまには速すぎる最適化で最高性能を出してみませんか?

それではよいクリスマスを。

(C#) Fast Span と Slow Span の挙動の不一致の罠

System.Span<T> および System.ReadOnlySpan<T>はメモリの連続領域を表すための配列ライクなオブジェクトです。 配列T[]およびそのスライス、スタックメモリstackalloc T[]、ポインタ (マネージドとアンマネージドの両方)を統一的に扱える便利なものです。

これらは .NET Core の時代に後から導入されたもので、これによって得られるパフォーマンス効果を100%使うためにはランタイムに修正が必要でした (Fast Span)。しかし、ランタイムに修正を加える前の古いランタイム (.NET Framework) およびランタイムを指定せず API が一致していれば使えるというライブラリ用の API 仕様である .NET Standard ではこれが使えず、本来のパフォーマンスを得られないが挙動だけ合わせたものとして提供されます (Slow Span)。

System.Span<T> および System.ReadOnlySpan<T>は Fast Span 環境では BCL に始めから入っており、Slow Span 環境では System.Memory の Nuget package で提供されています。本来ライブラリなのでパフォーマンスが劣る以外どちらも同じ挙動をすることが望まれるのですが、実は微妙に違いまして、Slow Span で使うと例外が発生するものがあります。

public unsafe struct Data
{
    public double* Pointer;
}

// -------------------------------------
// Create span (error !!!)
Span<Data> span = stackalloc Data[1] { new Data() };

実は上記のコードは Fast Span では実行できますが、Slow Span では例外を吐きます。 Span<T>に入れる型がポインタのフィールドを持っているとSpan<T>を作れません。ポインタの型は関係ありません。 これは再帰的に適用されるため、例えば上記のData構造体が別の構造体のフィールドを中に持っていて、それがポインタを持っている場合でも例外が出ます。

なら、Slow Span の環境ではポインタを持つ構造体をSpan<T>に入れられないのかというとIntPtrを使うと回避できます。

public unsafe struct Data2
{
    private IntPtr _pointer;
    public double* Pointer
    {
        get => (double*)_pointer;
        set => _pointer = (IntPtr)value;
    }
}

// -------------------------------------
// Create span (OK)
Span<Data2> span = stackalloc Data2[1] { new Data2() };

フィールドとしてポインタを直接持っていなければいいので上記のようにIntPtrに置き換えると例外が出ません。 ここでIntPtrの内部を知っている人からすれば、逆に混乱します。IntPtrは内部にvoid*のフィールドを持っているだけのラッパー構造体にすぎないため、先ほどの再帰的に適用されるという説明に矛盾します。

これは Slow Span のソースコードを確認したわけじゃないため推測ですが、おそらくIntPtrだけを特別扱いしていると思います。

個人的には Slow Span のレガシー環境を使わないので知ったこっちゃない!むしろ早く世の中全員ランタイム移行してくれ~って感じなんですが、まあそういうわけにもいかないので。

ランタイムを指定しない場合のライブラリなんかを作る場合は注意が必要ですね。

(C#) ランタイムが Blazor WebAssembly か否かを取得する

実行中のOS が何であるかを取得する時はSystem.Environment.OSVersion.Platformを使えそうかと思ってました。

using System;

// Windows 10 なら "Win32NT"
Console.WriteLine(Environment.OSVersion.Platform);

ところが、これを Mac .NET Core で取得すると"MacOSX"ではなく"Unix"と返ってきます。 それはともかく、MSDNを見ると何とも説明が古臭いです。SilverlightとかXboxとかはまだしも、"Win32S"っていつの時代だ……私生まれてない。(知らなくてググったら16bitのOSがうんぬんかんぬん)

deprecated が多すぎる。

時代は進んで Blazor なんていうものが出てきて、そもそもそんなものは想定されていない。Blazor WebAssembly (client-side のやつ) でこれを実行するとUnixと出てきます。自分のことを Unix だと思い込んでいる一般人かな。

 

代わりにSystem.Runtime.InteropServices.RuntimeInformation.OSDescriptionを使います。

using System.Runtime.InteropServices;

// Blazor WebAssembly で実行すると "web"
Console.WriteLine(RuntimeInformation.OSDescription);

ちゃんと"web"と出てきました。WebAssembly か否かが取得できました。