Quantcast
Channel: ++C++; // 未確認飛行 C ブログ
Viewing all 482 articles
Browse latest View live

C# 8.0 Ranges

$
0
0

今日もC# 8.0の新機能の話で、今日のはすでに Visual Studio 2019 Preview 1に入っているやつです。

Ranges and Indicesと呼ばれていて、配列などに対して、 a[^i]で「後ろからi番目」とか、 a[i..j]で「i番目からj番目の範囲」とかを取り出せるようにする機能です。

正確にいうと、^iとかi..jとかの部分がC#の新機能で、 これらはそれぞれIndex型、Range型になります。 IndexRangeを受け取るインデクサーやメソッドはライブラリ側の機能です。 (ただし、配列だけは言語レベルで処理している模様。)

背景1: 統一ルールが欲しい

一旦先ほどの説明は忘れてまっさらな状態で、 例えば「3..5」と言われると何を思い浮かべるでしょう。 文脈次第だとは思いますが、以下のようなものがあり得ます。

  • 3, 4, 5 (5も含む)
  • 3, 4 (5は含まない)
  • 3, 4, 5, 6, 7 (3から初めて5つ)

どれがいいかは用途次第で、実際、どれもあり得ます。 例えば、.NET でも、以下のようなメソッドがあります。

var r = new Random();
var a = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
 
var x = r.Next(3, 5); // 3, 4 (5を含まない)
var s = a.AsSpan(3, 5); // 3, 4, 5, 6, 7 (3から始めて5つ)

ちょっとでもわかりやすくしたければ、以下のように名前付き引数にすべきかもしれません。

var x = r.Next(minValue: 3, maxValue: 5); // 「5つ」でないことは明確なものの、5を含むかどうかわからず
var s = a.AsSpan(start: 3, length: 5); // これなら割とわかりやすく「3から始めて5つ」

Random.Nextの例のように、名前が「max」だけで、「含むかどうか」がわからないAPIも多いです。 この区別のために、Parallel.Forなんかは引数名がfromInclusivetoExclusiveとかになっていたりします。 しかし、どんどん名前が長くなって書きづらい上に、 所詮は命名規約なので規約が守られない場合だってあり得ます。

さらにいうと、多次元データになるともっとしんどくなります。

var m = new[,]
{
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 },
};
 
// (x, y) が (1, 2) ~ (3, 4) の範囲?
// x が 1~2、y が 3~4 の範囲?
// 2, 4 は含む?含まない?
var n = m.Slice(1, 2, 3, 4);

ということで、範囲を表す専用の文法が欲しいという話になります。

背景2: インデックス用途

その両端を含むか含まないか問題ですが、どちらがいいかは正直用途によります。

例えば、x in 1..3 みたいに「x がその範囲に入るかどうか」(マッチング用途)の場合、 大体は「3も含む」の方にしたいという要望が多いです。 一方で、x[1..3]みたいに「xの1番から3番の要素」(インデックス用途)の場合、 「3を含まない」にした方が都合がよかったりします。 インデックス用途における「含まない」の利点は以下のようなもの。

  • 実装上、パフォーマンス的に有利
    • 長さを length = maxExclusive - minInclusive で計算できる(+1が要らない)
    • ループが for (var i = minInclusive; i < maxExclusive; i++) になる(<= だと int.MaxValueに対する特別扱いが必要)
  • i..iが空(0要素)範囲になる。「含む」の方だと空範囲がi..i-1になってちょっとキモい

C# 8.0で導入される範囲構文は、後者のインデックス用途を狙ったもので、「末尾は含まない」の方になります。

ちなみに、「範囲に入るかどうか」の方は別途パターン マッチングの一種(range pattern)として提供される可能性はあるんですが、 おそらく別の文法(x in 1 to 3みたいな)になりそうです。

一方、インデックス用途に絞ったことで、 「配列の末尾からi番目」を表したいという別の要望も出てきます。 そこで、^演算子を導入して、^iで「末尾からi番目」を表すことになりました。

文法

ということで、C# 8.0で導入されるのは以下のような文法です。

  • ^i 演算子で「末尾からi番目」を表す Index型を作る
    • 正確には「Length - i」を表す。^0Length番目なので、array[^0]は OutOfRange。
  • i..j 演算子で、「i番目からj番目」を表すRange型を作る
    • 開始の方(i)は含む、末尾の方(j)は含まない
    • 両端は省略可能。i..なら「iから末尾」、..jなら「先頭からj」、..なら「配列全体」
    • Indexを受け付ける。^3..なら「末尾から3要素」

ちなみに、RangeIndexはいずれもSystem名前空間の構造体です。

例えば以下のように書けます。

var data = new[] { 0, 1, 2, 3, 4, 5 };
 
// 1~2要素目。2 は exclusive。なので、表示されるのは 1 だけ。
Write(data[1..2]);
 
// 先頭から1~末尾から1。 1, 2, 3, 4
Write(data[1..^1]);
 
// 先頭~末尾から1。 0, 1, 2, 3, 4
Write(data[..^1]);
 
// 先頭から1~末尾。 1, 2, 3, 4, 5
Write(data[1..]);
 
// 全体。0, 1, 2, 3, 4, 5
Write(data[..]);

範囲構文

内部実装

実装としては以下のようになります。

  • ^inew Index(i, true)になる(第2引数のtrueが「末尾から」の意味)
  • 整数から Index へは暗黙の型変換がある
  • i..jRange.Create(i, j)になる
  • i..Range.FromStart(i)になる
  • ..jRange.ToEnd(j)になる
  • ..Range.All()になる
var r1 = Range.Create(1, 2);                  // 1..2
var r2 = Range.Create(1, new Index(1, true)); // 1..^1
var r3 = Range.ToEnd(new Index(1, true));     // ..^1
var r4 = Range.FromStart(1);                  // 1..
var r5 = Range.All();                         // ..

ちなみに、RangeIndexはそれぞれ、

  • Indexintを1つだけ持つ構造体
    • .NET の配列は負のインデックスを想定していないので、負の数を使って「末尾から」を表現
  • RangeIndexを2つ持つ構造体

になっています。

また、構文上は、^の方は単なる単項演算子、 ..の方は専用の構文(オペランドを省略可能というのが特殊なので、単なる2項演算子扱いにはできない)だそうです。

Rangeを受け付けるインデクサー

配列に対して a[i..j] と書いた時の挙動はちょっとまだもめているみたいです。 要は以下のどちらにすべきか。

  • 配列からは配列で「subarray」を返すべきではないか
    • 新しい配列のアロケーションとコピーが発生
  • アロケーションを避けるために Span<T> で返すべきではないか

Visual Studio 2019 Preview 1 での実装は前者になっていて、 new T[]Array.Copyが生成されます。 パフォーマンスを気にするならa.AsSpan()[i..j]と書く必要があります。


C# 8.0 null許容参照型

$
0
0

今日も C# 8.0 の新機能の話。 C# 8.0 の中でおそらく一番の目玉機能扱いになると思われる null許容参照型の話です。

参照型でもそのままでは null を認めない

要は、参照型に対しても、単にTと書くとnullを認めない型になり、 null許容にしたければT?と書くようにするという機能です。

#nullable enable
    // string には null が来ない
    // null が来ないなら s.Length で OK
    static int M1(string s) => s.Length;
 
    // string? には null が来る
    // null が来るのに s.Length (null チェックしてない)はダメ
    static int M2(string? s) => s.Length;
 
    // string? には null が来る
    // null が来ても ?. や ?? を駆使すれば OK
    static int M3(string? s) => s?.Length ?? 0;

null-forgiving

原理的に null を消せない場合もあります。 一例としては循環参照を作りたいときとかなんですが、 例えば以下のような感じで一時的に有効な値を持てない場合があり得ます。

class Node
{
    public Node Next { get; private set; }
    public Node(Node next) => Next = next;
 
    public (Node a, Node b) CircularDependency()
    {
        // 参照に循環があるとき、どうしても片方は最初から有効な参照にできない
        var a = new Node(null); // やむなくいったん null
        var b = new Node(a);
        a.Next = b;
 
        // メソッドを抜けるまでには有効な値を入れておくのでどうかご容赦願いたい…
        return (a, b);
    }
}

こういうとき、警告をもみ消す処理があると問題を回避できます。 そのための演算子が後置きの!

// 非 null なところに null を渡すのを容赦してもらう
var a = new Node(null!);
var b = new Node(a);
a.Next = b;

今のところ、この演算子は null-forgiving 演算子と呼ばれています。 (日本語だとどうするといいんだろう。直訳だと「null容赦」。) (ちなみに、口頭だとたぶん!をそのまま読んで、「びっくり演算子」(英語でも"bang"とか)呼ばれると思います。)

null-forgiving演算子はあくまで「警告になるコードを無視してもらう」という処理です (そこが許容(able)と容赦(forgive)の差)。 コンパイル結果には何も影響を及ぼさないので、 間違ったコードを書くと普通にNullReferenceExceptionが出るようになります。 (実行時のチェック処理とかは別に何も挿入されません。)

途中入り

機能名が「null許容参照型」になっている(非null参照型じゃない)のは、 何もつけないTが非nullで、null許容の方に?を付けるという文法を選んだからです。

ということは、 C# 7.x 以前であれば参照型Tはnullを許容していたわけで、 これまでとTの意味が変わります。 そのせいで、最初からT?を考えて言語設計していたなら必要なかったであろう苦労が少しあります。 具体的には、以下のような感じになっています。

  • 有効にするにはオプション指定が必要
  • エラーではなく警告ベース
  • 値型のT?と扱いが違う
  • ジェネリクスに対して使いづらい
  • 漏れがあり得る

オプション指定

何も指定しないと、C# 7.x までの動きと同様になります。 参照型 T には null があり得るし、T? とは書けません (警告が出るだけですが)。

null 許容参照型を有効にするには、以下の2通りのオプション指定の方法があります。

  • #nullableディレクティブ … ファイルの行単位でオン/オフ切り替え
  • NullableReferenceTypes タグ … プロジェクト全体でオン/オフ切り替え

前者は、#if#pragmaなどと同じプリプロセス命令です。 以下のように、#nullableを書いた行から先がオン/オフ切り替わります。

#nullable enable
    static void M1(string s)
    {
        // enable 時に string に null を代入したら警告
        s = null;
    }
 
#nullable disable
    static void M2(string s)
    {
        // disable にしたので string に null を代入しても何も言われない
        s = null;
    }

これらenabledisableに加えて、 リリース版までにはrestoresafeonlyというオプションも入るそうです。 (restoreは名前通り前の状態に戻すもの。 safeonlyの方はちょっと仕様(書きかけ)を読んだだけだとピンとこなかったので、 実装されたら改めて確認します…)

後者は、csproj に対して設定を書きます。 (最終的にはVisual Studio上の設定画面からもオン/オフができると思いますが、 現状ではcsprojを手書きする必要があります。)

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
    <NullableReferenceTypes>true</NullableReferenceTypes>
  </PropertyGroup>
 
</Project>

完全に1から書くプロジェクトの場合ならオンになっていて特に困ることもないので、 積極的にこのオプションを指定するといいと思います。 先日書きましたがDirectory.Build.propsに書くのもありかもしれません。

警告ベース

null 許容参照型がらみの違反は、全て警告になっていて、エラーにはなりません。 オプションでオン/オフできるとは言え既存のC#とはTの意味が変わるものなので、エラーにするのは怖いというのがあると思います。 また、後述するように漏れがあり得るので、 もしかするとエラーにしてしまうとまずい状況もあり得るかもしれません。

まあ、C# には「警告をエラーとして扱う」というオプションもあるので、 「null は絶対に許さない。慈悲はない」という方はcsprojにTreatWarningsAsErrorsを加えるといいと思います。

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
    <NullableReferenceTypes>true</NullableReferenceTypes>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
 
</Project>

ちなみに、「全部警告にするにしてもオプション指定必須、既定動作ではオフ」なのもこいつのせいです。 例え警告であっても、既存コードに対して警告を起こす変更は破壊的変更になります。

null 許容“値型”との差

T?といえば、C# には、2.0 の頃からnull許容型というものがあります。 本来絶対にnullがない構造体Tに対して、T?と書くことでnull許容にできる機能です。 C# 8.0で「null許容参照型」が入ってしまったので、 これまでの「null許容型」は区別のために「null許容“値型”」と呼び変えるようになると思います。

見た目はどちらも「Tに対してT?でnull許容」ですが、 結構扱いに差があります。

まず根本的な差として、内部的な実装方法が全然異なります。 値型の場合はTT?が明確に違う型ですが、 参照型の場合はT?も内部的にTになっていて、単に C# コンパイラーがフロー解析を頑張るだけになっています。

// null 許容値型は Nullable<T> 構造体が作られる
// T と T? は型システムのレベルで別の型
// なので、typeof の結果もことなる
Console.WriteLine(typeof(int) == typeof(int?)); // false
 
// null 許容参照型は C# がフロー解析するだけ
// T? と書いても、内部的には T のまま
// なので、typeof の結果は同じ
Console.WriteLine(typeof(string) == typeof(string?)); // true

この余波なんですが、例えば以下のような差が出ます。

static void M(string? x)
{
    // 参照型の場合、フロー解析で保証をしている
    // この if を抜けた時点で、x が null でないことが保証される
    if (x == null) return;
 
    // なので、? の付かない string に代入できるようになる
    string y = x;
}
 
static void M(int? x)
{
    // 値型の場合、型自体が違う
    // この if を抜けても x はあくまで Nullable<int> 型
    if (x == null) return;
 
    // なので、「int? を int に暗黙的に変換できません」となる
    int y = x;
}
// int と int? は別の型なので、オーバーロード可能
void M(int x) { }
void M(int? x) { }
 
// string と string? 型システム上は同じ型なので、オーバーロードできない
void M(string x) { }
void M(string? x) { }

ジェネリクス

ある程度はジェネリックな型に対しても使えます。 例えば、null 許容参照型を表す class? 制約というものも追加されます。

struct A<T> where T : class
{
    public T Value;
}
 
struct B<T> where T : class?
{
    public T Value;
}
 
class Program
{
    static void Main(string[] args)
    {
        var a1 = new A<string> { Value = null }; // null ダメ
        var a2 = new A<string?> { Value = null }; // string? を渡しちゃダメ
        var b1 = new B<string> { Value = null }; // null ダメ
        var b2 = new B<string?> { Value = null }; // これなら警告が出ない
    }
}

しかし、「null許容参照型でもnull許容値型でもいいので、とにかくnull許容 or 非 null」みたいな指定ができません。

struct A<T> where T : class // 非 null 参照型
{
    public T NonNull;
    public T? Nullable; // OK
}
 
struct B<T> where T : struct // 非 null 値型
{
    public T NonNull;
    public T? Nullable; // OK
}
 
struct C<T> // 「非 null」だけを指定する手段はない
{
    public T NonNull;
    public T? Nullable; // これが無理
}

漏れがあり得る

フロー解析には漏れがあり得るそうです。 元々 null だらけな言語をいきなり完全に null のない言語に変えるのは無理でして。

例えば、現状だと以下のようなものすら漏れます。

using System;
 
class Program
{
    static void Main()
    {
        // new string[] { null } みたいなのはちゃんと警告になるものの、
        // new string[1] は通っちゃう。
        // 「既定値」が使われるので、null が入る。
        M(new string[1]);
    }
 
    static void M(string[] x)
    {
        foreach (var item in x)
        {
            // 本来は null は絶対来ないはずなものの…
            Console.WriteLine(item.Length); // ぬるぽ
        }
    }
}

この例は、将来的には「治る」可能性が高いです。 (徐々にフロー解析を賢くしたい意志はあるし、 この配列の既定値問題は既知の問題。 もしかしたら、リリース版までにはちゃんと警告が出るかもしれません。)

しかし、根本的に拾えないもの、 例えば unsafe コードや native 相互運用が絡むとどうしても解析しきれなくなります。

ということで、 #nullableを有効にしたうえで警告をすべて取り切っても、 NullReferenceExceptionが出るときは出ます。

試してみた感じ

とまあ、完全ではない感じの C# 8.0 の null 解析ですが、 ここからは個人の感想。

実際、職場のコードも含めて、自分の持っているコードに対して #nullable を有効にして試してみました。

まあ、とりあえず感想としては、

  • 完全ではないとしても元よりは絶対マシ
  • オン/オフ混在させてもそんなに違和感はなさそう
    • ちょっとずつ対応させていけばいい感じはちゃんとある

という感じ。

警告が出た量

ちなみに、いきなりプロジェクト全体に対してNullableReferenceTypesをtrueにしてやった場合どうなるかですが、 大体50~100行に1個くらい警告が出ます。 23万行ほどあるリポジトリに対してやってみたところ、 警告が3800個出ました。

ほとんどは、元々nullを意図していたコードに対してちまちまと?を付けて回る簡単なお仕事です。 今までさぼっていた人だと、if (x != null)?. を付けて回るお仕事も待っていると思います。

普通にやって取れなかった警告

何十個に1個かくらい、真っ当な方法では警告が取れなくて、!演算子でご容赦を願ったところもあったりはします。 (いくつかはバグだと思うので、リリースまでには治ることを期待。)

前述のジェネリクスの問題は本当にどうしようもなかったです。 例えば以下のような感じのやつ。

class MyDictionary<T>
{
    // 制約なしの T は T? にできないので…
    public T GetValueOrDefault(int key)
    {
        //if (keyで検索) return 見つかったら値を返す;
        return default!; // ! を付けないと警告
    }
}

あとは、「null は素通し」形のメソッド。 以下のような感じのコードがあって、結局は ! に頼りました。

class Program
{
    // 引数が null の場合に限り、戻り値も null
    // (例として素通ししているものの、実際のコードは多少の変換コードあり。
    //  ただし、メソッドの先頭で if (s == null) return null;)
    static string? M(string? s) => s;
 
    static void Main()
    {
        // null を与えて null が返ってくるのは想定通り。
        string? s1 = M(null);
 
        // 自分は、この場合に M が null を返さないことを知っているものの…
        // コンパイラーにはわからないので警告が出る。
        string s2 = M("abc");
    }
}

C# 8.0 switch 式

$
0
0

今日は switch 式の話。 ステートメントではなく、式。 var y = x switch { ... } みたいに書ける構文です。

C# 8.0 候補の中でも割と早い段階に実装されていて、 「the patterns and ranges preview」とかいってPreview 公開もされていました。 (後述するように、switch式は「patterns」のおまけです。)

なのでてっきり、Visual Studio 2019 Preview でもまず真っ先に入ると思っていたんですが。 なぜか Preview 1 には入らなかったという…

(たぶん、パターン マッチングにまだもうちょっと調整したいことができたから?)

新しい switch

C# の switch ステートメント(C# 1.0 の頃からあるやつ)は、 C 言語系の影響を受けすぎている感じがあり、 使いにくいものでした。 C# 1.0 当時の情勢から言うと C 言語の switch を意識するのはしょうがなかったと思いますが、 それから15年以上経った今となってはちょっとしんどい文法です。

例えばこれまでだと、以下のような書き方を時々見ると思います。

public void M(年号 e)
{
    int y;
    switch (e)
    {
        case 明治:
            y = 45;
            break;
        case 大正:
            y = 15;
            break;
        case 昭和:
            y = 64;
            break;
        case 平成:
            y = 31;
            break;
        default: throw new InvalidOperationException();
    }
    // y を使って何か
}

しんどい理由は、

  • それぞれの条件で1つずつ値を返したいだけなのにステートメントを求められる
  • break が必須
  • case ラベルもうざい

という辺り。 以下のように別メソッドを1段挟めば多少緩和するようなそうでもないような…

public void M(年号 e)
{
    int lastYear()
    {
        switch (e)
        {
            case 明治: return 45;
            case 大正: return 15;
            case 昭和: return 64;
            case 平成: return 31;
            default: throw new InvalidOperationException();
        }
    }
 
    var y = lastYear();
    // y を使って何か
}

一方、C# 8.0 で、switch に式として使えるバージョンが追加されます。 この「switch 式」を使えば、以下のように書き直せます。

public void M(年号 e)
{
    var y = e switch
    {
        明治 => 45,
        大正 => 15,
        昭和 => 64,
        平成 => 31,
        _ => throw new InvalidOperationException()
    };
}

casebreakが消えてちょっとすっきり。

提案上はパターン マッチングの一部

今の C# の文法は、csharplang リポジトリ上で提案があって、採用することに決まったものは Milestone で管理されていたりします。

が、switch 式はぱっと見 C# 8.0 Milestone に並んでいない。 なぜかというと、パターン マッチングの一部として提案されたから。 パターン マッチングの方の提案 issueを覗いてみれば、その中に「switch expression」の文字もあります。 実装も、features/recursive-patternsブランチ内で行われていたり。

けど、検索性は最悪なんですよね。 ただでさえ「issue の9割は重複か実現不能な提案」とか陰口言われるレベルの csharplang なのに、検索性が悪いとかちょっと… 実際、もう実装された後になって、「switch 式が欲しい」という重複提案が何度かありました…

後置き記法

書き方が e switch {} というちょっと変わった記法。

ステートメントの方の switch との弁別の意味もあるみたいです。switch (e) から初めてしまうとステートメントと混同してしまう。 かといって、新たに別キーワードを導入(例えば match (e) ...)するのもいまいち評判がよくなく。 (ちなみに、当初案はこのmatchキーワードの導入でした。 なので、長らくこの機能は「match 式」と呼ばれていました。)

あと、基本的に前置きの記法は評判が悪いです。 否定の!とかキャストとか。 csharplangのissueでも、「後置きキャスト文法が欲しい」という提案もよく見ます。 わざわざ As なんとかみたいな名前の拡張メソッドを生やすこともよくあります。 !の方も、Not()とかNegate()みたいな拡張メソッドを書いたことある人、結構いるんじゃないでしょうか。

なんか、前置きって書きにくいんですよねぇ。どうしても、「先に式を書いたうえで、カーソルを前に戻して、改めて!とかを入力する」みたいな書き方をしてしまい、そのカーソルを戻す作業がストレス。

そんな感じのこと、みんな思っているようで、swtich式も後置きの e switch {} になりました。

=>

case の方も提案ではいろいろとバリエーションがありました。

case の区切り:

  • , 区切り
  • ; 区切り

case の書き方

  • x: experssion
  • x -> experssion
  • x ~> experssion
  • x => experssion

以下のように、when句に条件演算子?:を書いたり、 返す値にラムダ式を書いたりできるので、:とか=>とかも案外弁別に悩む選択肢です。

var i = x switch
{
    string s when (s.Length >= 1 ? s[0] <= 0x7F : true) => () => true,
    _ => (Func<bool>)(() => false)
};

(ちなみにこのコード、()がないとコンパイルが通りません。 ()の入れ子弁別している模様。)

かといって見慣れない(ポインター用な)->とか、 ほんとに新たに追加する必要がある~>とかもだいぶ気持ち悪いです。

結局、アンケート的なのを取った結果、最終的に x => expression, に決まりました。

C# 8.0 パターン マッチング

$
0
0

今日はパターン マッチングの話。 昨日のswitchに引き続き、 真っ先に実装されてそうなものなのに Preview 1 には入っていなかったやつ。 というか、switch式自体、このパターン マッチングの一部として提案されているものです。

パターン マッチング “完全版”

パターン マッチングは、元々は C# 7.0 で検討されていたものの、 結局、一部分だけが C# 7.0 に入り、複雑なものは C# 8.0 に回りました。

パターン C# のバージョン 概要
discard C# 7.0 何にでもマッチ・無視 _
var C# 7.0 何にでもマッチ・引数で受け取り var x
定数パターン C# 7.0 定数との比較 null1
型パターン C# 7.0 型の判定 int istring s
位置パターン C# 8.0 分解と同じ要領で、Deconstructを元に(引数の位置に応じて)再帰的にマッチングする (1, var i, _)
プロパティ パターン C# 8.0 プロパティに対して再帰的にマッチングする { A: 1, B: var i }

要するに、再帰的に使える下の2つが C# 8.0 での新機能になります。

まあ、C# 7.0 のやつだと「“パターン”って言うほど複雑なマッチングしてない」感がありました。 (実際、なので C# 7.0 リリース当時は「型スイッチ」みたいな呼び方もされていました。 結局、まあ、C# 8.0 を見越してあくまで「パターン マッチングのうち、型パターンだけは先にリリース」みたいな感じでアナウンスされています。)

例えば以下のような感じ。

public class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) => (X, Y) = (x, y);
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
 
public class C
{
    static int M(object obj)
        => obj switch
        {
            0 => 1,
            int i => 2,
            Point(1, _) => 4, // new!
            Point { X: 2, Y: var y } => y, // new!
            _ => 0
        };
}

何に使うかと言われると

まあ、再帰パターンなわけで、再帰的なデータ構造相手なら便利そうではあります。 (再起データ構造自体どのくらいの頻度で使われるかという話を置いておけば…)

例えば、x + 1 みたいな式を Add(Variable(x), Const(1)) みたいなツリー構造で表す奴とか。 そのツリーに対して、x + 0x と等しいとか、x * 1x と等しいとかその手の簡単化をするやつは、以下のように書けるようになります。

public static Node Simplify(this Node n)
    => n switch
        {
            Add(var l, var r) => (l.Simplify(), r.Simplify()) switch
            {
                (Const(0), var r1) => r1,
                (var l1, Const(0)) => l1,
                (var l1, var r1) => new Add(l1, r1)
            },
            Mul(var l, var r) => (l.Simplify(), r.Simplify()) switch
            {
                (Const(0) c, _) => c,
                (_, Const(0) c) => c,
                (Const(1), var r1) => r1,
                (var l1, Const(1)) => l1,
                (var l1, var r1) => new Mul(l1, r1)
            },
            _ => n
        };

(コード全体はGist上に)

ちなみに、単に「複数の値を同時にマッチング」という使い方もできます。 以下のように、(x, y) switch { } でスイッチ。

static int Compare(int? x, int? y)
    => (x, y) switch
    {
        (null, null) => 0,
        (null, _) => -1,
        (_, null) => 1,
        ({} ix, {} iy) => ix.CompareTo(iy)
    };

要するに、このコードは「タプルに対する位置パターン」なんですが、 それが「x, y に対して多値マッチング」っぽく使えます。

(あと、{}は後述しますが、「プロパティ パターン」(の、中身空っぽ)です。)

ちなみに、switch ステートメントでも以下のような書き方ができます。

static int Compare(int? x, int? y)
{
    switch (x, y)
    {
        case (null, null): return 0;
        case (null, _): return -1;
        case (_, null): return 1;
        case ({ } ix, { } iy): return ix.CompareTo(iy);
        }
    }
}

先ほど書いた通り、これは実際には「タプルに対する位置パターン」なんですが、 だったら、本来は switch ((x, y)) という書き方(内側の()がタプル構築、外側の()switchステートメントのもの)をする必要があります。 これも C# 8.0 の新機能で、「タプルだったら()を1個省略して、多値 switch っぽく書けるようにした」というものです。

非 null マッチング

ちなみに、プロパティ パターンの {} は、 プロパティを調べる前に本体が null ではないことをチェックします。 中身が空っぽのプロパティ パターンでも null チェックだけは挿入されるので、 x is {}で、「xはnullではない」の意味で使えます。

C# 7.0 までのパターンだと、null チェックを楽に書く手段がなかったです。

struct LongLongNamedStruct { }
 
void M1(LongLongNamedStruct? x)
{
    // こういう書き方だと null チェックになる。
    if (x is LongLongNamedStruct nonNull)
    {
        // obj が null じゃない時だけここが実行される。
        // でも、x の型が既知なのに、長いクラス名をわざわざ書くのはしんどい…
    }
}
 
void M2(LongLongNamedStruct? x)
{
    // が、var パターンは null にもマッチしちゃう。
    // (var は「何にでもマッチ」。null でも true になっちゃう。)
    if (x is var nullable)
    {
        // obj が null でもここが実行される。
    }
}

もちろん、単に null チェックだけなら !(x is null) とか x.HasValue でいいんですけども、 値を使いたければその後ろで var nonNull = x.GetValueOrDefault(); を書かないと行けないのがしんどく。

そこで、プロパティ パターンが使えます。 以下のように、「空のプロパティ パターン」を書けば、「非 null のときだけ」判定ができます。

void M3(LongLongNamedStruct? x)
{
    // (C# 8.0) プロパティ パターンであれば、null チェックを含む。
    if (x is {} nonNull)
    {
        // obj が null じゃない時だけここが実行される。
    }
}

ちょっと「知ってないと使えない仕様」ですけども… 覚えておくと便利です。

対称性

C# 7.0 の時、タプルとか分解の構文を決めるにあたって、C# チームは結構「対称性」を気にしていました。

まず、タプルは「引数と対になるもの」として考えられています。

// タプル型宣言と引数宣言は同じような見た目。
(int x, int y) tup0;
int method(int x, int y) => x + y;
 
// タプル構築はメソッド呼び出しみたいな書き方になる。
// 位置指定:
var tup1 = (1, 2);
var ret1 = method(1, 2);
 
// 名前指定:
var tup2 = (x: 1, y: 2);
var ret2 = method(x: 1, y: 2);
 
// タプル戻り値は、引数と同じような書き方に。
(int x, int y) swap(int x, int y) => (y, x);

また、分解は「コンストラクターと対になるもの」です。

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
 
    // 複数の値を組み合わせて1つの型にまとめるのが構築(construct)。
    public Point(int x = 0, int y = 0) => (X, Y) = (x, y);
 
    // 1つにまとまっている値をバラバラに戻すのが分解(deconstruct)。
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
var p = new Point(1, 2); // contruct
var (x, y) = p;          // deconstruct

C# 8.0 の再帰パターンもこの話の延長にあります。

// 位置指定で構築できるんなら、位置指定でマッチングできるべき
var p1 = new Point(1, 2);
var r1 = p1 is (1, 2);
 
// 名前指定で構築できるんなら、名前指定でマッチングできるべき
var p2 = new Point(x: 1, y: 2);
var r2 = p2 is (x: 1, y: 2);
 
// 初期化子でプロパティ指定できるんなら、プロパティ指定でマッチングできるべき
var p3 = new Point { X = 1, Y = 2 };
var r3 = p3 is { X: 1, Y: 2 };
 
// 混在構築できるんなら、混在マッチングできるべき
var p4 = new Point(x: 1) { Y = 2 };
var r4 = p4 is (1, _) { Y: 2 };

最近の変更

冒頭で言ったように、再帰パターンも元々は C# 7.0 で考えられていました。 それに、C# 8.0 機能の中では相当早い段階から実装済みで、 確か今年の初めくらいにはすでに実装がありました。

(なので、sharplab.ioで割かし安定して試せたりします。 Visual Studio 2019 Preview 1 で実装されていなくても割と細かくブログを書けるのはこれのおかげ。)

個人的には「C# 7.4 があってもよかったんじゃ… 再帰パターンだけのリリース」とかもちょっと思ったり。 C# チーム的には「マイナー リリースで出すほど小さい機能ではない」とのことで、C# 8.0 での追加になります。

ということで、大半の機能はだいぶ前から試せる状態にあったんですが、 割と最近にもいくつか細かい追加・変更がありました。

  • switch (x, y) の「() を1段省略」は割と最近の採用
  • プロパティ パターンの構文は { X is pattern }{ X = pattern }{ X: pattern } のどれがいいか
    • : になったのは割と最近
  • var (x) みたいな、「1引数 Deconstruct
    • キャストや、「(1)は単なる1と同じ意味」という既存の構文との弁別の問題があるものの、varの後ろなら弁別できるので認めようということに最近なった
  • 同じく、「0引数Desonctruct」に対するvar ()パターンも

C# 8.0 Async streams

$
0
0

一応、Preview 1で実装されてはいるんですが、ちょっと不具合があって動かない機能が1つあったりします。

非同期ストリーム(async streams)と呼ばれていて、具体的には以下の2つの機能からなります。

  • 非同期イテレーター … 戻り値をIAsyncEnumerable<T>インターフェイスにすることで、awaityieldを混在させることができる
  • 非同期 foreachawait foreachという書き方で、IAsyncEnumerable<T>から値を列挙できる

要は、一連のデータ(data stream)を、非同期に生成(イテレーター)して非同期に消費(foreach)する機能です。

非同期 foreach

消費側の方が簡単なので先に非同期 foreach の方を。 IEnumerable<T>の非同期版であるIAsyncEnumerable<T>に対して要素の列挙ができる機能です。 (実際には同名のメソッドを持っていればインターフェイスの実装は不問なところも、同期版foreachと一緒。)

文法の候補は async foreachforeach asyncforeach awaitなど他にもあったんですが、 現状は以下のようなawait foreachが採用されました。

// 非同期 foreach … IAsyncEnumerable からの列挙
static async Task AsyncForeach(IAsyncEnumerable<int> items)
{
    await foreach (var item in items)
    {
        Console.WriteLine(item);
    }
}

これまでのawait同様、これが書けるのは非同期メソッド(async修飾付きのメソッド)内だけです。

こいつは、同期版のforeachと似たような感じで、以下のように展開されます。 同期版と比べて、MoveNextDisposeが非同期になっただけです。

private static async Task AsyncForeach(IAsyncEnumerable<int> items)
{
    IAsyncEnumerator<int> e = items.GetAsyncEnumerator();
    try
    {
        while (await e.MoveNextAsync())
        {
            int item = e.Current;
            Console.WriteLine(item);
        }
    }
    finally
    {
        if (e != null)
        {
            await e.DisposeAsync();
        }
    }
}

非同期イテレーター

続いて生成側の非同期イテレーター。 要は、awaityieldを混在できる機能です。

非同期メソッドと同様に async修飾が必須で、 戻り値はIAsyncEnumerable<T>である必要があります。

// 非同期イテレーター … await/yield混在
static async IAsyncEnumerable<int> AsyncIterator()
{
    await Task.Delay(1);
    yield return 1;
    await Task.Delay(1);
    yield return 2;
}

非同期イテレーターから生成されるコードは、 やっぱり同期版のイテレーター非同期メソッドを組み合わせたようなコードになります。 イテレーターも非同期メソッド元々結構複雑なので、非同期イテレーターはもっと複雑です。

後述するバグのせいで今のところコンパイルが通らないので、詳細はバグが治ったら(Preview 2?)改めて書こうかと思います。

IAsyncEnumerable

非同期foreachでも非同期イテレーターでも、IAsyncEnumerable<T>インターフェイス(System.Collections.Generc名前空間)が出てきます。 これも、割と素直に「IEnumerable<T>の非同期版」という感じのインターフェイスになりました。

以下のようなインターフェイスになる予定です。 (割かし最近変更があって、Preview 1 の時点では CancellationToken を受け取る引数がまだないです。)

using System.Threading;
using System.Threading.Tasks;
 
namespace System.Collections.Generic
{
    public interface IAsyncEnumerable<out T>
    {
        IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
    }
    public interface IAsyncEnumerator<out T> : IAsyncDisposable
    {
        T Current { get; }
        ValueTask<bool> MoveNextAsync();
    }
}

前にちょっと書きましたが、 以下のような構造もちょっと検討されました。

public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
    ValueTask<bool> WaitForNextAsync();
    T TryGetNext(out bool success);
}

こちらの没案の方が、うまく使えばパフォーマンスがよくなります。 ただ、ちょっと使いにくい構造なので、ちょっと複雑なことをしようと思うと、パフォーマンスの良いコードを書くのが結構大変になったりします。 なので、「シンプルさにこだわりたい」とのことで、結局、現在の素直な構造になったみたいです。

Preview 1 でのバグ

非同期 foreach の方はPreview 1でも問題なく動きます。 一方で、非同期イテレーターの方は、文法上はエラーなく解釈できるんですが、 実行ファイルを生成する段階で「ManualResetValueTaskSourceLogic構造体が存在しない」というエラーを起こします。

どうも、Preview 1としてリリースするブランチが、Roslyn側とcoreclr側で食い違っているみたいです。 非同期イテレーターが内部的に使う型があって、 その型の仕様は最近ちょっと変更されています。 元々はManualResetValueTaskSourceLogicという名前で実装されていたんですが、 名前もManualResetValueTaskSourceCoreに変更されました。 そして、Roslynの方は変更前のままで、corefxの方は変更後のブランチでPreview 1をリリースしてしまったみたいです。

ソースコードを取ってきて名前だけ"Logic"に戻して動くなら良かったんですが、 ちょっと実装も変わっていて、無理やり動かすのもそこそこ面倒そうでした。 まあ、Preview 2では治っていると思うので、治ったら本気出します。

C# 8.0 その他 (Preview 1での未実装機能)

$
0
0

これまで紹介してきたもの以外にも、C# 8.0での導入が予定されている機能はいくつかあります。 ただ、Visual Studio 2019 Preview 1でまだ実装されていない機能・ちゃんと動いていない機能はまとめて軽く紹介して終わりにしようかと思います。 次以降のPreviewで実装されたらまた改めて紹介します。

インターフェイスのデフォルト実装

インターフェイス中のメソッドに実装を持てるようになります。 これに関しては昔書いた記事があるのでそちらを参照:

先日「RuntimeFeature クラス」で紹介した通り、 ランタイムの修正が必須の機能です。

pattern-base な using/foreach

前に1度書いていますが、C# には単なるメソッド呼び出しに置き換えるような、シンタックスシュガーな文法が結構あります。 例えば、クエリ式の場合、以下の2行は全く同じ意味になります。

var q1 =
    from x in source
    where x > 5
    select x * x;
 
var q2 = source
    .Where(x => x > 5)
    .Select(x => x * x);

問題はここから先。 クエリ式の場合は、このWhereSelectメソッドにかなり自由が効きます。

  • 特にインターフェイスの実装等は必要なく、所定のパターンを満たしていれば何でもいい
  • インスタンス メソッドでも拡張メソッドでもいい
  • オプション引数や params があってもいい

一方で、foreachの場合だと以下の制限が掛かります。

  • インスタンス メソッドでないとダメ
  • オプション引数や params があるとダメ

さらに、usingステートメントに至ってはもっと厳しい制限が掛かっています。

  • IDisposableインターフェイスを実装していないとダメ

これに対して、C# 8.0 では、foreachusingでもクエリ式と同程度の緩さで「パターンでの(pattern-based)実装」が認められるようになります。 昨日紹介した非同期版の foreach も同様です。

ちなみに、提案では「enhanced using」と呼ばれていて、 次節の「using declarationとセット」、かつ、「usingの方が主役でforeachの方はおまけ」です。

using declaration

usingステートメントに対して、以下のような要望は多いです。

  • usingのネストがしんどい、
  • Disposeしたいタイミングはほとんどの場合、変数のスコープと同じ

ということで、以下のように、変数に対する修飾子としてusingを書くことで、 その変数のスコープから抜けるときにDisposeを呼ぶという機能を追加する予定です。

struct A
{
    void Dispose() => Console.WriteLine("A Disposed");
}
 
class Program
{
    static void Main()
    {
        using var a = new A();
        using var b = new A();
 
        {
            using var c = new A();
            // c のスコープはここまでなので、ここで c.Dispose()
        }
 
        // ここで b.Dispose(); a.Dispose();
        // ちなみに、宣言とは逆順で呼ばれる
    }
}

Target-typed new

C# 7.1で入ったdefaultと同様に、newに対しても左辺からの型推論が効くようになります。

// これは 右→左 の推論。C# 3.0 の頃から使える。
var a1 = new A(1, 2);
 
// C# 8.0 では、左→右 の推論が入る。
A a2 = new(1, 2);

caller expression attribute

C# 5.0で、Caller Info 属性というものがいくつか入っています。 以下のように、コンパイラーによって呼び出し元のメソッド名などを挿入してもらう機能です。

using System;
using System.Runtime.CompilerServices;
 
class Program
{
    static void M([CallerMemberName]string callerName = null)
        => Console.WriteLine(callerName);
 
    static void Main()
    {
        // M には何も引数を渡していないものの、
        // CallerMemberName が付いているので null ではなく、呼び出し元のメソッド名
        // (この場合は "Main")がコンパイラーによって挿入される。
        M();
    }
}

C# 8.0で、この手の属性が1つ増えます。 CallerArgumentExpression属性を付けることで、 引数に渡した式全体を受け取れます。

using System;
using System.Runtime.CompilerServices;
 
class Program
{
    static void M(int x, [CallerArgumentExpression("x")]string xExpression = null)
        => Console.WriteLine(xExpression);
 
    static void Main()
    {
        M(1 + 2 + 3); // "1 + 2 + 3" が xExpression に渡る
        M(2 * 3);     // 同上、"2 * 3"
    }
}

わかりやすい用途は、例えばXUnit.Assertとかです。 単体テストが失敗したときに、失敗の原因になった式をログに表示できます。

generic attributes

属性にジェネリックなクラスを使えるようになります。

using System;
 
class MyAttribute<T> : Attribute { }
 
[My<int>]
class Target { }

機能一覧

ここで紹介したのは、roslyn リポジトリにあるLanguage Feature Statusを元に選んだものです。 一方で、csharplang の方の 8.0 candidate マイルストーンの方には他にもいくつか並んでいます。

7.0 の時の経験からいうと、 基本的にはLanguage Feature Statusに並んでいるものが実装されていきますが、 多少の入れ替わりはあったりします。 急にLanguage Feature Statusに追加されるものもあれば、 今並んでいても8.xに回されることもあります。

例えば、実装状況を見るに、以下の2つなんかはLanguage Feature Statusに並んでいませんが、8.0 に入るんじゃないかという感じがします。

配列のインデクサー

$
0
0

C# 8.0 がらみの話も一段落してしまったので、 今日からしばらく予告通り、Gist に書き捨ててたもののブログ化になります。 ぶっちゃけ、在庫一掃処分セールみたいなものなので過度な期待はしないでください。

今日は C# コンパイラーと JIT レベルの最適化の話。

配列の範囲チェック

.NET の配列は、バッファオーバーランとかのメモリ破壊を避けるべく、範囲チェックがかかっています。

using System;
 
class Program
{
    static void Main()
    {
        var a = new int[4];
        var x = a[5]; // 範囲外なのでここで IndexOutOfRangeException が飛ぶ
        Console.WriteLine(x);
    }
}

a[5] のところのコンパイル結果は以下のようになります。

IL:

IL_0006: ldc.i4.5   // 5 をロード
IL_0007: ldelem.i4  // 配列要素の読み込み命令

IL には配列の要素を読み書きする命令があります。 この時点では ldelem 命令が出力されているだけで、 例外を飛ばすコードはありません。 範囲チェックが挿入されるのはその次の、JIT でネイティブ コード化される段階になります。

x86 コード:

L000f: cmp dword [eax+0x4], 0x5 ; 配列長と 5 を比較
L0013: jbe L001e                ; 例外を投げるコードにジャンプ
L0015: mov ecx, [eax+0x1c]      ; a[5] の場所のデータを読み込み

元のコードにはない比較・ジャンプ命令が挟まっています。

配列の列挙

今度は、全要素列挙することを見てみましょう。 以下のようなコードを考えます。

void M(int[] array)
{
    for (var i = 0; i < array.Length; ++i)
    {
        var x = array[i];
        Console.WriteLine(x);
    }
}

この場合は、ループ付近が以下のようにコンパイルされます。

IL:

IL_0004: ldarg.1
IL_0005: ldloc.0
IL_0006: ldelem.i4                  // array[i]
IL_0007: call void WriteLine(int32)
IL_000c: ldloc.0
IL_000d: ldc.i4.1
IL_000e: add
IL_000f: stloc.0
IL_0010: ldloc.0
IL_0011: ldarg.1
IL_0012: ldlen
IL_0013: conv.i4                    // i < Length
IL_0014: blt.s IL_0004

x86 コード:

L0008: xor esi, esi
L000a: mov ebx, [edi+0x4]
L000d: test ebx, ebx
L000f: jle L001f
L0011: mov ecx, [edi+esi*4+0x8]  ; array[i]
L0015: call WriteLine(Int32)
L001a: inc esi
L001b: cmp ebx, esi              ; i < Length
L001d: jg L0011

for ステートメント中の i < Length 相当のコードはありますが、 その他の比較はありません。 要するに、単品だとarray[i]のところに挟まっていた範囲チェックが、このループでは消えています。

これは JIT が行っている最適化で、要するに、

  • 何もしなければ、array[i] のところに暗黙的な範囲チェックを追加する
  • 明示的な範囲チェックがあれば、余計な範囲チェックの追加はしない

という挙動になります。 なので、安全性は保たれつつ、ループの速度は落としません。

foreach 最適化

次に以下のコードを考えます。 先ほどの for を使ったコードとやっていることは全く一緒です。 配列の全要素の列挙。

void M(int[] array)
{
    foreach (var x in array)
    {
        Console.WriteLine(x);
    }
}

こいつは以下のようにコンパイルされます。

IL:

IL_0006: ldloc.0
IL_0007: ldloc.1
IL_0008: ldelem.i4                  // array[i]
IL_0009: call void WriteLine(int32)
IL_000e: ldloc.1
IL_000f: ldc.i4.1
IL_0010: add
IL_0011: stloc.1
IL_0012: ldloc.1
IL_0013: ldloc.0
IL_0014: ldlen
IL_0015: conv.i4                    // i < Length
IL_0016: blt.s IL_0006

ldloc (ローカル変数読み込み)の後ろの番号とかが違うだけで、 他は先ほどの for のコードと全く同じです。 (要するに、「変数名が違うけど同じロジック」程度の差です。)

GetEnumeratorとかMoveNextCurrentは一切出てきません。 代わりにインデックスの ++i とか array[i] 相当のコードが出てきます。

要するに、C# コンパイラーは、配列の foreach を見たら for (var i ... 相当のコードに変換します。

配列の一部分を列挙

配列全体の列挙に対して結構よい最適化がかかっていることはわかりました。 次は、一部分だけ列挙することを考えます。

void M(int[] array, int start, int count)
{
    var end = start + count;
    for (var i = start; i < end; ++i)
    {
        var x = array[i];
        Console.WriteLine(x);
    }
}

これを同じようにコンパイルすると… ループ近辺だけ抜き出すと以下のようになります。

x86 コード:

L0017: mov eax, [edi+0x4]
L001a: mov [ebp-0x10], eax
L001d: mov eax, [ebp-0x10]
L0020: cmp esi, eax        ; ここの比較は array[i] に対して暗黙的に追加されるもの
L0022: jae L003a           ; 例外を投げるコードへのジャンプ
L0024: mov ecx, [edi+esi*4+0x8]
L0028: call System.Console.WriteLine(Int32)
L002d: inc esi
L002e: cmp esi, ebx        ; ここの比較は i < end
L0030: jl L001d            ; これはループを抜ける・抜けないの分岐

さすがに、array.Length 以外のものまで見て最適化は掛けてくれないみたいです。 ちなみに、事前に startendの範囲チェックをしてもダメ。

そこで Span<T>

C# 7.2 では、Span<T>がらみの対応がいろいろ入ったわけですが、このタイミングで、Span<T>に対する列挙の最適化も入っています。

まず、配列同様、Span<T>に対するforeachforに最適化されます。 例えば、以下の2つのメソッドはほぼ同じ IL にコンパイルされます。

public void M1(Span<int> array)
{
    for (var i = 0; i < array.Length; ++i)
    {
        var x = array[i];
        Console.WriteLine(x);
    }
}
 
public void M2(Span<int> array)
{
    foreach (var x in array)
    {
        Console.WriteLine(x);
    }
}

また、JIT 時の最適化で、「暗黙の範囲チェック」も消えてくれるようです。 要するに、先ほどの for (var i = start; i < end; ++i) なループよりも、 以下のような、Span<T>を介したコードの方が最適化がかかりやすいです。

public void M2(int[] array, int start, int count)
{
    foreach (var x in array.AsSpan(start, count))
    {
        Console.WriteLine(x);
    }
}

ちなみに、今のところこういう最適化がかかるのは配列と Span<T> だけです。 Span<T> だけ特別扱いするのもちょっと嫌な話で、 もっと汎用的に、所定のパターンを満たした型なら foreachfor (var i ... に変換できるような仕組みも一応検討はされています。 (Span<T>以外に対する需要はそこまで高くないので、優先度は低め。)

System.Runtime.CompilerServices.Unsafe

$
0
0

昨日から始まった在庫一掃処分セール的なブログなんですが、結構な頻度で「Unsafe クラス」ってのが出てきます。

以下のパッケージに含まれているもので、こいつをを参照すれば、通常の C# では書けないようなどぎつい unsafe な真似がし放題になります。

これの登場はもう結構前なんですけども、そういえばちゃんとした説明をしたことなかったなと。

.NET の IL は意外とやりたい放題

上記パッケージにはパッケージ名と同じUnsafeというクラスが入っています。 このUnsafeクラス、ソースコードはこんな感じ:

ILアセンブリ実装です。

C# では書けなくても、IL なら何も特別なことをしなくてもやりたい放題。 要するに、.NET における「安全」は、結構 C# のレベルで保証しています。

とはいえ、unsafe でもいいので、C# でできないのは困るということで提供されるようになったのがこのUnsafeクラスです。 C# の文法を拡張するよりは、こういう IL 実装なクラスを提供する方が手っ取り早かったのでこんなことになりました。

ポインターの方がまだマシ疑惑

とはいえ、このUnsafeクラスをフル活用すると、こんなコードになります。

using System.Runtime.CompilerServices;
 
class Program
{
    static int UnsafeClass(int[] array)
    {
        var sum = 0;
        ref var begin = ref array[0];
        ref var p = ref Unsafe.As<int, byte>(ref begin);
        var length = array.Length * 4;
        for (int i = 0; i < length; i++, p = ref Unsafe.Add(ref p, 1))
            sum += p;
        return sum;
    }
}

ちなみに、普通に C# で unsafe コードを使って同じものを書くと以下のようになります。

unsafe static int UnsafeContext(int[] array)
{
    var sum = 0;
    fixed (int* begin = &array[0])
    {
        var p = (byte*)begin;
        var length = array.Length * 4;
        for (int i = 0; i < length; i++, p++)
            sum += *p;
    }
    return sum;
}

見た目に関しては、ポインターを使った後者の方がまだマシなんじゃないでしょうか。

だったら素直に unsafe コードを使う方がいいんじゃないかという話になるとは思いますが、 いくつか、Unsafeクラスでしかできないことがあります。

  • ポインターの代わりに ref で操作できる
  • ジェネリックな型をポインター化できる

ポンターの代わりに ref

ポインターと ref は内部的には似たようなものです。 大体同じ命令を使って間接参照します。 ですが、 1つ決定的に違うのが、refならガベコレが追えるという点があります。

// ref 戻り値ならこんなコードを書いても平気。
// 戻り値が「参照」されている限り、配列自体の参照がガベコレにトラッキングされる。
ref int X()
{
    var array = new int[1];
    return ref array[0];
}
 
// 一方、これはダメ。
// ガベコレが走ったら、もはやポインターが有効な場所を指さなくなる。
unsafe int* Y()
{
    var array = new int[1];
    fixed (int* p = array)
        return p;
}

ということで、refを使って unsafe なことをしたいときに使うのが Unsafe クラスです。

例としてはSpan<T>構造体があります。 (というか、Unsafeクラスを導入するに至った最初の動機はSpan<T>構造体を作るためでした。)

Span<T>は、以下のように、配列でもポインターでも統一的に扱える型です。

using System;
using System.Runtime.InteropServices;
 
class Program
{
    static void Main()
    {
        // 配列
        Span<int> array = new int[8].AsSpan().Slice(2, 3);
 
        // 文字列
        ReadOnlySpan<char> str = "abcdefgh".AsSpan().Slice(2, 3);
 
        // スタック領域
        Span<int> stack = stackalloc int[8];
 
        unsafe
        {
            // ガベコレ管理外メモリ
            var p = Marshal.AllocHGlobal(sizeof(int) * 8);
            Span<int> unmanaged = new Span<int>((int*)p, 8);
 
            // 他の言語との相互運用
            var q = malloc((IntPtr)(sizeof(int) * 8));
            Span<int> interop = new Span<int>((int*)q, 8);
 
            Marshal.FreeHGlobal(p);
            free(q);
        }
    }
 
    [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
    static extern IntPtr malloc(IntPtr size);
 
    [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
    static extern void free(IntPtr ptr);
}

こういう型を作ろうと思うと、通常なら unsafe コードだらけ・ポインターだらけになるんですが、 Span<T>構造体はその代わりに Unsafe クラスだらけ・refだらけです。

ジェネリックな型をポインター化

C# の unsafe コードの仕様では、ジェネリックな型はポインター化できません。 とはいえ、この制限は実はちょっと厳しすぎです。

// 値型しか含まない構造体はポインター化 (A*) できる。
struct A
{
    public int X;
}
 
// 1つでも参照型を含んでいる場合、ポインター化されるとガベコレが追えなくなって困る。
// なので、ポインター化できない仕様もやむなし。
struct B
{
    public string X;
}
 
// ならこのジェネリックな場合はどうか。
// T に値型を渡したとき、値型しか含まない構造体になり得る。
// T 次第でポインター化できるかどうか変えてもよかったのではないか。
// (現状は無条件にポインター化 (C<int>* とかも) 不可)
struct C<T>
{
    T X;
}

C# 7.3 で unmanaged 制約が入って、 多少は制限が緩和したんですが、いまだこの例の C<T> のような型はポインター化できません。 (C# 8.0 で緩和される可能性あり。遅くとも C# 8.x の間には緩和されると思われます。)

が、Unsafeクラスを使えば(今でも)そんな制限をガン無視できます。

using System;
using System.Runtime.CompilerServices;
 
struct C<T>
{
    public T X;
}
 
public class Program
{
    unsafe static void Main()
    {
        var c = new C<int>();
        int* p = (int*)Unsafe.AsPointer(ref c);
        *p = 1;
        Console.WriteLine(c.X); // 1
    }
}

Unsafe クラスを safe なところから呼べる

もちろん、Unsafe クラス悪用すると、unsafe コード以上に unsafe になります。

にもかかわらず、Unsafeクラスのメソッドの引数・戻り値は大半が ref になっているので、unsafe コードなしで呼び出せます。 ある意味、これが一番の欠陥で、言語機能の不足を感じます (「ポインターは使っていないけども unsafe コードからしか呼べない」みたいな制約を付けれる機能が欲しい)。 (実際、corefx/coreclr 内でも度々そういう話題は上がっています。 そもそも利用頻度が低いクラスなので需要はあんまりありませんが…)


配列のダウンキャスト

$
0
0

今日は Unsafe クラスを使った配列の最適化の話。

object[]

.NET Framework 1.x 時代からある古い API の中にはいくつか、 本当は T 型の配列なのに object[] で戻り値を返してくるようなメソッドがいくつかあります。 object[]とまでは言わないものの、基底クラスの配列で返すメソッドは多いです。

1.x 時代にはジェネリクスがなかったせいなんですが、今となっては不便ではあります。

例: マルチキャスト デリゲート

例を1個。 C# のデリゲートは、複数のメソッドを += で繋いで、一斉に呼び出すという機能があり、 これをマルチキャスト デリゲートと言います。 例えば以下のコードは、

Action f = null;
 
foreach (var i in new[] { 1, 2, 3, 4, 5 })
{
    f += () => Console.WriteLine($"lambda {i} invoked");
}
 
f();

以下のような結果を出力します。

lambda 1 invoked
lambda 2 invoked
lambda 3 invoked
lambda 4 invoked
lambda 5 invoked

基本的にはイベントのための機能で、 戻り値は想定していません。void戻り値以外のメソッドに使おうとするとトラブります。 以下のようなコードを書いたとすると、

Func<int> f = null;
 
foreach (var i in new[] { 1, 2, 3, 4, 5 })
{
    f += () =>
    {
        Console.WriteLine($"lambda {i} invoked");
        return i;
    };
}
 
Console.WriteLine($"f returns {f()}");

最後の行の出力は

f returns 5

になります。要するに、最後の1個の戻り値以外は消えてなくなります。 全ての戻り値を取りたければ以下のように、 個々のデリゲートを配列で受け取って、1つ1つ呼び出すようなコードを書きます。

Delegate[] list = f.GetInvocationList();
foreach (Func<int> item in list)
    Console.WriteLine($"f returns {item()}");

fFunc<int> なんだから、Func<int>[] で一覧を取りたいところなんですが、 残念ながら GetInvocationList の結果は Delegate[] で帰ってきます。 それを再び Func<int> にダウンキャストして使うことになります。

特に、Task戻り値のデリゲートを await したいときとかに必須の手段です。

配列ダウンキャスト

そしてここからが本題。

基底クラスの配列から、元の派生クラスの要素を列挙したい場合、どうするのが最速でしょうか。 string[]object[] でベンチマークを取ってみます。

ベンチマーク全体は Gist に置いておきます。 要点を抜き出すと…

比較用データ: 同じ string 配列を、string[] のフィールドと object[] にフィールドに格納して使います。

string[] _stringData = new string[] { "a", "ab", "abc", "abcd", "abcde", "abcdef", "abcdefg" };
object[] _objectData = new string[] { "a", "ab", "abc", "abcd", "abcde", "abcdef", "abcdefg" };

これを、以下の3パターン(+ 参考までに1パターン)のコードに与えてみます。

(1) MemberwiseCast: 要素ごとにダウンキャスト

foreach (string s in _objectData)
    sum += s.Length;

(2) ArrayCast: 最初に配列自体をダウンキャスト

var data = (string[])_objectData;
foreach (var s in data)
    sum += s.Length;

(3) UnsafeStructCast: 謎の最適化

public struct Wrap<T> { public T Value; }
var data = Unsafe.As<object[], Wrap<string>[]>(ref _objectData);
foreach (var s in data)
    sum += s.Value.Length;

(参考) Static: 最初から string[] の方を列挙

foreach (var s in _stringData)
    sum += s.Length;

比較の結果、以下のような感じになります。

Method Mean Error StdDev Scaled ScaledSD
MemberwiseCast 8.552 ns 0.1170 ns 0.1094 ns 2.56 0.04
ArrayCast 7.074 ns 0.0952 ns 0.0844 ns 2.11 0.03
UnsafeStructCast 3.589 ns 0.0200 ns 0.0167 ns 1.07 0.01
Static 3.346 ns 0.0249 ns 0.0233 ns 1.00 0.00

お分かりいただけるだろうか。 1要素ごとにキャストのオーバーヘッドが掛かっていそうな (1) が遅いのは当然として。 最初に1回だけオーバーヘッドが掛かってあとは大丈夫そうに見える (2) がだいぶ遅いという。 そして、「謎の最適化」を自称しているだけあって (3) が倍くらい速い。

謎の最適化 Wrap<T>

ということで、この謎の最適化が速くなる原理について。

.NET の配列には共変性があります。

string[] derivedItems = { "Aleph", "Beth", "Gimel" };
object[] baseItems = derivedItems; // この代入は明示的なキャストなしでできる

だいたいこいつが犯人。

この状況で、baseItems[i] = 10; とか書いてしまうとまずいことになります。 なので、baseItems[i] に対していちいち型チェックが挿入されていて、 本来の型と違う型の値を代入しようとすると例外が飛びます。 その型チェックのコストが、前節の (2) が遅くなる原因。

ちなみに、共変性は参照型にしか働かないので、例えば以下のようなコードはコンパイル エラーになります。int は値型なので、共変ではなくなります。

int[] derivedItems = { 1, 2, 3 };
object[] baseItems = derivedItems; // この代入は(キャストの有無によらず)認められない

謎の最適化 (3) が速くなる理由はここにあります。 Wrap<T>構造体を介することで、共変性がなくなっています。 そして、共変じゃないことがわかっているので、型チェックが挟まらなくなる。 結果的に速い。 ただし、本来変換できないはずの object[] から Wrap<string>[] への嘘ダウンキャストが必要になるので、Unsafe クラスが必須です。

(ちなみに、Wrap<T>構造体はT型のフィールド1つだけの構造体なので、 objectWrap<object>のメモリ上での構造は全く同じになります。 なので、今回のような嘘ダウンキャストしても「メモリ上の配置が同じだから同じコードで動く」みたいな感じになります。)

割かしひどい話です。 Unsafe クラスを避けれるなら避けたいわけでして。 最初から「共変性は使わないから型チェックしないでくれ」と指示できるような、 配列に代わる手段が求められます。 ということで出ている提案が以下のようなもの。

まあ、採用される気配ないんですけども… 普通に coreclr 内にさっきの「謎の最適化」と同種のコード入ってたりするんですけども。

静的なデータの ReadOnlySpan 最適化

$
0
0

今日は C# コンパイラーのレベルの最適化(割と最近の追加)。

静的な byte データ列をプログラム中で使いたいとき、どう書くのが効率良いかといいかという話になります。

静的な byte データ

例えば、以下のようなコードを考えます。

using System;
 
public class Program
{
    static void Main()
    {
        var data = new byte[] { 65, 66, 67, 68, 69, 70, 71, 72 };
 
        foreach (var x in data)
            Console.WriteLine(x);
    }
}

data の中身は全て定数です。 なので、コンパイルすると、定数として DLL 中に埋め込まれます。 コンパイル結果の DLL を適当にエディターででも開いてみると、以下のような文字列が見つかると思います。 (65~72 は 'A''H' の文字コードです。)

静的なデータは DLL 中に埋め込まれる

無駄な配列生成

そして、配列自体も書き換えているわけではなく、その定数を読みだしているだけです。 にもかかわらず、このコードは配列のインスタンスが作られます(ヒープを使っちゃう)。 data を初期化する行は、概ね以下のような命令列になります。

IL_0000: ldc.i4.8
IL_0001: newarr [mscorlib]System.Byte
IL_0007: ldtoken field int64 フィールド名割愛
IL_000c: call void 中略::InitializeArray(略)

上から順に、

  • 配列長の8をロード
  • 配列を作成
  • 読み込みたいデータ(65~72) が書かれた場所のアドレスをロード
  • 配列の中身をその65~72で上書き

という感じ。

書き換えもしないのに配列を new した上にコピーが走るのはもったいないです。

ReadOnlySpan 最適化

これに対して、割と最近(Visual Studio 15.7、C# 7.3 世代で)実装された最適化があります。 先ほどのコードを、以下のように書き換えてみましょう。

using System;
 
public class Program
{
    static void Main()
    {
        ReadOnlySpan data = new byte[] { 65, 66, 67, 68, 69, 70, 71, 72 };
 
        foreach (var x in data)
            Console.WriteLine(x);
    }
}

dataの型をReadOnlySpan<byte>に変えただけです。 しかしこれで、dataの中身を書き換えないという保証ができたので、 最適化が掛かります。 C# 7.3 以降でコンパイルすると、結果は以下のようになります。

IL_0000: ldsflda int64 フィールド名割愛
IL_0005: ldc.i4.8
IL_0006: newobj instance void valuetype 中略.ReadOnlySpan`1::.ctor(void*, int32)

上から順に、

  • 読み込みたいデータ(65~72) が書かれた場所のアドレスをロード
  • 配列長の8をロード
  • ReadOnlySpan<byte> のコンストラクター呼び出し

です。ReadOnlySpan<byte>は構造体なので、ヒープは使いません。 直接、DLL 中に埋め込まれた ABCDEFGH のところを参照します。

ちなみに、この最適化が効くのはReadOnlySpan<byte>ReadOnlySpan<char>だけみたいです。 bytechar以外の値はダメですし、書き換え可能なSpan<T>を使ってもダメです。

実用例

つい最近なんですが、coreclr で、この最適化を適用する Pull Request が出ていたりします。

preamble ってのはいわゆる BOM (Byte Order Mark)です。 Unicode テキストが Big Endian か Little Endian かを判定するための文字ですが、 それを流用して文字コードの判定自体に使われてしまうあれ。 テキストの先頭に以下のような byte 列(code point U+FEFF をエンコードしたもの)を入れるやつ。

  • UTF-8 → EF, BB, BF
  • Big Endian UTF-16 → FE, FF
  • Little Endian UTF-16 → FF, FE
  • Big Endian UTF-32 → 00, 00, FE, FF
  • Little Endian UTF-32 → FF, FE, 00, 00

この静的な byte 列に対して、上記の ReadOnlySpan<byte> 最適化を適用しています。

Devirtualization (脱仮想化)

$
0
0

今日は一般論として「仮想メソッドは避けれるなら避けたい」という話と、 .NET Core 2.1 で仮想メソッドの「devirtualization」(脱仮想化)のための最適化が入っているという話。

仮想メソッドのコスト

多くのプログラミング言語で、 いわゆる多態的な動作の実現のために、 仮想呼び出し(virtual call)という機能が提供されています。 仮想呼び出しは、仮想関数テーブルを使った実装が一般的です。

テーブルを使った実装には以下のような利点があります。

  • 後からクラスが増えても呼び出し側コードには変更が必要ない
  • 分岐の数がどれだけ増えても呼び出しにかかる時間が一定

ただし、仮想呼び出しには以下のようなコストがかかります。

  • テーブルを引くために間接参照が増える
    • ちなみに、今の .NET Coreの実装だとテーブルは2段構造になってるらしく、間接参照は2回
  • インライン展開が効かなくなる

特に、インライン展開が効くか効かないかで、だいぶパフォーマンスが変わります。

例えば以下のようなコードを考えます。

interface IValue { int Value { get; } }
class Impl : IValue { public int Value => 0; }
 
public class VirtualCallBanchmark
{
    // インターフェイス越し
    IValue A { get; } = new Impl();
 
    // クラスを直公開
    Impl B { get; } = new Impl();
 
    [Benchmark]
    public int Interface() => A.Value;
 
    [Benchmark]
    public int Class() => B.Value;
}

Interfaceメソッドの方はIValueインターフェイス越しにValueプロパティを参照(仮想呼び出しになるので遅い)で、 Classメソッドの方は具象型であるImplクラスを直接参照しています(仮想呼び出しが必要ないので速い)。

このベンチマークを実行すると、Classメソッドの方は「計測できないくらい何もしてない」と言われるくらい「ほぼ0時間」です。 一方、Interfaceメソッドの方には、手元の環境では 1.4ns くらいの時間が掛かりました。

この差はインライン展開によるもので、Classの方は、int Class() => 0; という単に定数を返すだけのメソッドに最適化されて、ほとんど何もコードが残りません。

1.4ns も微々たる時間ですが、このコストすらも避けたいことはよくあります。 それに、多態動作が特に必要ないなら仮想呼び出しはただ無駄なコストなので、できれば消したいものです。 なので、仮想メソッドであっても、必要がなければただのメソッド呼び出しに変換して、 インライン展開が効くようにする最適化を「devirtualization」(脱仮想化)と呼びます。

.NET Core 2.1 の devirtualize 最適化

ということで、.NET Core 2.1 から devirtulization 最適化が入っているみたいです。 (単純な最適化がいくつか 2.0 に対してマージされていて、ドキュメント上も 2.0 的な記述を見かけるんですが、 ベンチマークを取ってみてる感じは 2.0 ではあんまり有効じゃなさそう…)

以下のようなコードを書いた時、 .NET Core 2.0 以前と 2.1 以降で実行時間が大きく変わります。

public interface IX { void M(); }
public class X : IX { public void M() { } }

public class Program
{
    public void M()
    {
        IX x = new X();
        x.M();
    }
}

基本的には、この例のように、メソッド内でさかのぼれば具体的な型がわかる場合にだけ devirtualization 最適化が掛かります。 (この例の場合、new X()を呼んでいるところがすぐに見つかるので、MIX.Mの仮想呼び出しではなく、X.Mを直接呼び出します。)

.NET Core 2.1 以降なら、このコードは devirtualization された結果、インライン展開も効きます。X.M() の中身が空っぽなので何も残らず、実行時間が完全に0になります。

また、.NET Core 2.1 では devirtualization に関係する特殊な最適化もいくつか入っています。

値型のインターフェイス明示的実装

devirtualization が掛かって特にうれしいのは構造体です。 構造体に対してインターフェイスを介したメソッド呼び出しをする場合、 仮想呼び出しのコストに加えて、ボックス化のコストも掛かります(こっちは仮想呼び出しよりもさらにはるかに高コスト)。 devirtualization が掛かれば、ボックス化のコストももろとも最適化で消せます。

なので、以前にも書いたんですが、.NET Core 2.1 で、構造体のインターフェイス メソッド呼び出しに最適化が掛かりました

以下のような構造体があったとします。

struct X : IDisposable
{
    public bool IsDisposed;
    void IDisposable.Dispose() => IsDisposed = true;
}

で、この Dispose メソッドを以下のように呼び出してみます。

// (1) インターフェイス引数で受け取って呼ぶ
public static void Interface(IDisposable x) => x.Dispose();
 
// (2) X のまま受け取って、メソッド内でインターフェイスにキャストして呼ぶ
public static void NonGeneric(X x) => ((IDisposable)x).Dispose();
 
// (3) ジェネリックなメソッドで受け取って呼ぶ
public static void Generic<T>(T x) where T : IDisposable => x.Dispose();

(1)と(3)に関しては今も昔も変わらず、(1)が遅くて(3)が速いです。(2)についてが .NET Core 2.0 以前か 2.1 以降かで変わります。

.NET Core 2.0 以前だと、(2)の呼び出し方にはボックス化が掛かって遅かったんですが、 これが、.NET Core 2.1 からは devirtualization されて、(3) と同じ速度が出るようになりました。

EqualityComparer.Default

ジェネリックな型のインスタンスに対して等値比較する際、 EqualityComparer<T>.Defaultをよく使います。

public abstract class EqualityComparer<T> : IEqualityComparer, IEqualityComparer<T>
{
    public static EqualityComparer<T> Default { get; } // ← こいつ
    public abstract bool Equals(T x, T y);
    public abstract int GetHashCode(T obj);
}

EqualityComparer<T>.Default.Equals(x, y)の呼び出しは、 冒頭で出したIValue.Valueの呼び出し同様、 仮想呼び出しのコストが掛かります (インターフェイス越しの呼び出しと同様に、抽象クラス越しの呼び出しも、仮想呼び出しが必須になります)。

こういう抽象クラスで戻り値を返しているものは、 通常、devirtualization 最適化の対象になりません。 これに対して、.NET Core 2.1 では、 「EqualityComparer<T>.Defaultを見かけたら、抽象クラスの EqualityComparer<T>ではなくて、具象クラスに差し替えて処理する」というような特殊処理が入っていて、 その結果、devirtualization が掛かります。

例えば以下のようなベンチマークは、手元の環境で、 .NET Core 2.0 では 0.95ns、 .NET Core 2.1 では 0.05ns と、1桁実行速度が違います。

public class EqualityComparerDefaultBenchmark
{
    [Benchmark]
    public bool IntEquals() => EqualityComparer<int>.Default.Equals(1, 2);
}

Guarded Devirtualization

$
0
0

今日はちょっと将来の話。 提案ドキュメントとか予備実験的な実装はあるんですが、 リリースされる時期については未定のものです。

Guarded Devirtualization という最適化手法。

(余談ですが、この提案に当たっての調査レポート、ものすごく丁寧で良い内容です。 何かを提案する際の理想形。)

Devirtualization の実情

昨日のDevirtualization 最適化の話で書きましたが、 仮想呼び出しを通常のメソッド呼び出しに置き換える最適化があって、これを devirtualization といいます。

ただ、devirtualization できる状況はかなり限られています。 coreclr 内で統計を取ってみたところ、クラスの仮想メソッドの呼び出ししているところのうち15%程度しか、devirtualization 最適化が掛からないそうです。 インターフェイスを介しているものについてはもっときつくて、5%程度だそうです。

なんせ、devirtualization が有効になるためには、「メソッド内をさかのぼれば静的な型が1つに確定している」という状態でないといけない。 それに対して、実際のところ多い状況は、 「ほとんどの場合には決まったある1つの型のが来るものの、まれに別の型が来る」というものです。

if + Devirtualization

そこで、最頻で来てそうな1つ(あるいはせいぜい数個)の型に対してだけ if を挟んでしまうという最適化が考えられます。

例えば、以下のようないくつかの型があったとして

interface I { void M(); }
struct A1 : I { public void M() { } }
struct A2 : I { public void M() { } }
struct A3 : I { public void M() { } }
struct A4 : I { public void M() { } }

以下のような呼び出しを考えます。

static void M(I[] items)
{
    foreach (var i in items)
    {
        i.M();
    }
}

何の前提もないと、このコードは最適化のやりようがないんですが、 例えば、 「ほとんどの場合にA1A4の構造体が来る。他の型が来る率は低い」、 「その中でもA1の頻度が特に高い」みたいな前提が入ると、 以下のようなコードが速くなったりします。

static void M(I[] items)
{
    foreach (var i in items)
    {
        if (i.GetType() == typeof(A1)) ((A1)i).M();
        else if (i.GetType() == typeof(A2)) ((A2)i).M();
        else if (i.GetType() == typeof(A3)) ((A3)i).M();
        else if (i.GetType() == typeof(A4)) ((A4)i).M();
        else i.M();
    }
}

数個程度の if 分岐であれば仮想呼び出しのコストよりも安くなります。 特に、発生確率に偏りがある場合には分岐予測が効くので、 「ほとんどがA1」みたいな状況では分岐のコストがほぼ消えます。 また、メソッド M の実装がインライン展開可能なものだった場合、 インライン展開の効果でかなり速くなります。

ベンチマークを取ってみた感じ、 普通に i.M() で仮想呼び出しするよりも、3倍くらい高速です。

ということで、こういう「よく来る型」を実行時に検出して、上記のようなif分岐を生成するような最適化を CoreCLR に入れたいみたいです。 「ほとんどが A1」という予想が外れたときのための“防護策”(guard)としてif挿入するので、 Guarded Devirtualization と呼ばれます。

IEnumerator の別実装

$
0
0

Devirtualization 最適化の話で仮想呼び出しのコストの話もしました。 そこでもう1つ思い出してほしいのが、C# 8.0 Async streamsで書いた、 IAsyncEnumerator<T>インターフェイスの話。 最終的な決定としては以下のような API を持っています。

public interface IAsyncEnumerator<out T>
{
    T Current { get; }
    ValueTask<bool> MoveNextAsync();
}

一方で、検討段階では以下のような API も考えられていました。

public interface IAsyncEnumerator<out T>
{
    ValueTask<bool> WaitForNextAsync();
    T TryGetNext(out bool success);
}

今日はこの後者のメリットについての話。

参考コード: FastEnumeration

IEnumerator 版

同様の話は実は IEnumerator<T> にも言えます。 正確に言えば IAsyncEnumerator<T> の方が WaitForNextAsync を持っている分だけ複雑なんですが、とりあえず単純化のために IEnumerator<T> で話を進めます。

IEnumerator<T> インターフェイス(System.Collections.Generic名前空間)は、 歴史的経緯から非ジェネリックな IEnumerator インターフェイス(System.Collections名前空間)からの派生になっていますが、 そういう歴史的経緯を抜いて考えれば以下のような API を持つインターフェイスです。

public interface IEnumerator<out T>
{
    bool MoveNext();
    T Current { get; }
}

一方、冒頭で上げた検討段階の IAsyncEnumerator<T> と同じ考え方で、 以下のような API での実装が考えられます。 (区別のために名前をちょっと変えて、IFastEnumerator にしています。)

interface IFastEnumerator<out T>
{
    T TryMoveNext(out bool success);
}

要するに、MoveNextCurrentに分かれている機能を、1つのメソッドにまとめた方がいいのではないかという話です。

(.NET のジェネリクスでは、out 引数共変にできないという嫌な制限があるので、success の方が out 引数になっています。 可能であれば bool TryMoveNext(out T current) にしたいものです。 とりあえずそこは今回関係なく、あくまで「メソッドを1つにしたい」というところが今回の話の本質です。)

仮想呼び出しのコスト

まあ、冒頭で仮想呼び出しのコストの話を振っているのでどういう問題なのか察してもらえると思います。 単純に、そこそこコストが掛かる仮想呼び出しを2回に分けたくないという話です。

foreach が含まれるメソッドはたいていインライン展開されません。 そうなると devirtualization 最適化は大体かからなくなるんですが、 なので、MoveNext/Current には普通に仮想呼び出しのコストが掛かります。 結果、仮想呼び出しが2回。 一方の IFastEnumerator<T> であれば、仮想呼び出しは TryMoveNext の1回だけです。

ということで、どのくらい変わるかベンチマークを用意しました。

配列の中身を列挙するだけのクラスを2つ用意します。 片方は IEnumerator<T> 実装(本題に関係するところだけ抜き出し)。

class NormalEnumerator : IEnumerator<int>
{
    private readonly int[] _data;
    private int _i = -1;
 
    public int Current => _data[_i];
    public bool MoveNext() => ++_i < _data.Length;
}

もう一方は IFastEnumerator<T> 実装。

class FastEnumerator : IFastEnumerator<int>
{
    private readonly int[] _data;
    private int _i = -1;
 
    public int TryMoveNext(out bool success)
    {
        var i = ++_i;
        var data = _data;
        if ((uint)i < (uint)data.Length)
        {
            success = true;
            return data[i];
        }
        else
        {
            success = false;
            return default;
        }
    }
}

これに対して以下のようなループを回します。 (IEnumerator 版の方は、まんま、foreach の展開結果です。)

static int VirtualSum(IEnumerator<int> e)
{
    var sum = 0;
    while (e.MoveNext())
    {
        var x = e.Current;
        sum += x;
    }
    return sum;
}

// IFastEnumerator の方が1.5倍くらい速い。
static int VirtualSum(IFastEnumerator<int> e)
{
    var sum = 0;
    while (true)
    {
        var x = e.TryMoveNext(out var success);
        if (!success) break;
        sum += x;
    }
    return sum;
}

これの結果は、IFastEnumerator 版の方が1.5倍くらい高速です。 TryMoveNext の方が複雑なコードになりがちなので最適化も効きにくいんですが、 それ以上に仮想呼び出しのコストが高くて、TryMoveNext の方が速くなります。

インターフェイスをやめてしまえば…

ちなみに、あくまでこれは仮想呼び出しのコストの問題なので、 以下のように、インターフェイスを介さず具象クラスで呼ぶと、 むしろ MoveNext/Current 型の方が速くなります。

// さっきとの違いは引数の型だけ。
// IEnumerator インターフェイスだったのを、NormalEnumerator クラスに変えただけ。
// この場合は普通にこっちの方が速い。
static int NonVirtualSum(NormalEnumerator e)
{
    var sum = 0;
    while (e.MoveNext())
    {
        var x = e.Current;
        sum += x;
    }
    return sum;
}
 
// 同じく、IFastEnumerator インターフェイスを FastEnumerator クラスに変えただけ。
static int NonVirtualSum(FastEnumerator e)
{
    var sum = 0;
    while (true)
    {
        var x = e.TryMoveNext(out var success);
        if (!success) break;
        sum += x;
    }
    return sum;
}

とはいえ、汎用性がなくなるので具象クラスで受け渡しするのはちょっとつらいです。 ジェネリクスを使えば多少緩和はされるんですが…

// ジェネリクスを使えば、構造体の時には仮想呼び出しが消える。
// (構造体限定。クラスの時は別に仮想呼び出しは消えない。)
static int GenericSum<T>(T e)
    where T : IEnumerator<int>
{
    var sum = 0;
    while (e.MoveNext())
    {
        var x = e.Current;
        sum += x;
    }
    return sum;
}

速くなる(仮想呼び出しが消える)のは構造体限定です。 しかもなお悪いことに、foreach は、GetEnumerator を介する構造なので、 普通にやるとどうやっても仮想呼び出しが消えません。

static int Sum<T>(T items)
    where T : IEnumerable<int>
{
    var sum = 0;
    foreach (var x in items) sum += x;
    return sum;
} 
// ↑は↓みたいに展開される
static int Sum_<T>(T items)
    where T : IEnumerable<int>
{
    var sum = 0;
 
    // この GetEnumerator の仮想呼び出しは消える可能性があるものの…
    IEnumerator<int> e = items.GetEnumerator();
    try
    {
        // 結局ここの MoveNext/Current はインターフェイス越し。
        // 必ず仮想呼び出しになる。
        while (e.MoveNext())
        {
            var x = e.Current;
            sum += x;
        }
    }
    finally
    {
        if (e is IDisposable d) d.Dispose();
    }
    return sum;
}

ということで、foreach 中の MoveNext/Current の仮想呼び出しはなかなか消せなかったりします。 なのでなおのこと、1回で済む TryMoveNext 型のインターフェイスがよかったかもしれない、という話になったりします。

foreach の掛け方いろいろ

$
0
0

IEnumerator の別実装で、 インターフェイス越しの foreach には仮想呼び出しのコストが結構掛かっているという話を書きました。 (そちらでの主題は「なので、MoveNext/Currentの2つに分かれているのはちょっともったいない」という話でした。 もちろん、それを気にしないといけないのは大体パフォーマンス最優先のエクストリームな状況だけです。)

あと、配列のインデクサーでは、配列とSpan<T>構造体の列挙には C# のレベルでも JIT のレベルでも最適化が掛かっていて、かなり速いという話をしました。

今回はその辺りを踏まえて、列挙の仕方をいろいろ比較。

参考コード: ArrayEnumeration

内部的には配列なコレクション

List<T> (System.Collections.Generic 名前空間)とか、 ImmutableArray<T> (System.Collections.Immutable 名前空間)とか、 内部的に配列を持っていて、その上に何か機能を重ねたり(あるいは逆に書き換えを制限したり)している型は結構あります。 今回はその手の型の列挙について考えます。

とりあえず以下のような型を用意。参考にするために、配列を生列挙するコードも書いておきます。

using BenchmarkDotNet.Attributes;
 
public partial struct ArrayWrapper<T>
{
    // 比較のために生列挙をしたいので public (本来は不要というかむしろダメ)
    public readonly T[] Array;
    public ArrayWrapper(T[] array) => Array = array;
}
 
public partial class ArrayEnumerationBenchmark
{
    public ArrayWrapper<int> _array;

    // 比較のための生列挙。
    [Benchmark(Baseline = true)]
    public int RawEnumeration()
    {
        var sum = 0;
        foreach (var x in _array.Array) sum += x;
        return sum;
    }
}

とりあえず、結果:

Method Mean Error StdDev Ratio RatioSD
RawEnumeration 385.6 ns 1.031 ns 0.9646 ns 1.00 0.00

IEnumerable の実装

foreachで使いたいというのが主題なので、とりあえず先ほどの型に IEnumerable<T> インターフェイスを実装してみます。

とはいえ、インターフェイスを介した GetEnumerator/MoveNext/Current はちょっとオーバーヘッドが掛かるので、以下のような作りにします。 (List<T> なんかはまさにこの作りになっています。)

public partial struct ArrayWrapper<T> : IEnumerable<T>
{
    // 専用の型を作って、それを具象型のまま公開する
    public Enumerator GetEnumerator() => new Enumerator(Array);
 
    // インターフェイスは明示的実装にして別実装
    IEnumerator<T> IEnumerable<T>.GetEnumerator() => new EnumeratorObject(Array);
    IEnumerator IEnumerable.GetEnumerator() => new EnumeratorObject(Array);
}

専用実装(構造体)

まずは専用実装の方。 配列の全要素を列挙するような IEnumerator<T> 実装は以下のようになります。 無駄なアロケーションが発生しないように構造体製。

public partial struct ArrayWrapper<T>
{
    // 「仮想呼び出しは遅い」ということがわかっているわけで、
    // こんな感じで具象型を返す GetEnumerator を作った方が高速。
    // 構造体にした方が最適化が効く。
    public Enumerator GetEnumerator() => new Enumerator(Array);
 
    public struct Enumerator : IEnumerator<T>
    {
        private readonly T[] _array;
        private int _i;
        internal Enumerator(T[] array) => (_array, _i) = (array, -1);
 
        public T Current => _array[_i];
        public bool MoveNext() => ((uint)++_i) < (uint)_array.Length;
        // 残りは省略
    }
}
 
public partial class ArrayEnumerationBenchmark
{
    // 構造体の Enumerator 越しの列挙
    // 構造体で返してるとほんとにきっちり最適化が効くみたいで、
    // ほぼ配列生列挙と同じ速度が出る。
    [Benchmark]
    public int StructEnumeration()
    {
        var sum = 0;
        foreach (var x in _array) sum += x;
        return sum;
    }
}

これを使って foreach (var x in _array) とすると、 MoveNextCurrentもインライン展開されて、 最適化でほとんど配列の生列挙と同じコードに展開されます。 誤差の範囲内で生列挙と同じ速度が出ます。

Method Mean Error StdDev Ratio RatioSD
RawEnumeration 385.6 ns 1.031 ns 0.9646 ns 1.00 0.00
StructEnumeration 386.3 ns 1.100 ns 0.9751 ns 1.00 0.00

インターフェイス実装

構造体実装なものだけでは IEnumerable<T> インターフェイスの要件を満たさないので、 別途明示的実装を足します。

このとき、実装要件を満たすだけなら IEnumerator<T> IEnumerable<T> GetEnumerator() => GetEnumerator(); (構造体実装の GetEnumerator を素通しするだけ)でも構いません。 ただ、構造体をインターフェイス化して使うとかえって遅くて、 少しでもパフォーマンスを上げたりならクラスで作り直す方がよかったりします。

public partial struct ArrayWrapper<T> : IEnumerable<T>
{
    IEnumerator<T> IEnumerable<T>.GetEnumerator() => new EnumeratorObject(Array);
    IEnumerator IEnumerable.GetEnumerator() => new EnumeratorObject(Array);
 
    // 構造体の Enumerator と中身は全く同じで、ただクラスになってるだけ。
    // 構造体をインターフェイス越しに返すとかえって遅くなるので、こんなクラスが別途必要に…
    public class EnumeratorObject : IEnumerator<T>
    {
        private readonly T[] _array;
        private int _i;
        internal EnumeratorObject(T[] array) => (_array, _i) = (array, -1);
 
        public T Current => _array[_i];
        public bool MoveNext() => ((uint)++_i) < (uint)_array.Length;
 
        object IEnumerator.Current => Current;
        public void Dispose() { }
        public void Reset() => throw new NotImplementedException();
    }
}
 
public partial class ArrayEnumerationBenchmark
{
    // インターフェイス越し列挙になるように、IEnumerable<T> にキャストして使ってる。
    // びっくりするくらい遅い。
    // StructEnumeration とかに比べて10倍遅い。
    [Benchmark]
    public int InterfaceEnumeration()
    {
        var sum = 0;
        foreach (var x in (IEnumerable<int>)_array) sum += x;
        return sum;
    }
}

構造体/具象型実装が配列生列挙とそん色ないのに対して、 こいつは10倍以上遅いです。 それでも、別途クラスで実装した方がちょっとだけマシ。

Method Mean Error StdDev Ratio RatioSD
RawEnumeration 385.6 ns 1.031 ns 0.9646 ns 1.00 0.00
StructEnumeration 386.3 ns 1.100 ns 0.9751 ns 1.00 0.00
InterfaceEnumeration 4,407.3 ns 14.790 ns 13.8350 ns 11.43 0.05

出来合いの型

おまけで、出来合いの型を被せて返すのもやっておきます。

ReadOnlyCollection

配列を生で返したくない状況の1つが、書き換えを認めたくない場合です。 そういう場合、 ReadOnlyCollection<T> クラス(System.Collections.ObjectModel 名前空間)を使ったりします。

ただ、このクラス、IList<T> 向けなので、 配列だけでいいときには余計(繰り返しますが、インターフェイス越しは遅い)ですし、 .NET Framework 2.0 時代からあってパフォーマンスへの考慮はあんまりない型です。 要するに、遅い…

public partial struct ArrayWrapper<T>
{
    public ReadOnlyCollection<T> AsReadOnlyCollection() => new ReadOnlyCollection<T>(Array);
}
 
public partial class ArrayEnumerationBenchmark
{
    // ReadOnlyCollection<T> 列挙。
    // InterfaceEnumeration 以上に遅い。とにかく遅い。
    // ReadOnlyCollection<T> は内部的に IList<T> 越しに配列アクセスするので、それがほんとに遅い。
    [Benchmark]
    public int ReadOnlyCollectionEnumeration()
    {
        var sum = 0;
        foreach (var x in _array.AsReadOnlyCollection()) sum += x;
        return sum;
    }
}
Method Mean Error StdDev Ratio RatioSD
RawEnumeration 385.6 ns 1.031 ns 0.9646 ns 1.00 0.00
StructEnumeration 386.3 ns 1.100 ns 0.9751 ns 1.00 0.00
InterfaceEnumeration 4,407.3 ns 14.790 ns 13.8350 ns 11.43 0.05
ReadOnlyCollectionEnumeration 5,199.8 ns 21.591 ns 20.1960 ns 13.48 0.07

Span

まあ、今なら、特に .NET Core を使えるのであれば、 ReadOnlySpan<T> 構造体 (System 名前空間)を使うのがいいと思います。 Span<T> と同様最適化が掛かるので、 書き換えを防止しつつ、配列の生列挙とそん色ない速度が出ます。

public partial struct ArrayWrapper<T>
{
    // インデクサーも使いたいとき用、その2。
    // Span<T> を介してみる。
    // パフォーマンスに焦点が当たってた .NET Core 2.1 世代の型だけあって、かなり速い。
    public ReadOnlySpan<T> AsSpan() => Array;
}
 
public partial class ArrayEnumerationBenchmark
{
    // Span<T> 列挙
    // こいつも配列生列挙とほぼ同じ性能。速い。
    [Benchmark]
    public int SpanEnumeration()
    {
        var sum = 0;
        foreach (var x in _array.AsSpan()) sum += x;
        return sum;
    }
}
Method Mean Error StdDev Ratio RatioSD
RawEnumeration 385.6 ns 1.031 ns 0.9646 ns 1.00 0.00
StructEnumeration 386.3 ns 1.100 ns 0.9751 ns 1.00 0.00
InterfaceEnumeration 4,407.3 ns 14.790 ns 13.8350 ns 11.43 0.05
ReadOnlyCollectionEnumeration 5,199.8 ns 21.591 ns 20.1960 ns 13.48 0.07
SpanEnumeration 385.0 ns 1.612 ns 1.5079 ns 1.00 0.00

Span<T> の利用率アップ

ここまでの説明の通り下手すると10倍性能が違ったりするので、 .NET Core 2.1 が出て以降、 Span<T>ReadOnlySpan<T>を引数に取るAPIが増えていたりします。 IEnumerable<T>IList<T>が減って。

そうなると、既存のコレクションに対しても、「可能なもの(内部的に配列とか連続したデータになってるやつ)はSpanで取りたい」という要求がかなり高くなっています。

が、既存の型を改修してもらえるまで待てないという人も… Unsafe な手段で無理やり中身の配列を取得して、 無理やり Span にしてしまったり…

過渡的な手段とはいえ、結構邪悪です。 以下のようなコード。

[StructLayout(LayoutKind.Sequential)]
private struct ImmutableArrayProxy<T>
{
    internal T[] MutableArray;
}
 
internal static T[] DangerousGetUnderlyingArray<T>(this ImmutableArray<T> array)
     => Unsafe.As<ImmutableArray<T>, ImmutableArrayProxy<T>>(ref array).MutableArray;

また、Spanを使うと、中身を参照で外に漏らしちゃうことになるので、 ちょっと変な挙動をすることがあります。 以下のようなコードには注意を。

using System;
 
// System.Collections.Generic.List<T> と同じような実装 + AsSpan
class List<T>
{
    private T[] _buffer;
    private int _count;
    public List(int capacity) => _buffer = new T[capacity];
 
    public ref T this[int index] => ref _buffer[index];
    public ReadOnlySpan<T> AsSpan() => _buffer.AsSpan(0, _count);
 
    public void Add(T item)
    {
        if(_count == _buffer.Length)
        {
            var newBuffer = new T[_buffer.Length * 2];
            _buffer.AsSpan().CopyTo(newBuffer);
            _buffer = newBuffer;
        }
        _buffer[_count++] = item;
    }
}
 
public class Program
{
    static void Main()
    {
        var list = new List<int>(2);
        list.Add(1);
        list.Add(2);
        // この時点で容量満杯
 
        // Span 取得してから…
        var span = list.AsSpan();
 
        list.Add(3);  // Add で内部バッファーの再確保が発生
        list[0] = 99; // 新しいバッファーへの書き込み
 
        Console.WriteLine(span[0]); // 古いバッファーを参照してるので 1 のまま
        Console.WriteLine(list[0]); // 新しいバッファーを参照してるので 99
    }
}

まとめ

  • GetEnumerator を実装するなら、専用の構造体をまず考える
  • それとは別に、IEnumerable<T>.GetEnumerator を明示的実装
  • Span<T>/ReadOnlySpan<T> 速い
    • ので、パフォーマンス的には Span を使いたい
    • でも、参照が外に漏れるので注意

ピックアップRoslyn 12/21 & Connect() Japan フォローアップ

$
0
0

昨日、Connect(); Japan 2018でちょっとだけですけども、C# 8.0の話をしたりしました。 7分(ちょっと超過したけど)だとあんまり大したことを話せず…

とりあえず、昨日やったデモは、1機能1コミットでプルリクを作って GitHub においてあるのでそちらも参照してみてください。

で、今日は、Visual Studio 2019 Preview 1のその後のピックアップRoslynでパターン マッチングがらみの話が1件と、 機能やれなかったUfcppSampleデモのフォローアップ。

パターン マッチング

v2。

元々あった issueが長大になりすぎたので、今残ってる作業だけを抜き出して新しくissueを立てた模様。

今月1回書いてますけども、 パターン マッチングは Preview 1 に入ると思ってたけど入ってなかったって感じなんですが。 上記 issue はその現状で残ってる課題の一覧。

  • switch 式を、void も認めて、「式ステートメント」も認めたい
    • void M1()void M2() に対して、x switch { 1 => M1(), 2 => M2() }; みたいなのを認めたい
  • switch 式、末尾 , を認めたい
    • 今の実装だと x switch { 1 => M1(), 2 => M2(), } (M2()の後ろの,) を書くとエラー
  • 0, 1要素分解を認めたい
    • if (o is (3) _) みたいなの
    • キャスト+定数パターン o is (int)0 みたいなのとの弁別で悩み中
  • 名前付き引数でのオーバーロード解決を認めるかどうか
    • Deconstruct(int X, int Y)Deconstruct(double Angle, double Length)があるとき、p is (X: 3, Y: 4)で前者を呼べるようにするかどうか
  • プロパティ パターンで、インデクサーとかイベントとかを認めるか
  • ref構造体のトラッキングがバグってる
    • 今、パターン マッチングを使うと、本来返せないはずの Span<T> を返せちゃうバグあり
  • ITupleインターフェイス越しの分解と、Deconstructメソッド越しの分解の優先度をどうするか

UfcppSample に対して NullableReferenceTypes true

null許容参照型は待望の機能なわけですが、 1つ懸念としては、既存コードに対して適用するとどうなるかでしょう。 一応は、既存コードを壊さないようにopt-in (明示的にオプション指定しないと有効にならない)になっているわけですが、 「問答無用に全体に opt-in してしまうとどうなるか」は気になるところだと思います。

ということで、昨日は、時間が許せばC# によるプログラミング入門で書いてるコードに対して opt-in してみる話もしたかったんですが。 特に、うちのサイトは結構 C# 1.0 とか 2.0 の頃からある古いコードも残っていますし。 それに対して opt-in してみようと。

まあ、時間的に無理だったのでここで改めて。

普通な範囲

大半は、「意図して null を受け付けているところにちまちまと ? を付けていく作業になります。

これで、51件あった警告が、28件減って23件に。

ジェネリクス

Preview 1の実装では、結構ジェネリクス周りの実装が抜けています。 これに関しては、最近、Roslyn 上で generics がどうこうみたいなプルリクをよく見かけるので、Preview 2までにはだいぶ改善するかもしれません。

とりあえず、今はあきらめて(Preview 2で良くなることを祈って)、無視します。

基本的に、後置き!演算子を付けると、forgiving (警告もみ消しを容赦してもらう)になります。 ジェネリクスがらみにはこいつを使って対処。

ローカル関数に変更

ラムダ式に対して再帰したり、自分自身を参照したりするとき、以下のように、デリゲートをいったん空初期化した上で改めてラムダ式を代入する必要があります。

Func<int, int> f = null;
f = x => x <= 1 ? 1 : f(x - 1);

この、最初の = null がよくない。

で、これは単に、ラムダ式をローカル関数に書き換えるだけで解消します。

あと片付け

ガベコレで少しでも早く不要メモリを回収してもらうために、もう要らない変数に null を代入することもあったりします。

これに関しては、

  • 要らなくなる(Dispose する)までは絶対に null にならないので、T で使いたい
  • 要らなくなった後だけのために T? に変えるのはちょっと嫌

という感じ…

ちょっと迷ったんですが、結局は ! に頼ることにしました。

バグ

まあ、バグっててどうしようもない奴は #pragma warning disable で黙殺。

バグ報告済みなので、Preview 2までに治ってるといいなぁ…

ちなみに、このバグは Visual Studio 自体を落とします。

static class Ex
{
    // こういう、カリー化デリゲート(拡張メソッドを使ったデリゲート構築)に対する null 検証がバグってる。
    // 非 null なインスタンスを渡していても、なぜか null 警告が出る。
    // バグを黙殺するために ! を付けようとすると Visual Studio が落ちる。
    public static Action a = new object().M;
    public static void M(this object x) { }
}

デモ都合

C# によるプログラミング入門内には、「null がダメなのは百も承知で、もしそれでも null を渡してしまったらどうなるか」を示すデモがいくつかあります。

百も承知でわざとやってるんだからうるせー(おもむろに #pragma warning disable)。

どうしていいのかわからなかった奴…

で、6件ほど、ほんとにどう対処すべきなのかわからなくてとりあえず ! とか #pragma warning disable とかでやっつけたのが6件ほど。

デリゲートがらみはほんとに鬼門かも…


配列インデックスは0以上

$
0
0

今日は corefx (.NET の標準ライブラリ)の実装レベルの最適化の話。

.NET Core 2.0 とか 2.1 リリースの頃にブログも出ていましたが、 .NET Core 2.X 世代は結構パフォーマンス改善を頑張っています。

実際、 .NET Framework で動かしていたアプリを、 .NET Core 2.1 で動かすようにするだけで、アプリ側では何もしなくても1~2割くらいは高速化します。

改善の方法としては、Span<T>構造体を使ってアロケーションを減らしたり、Devirtualize 処理を掛けたりといったなんか上等そうな最適化もたくさんやっています。 でも、今日はそんな高尚なものの話ではなく、「.NET の配列のインデックスは0以上の整数 (int なのに負の値は絶対来ない)」と言う前提での細かい最適化の話です。

負のインデックス?

corefx/coreclr のプルリクエストで、ここ1年くらいの間、頻出する最適化がありまして。 配列操作で以下のようなコードはよく書くと思います。

if (index < 0 || index >= length)
    throw new IndexOutOfRangeException();

これを、以下のように書き換えるだけ。

if ((uint)index >= (uint)length)
    throw new IndexOutOfRangeException();

比較と OR が1回ずつ減っているので速くなるという理屈。 corefx 内で「uint length」で検索してもらえばわかりますけど、割かし大量に出てきます。

これは、でも、配列のインデックスが0開始のintだからできる最適化になります。 (元からインデックスがuintだった場合、int.MaxValueより大きいインデックスの時の挙動が狂う。)

Array.CreateInstance

.NET の配列のインデックスは必ず0以上。いいね?

ところで、以下のコードはどう思います。

using System;
 
public class Program
{
    static void Main()
    {
        var array = Array.CreateInstance(typeof(string),
            lengths: new[] { 4 },
            lowerBounds: new[] { -4 });
 
        array.SetValue("a", -4);
        array.SetValue("b", -3);
        array.SetValue("c", -2);
        array.SetValue("d", -1);
 
        foreach (var x in array)
        {
            Console.WriteLine(x);
        }
    }
}

ちゃんと動きます。 VB 6 時代の名残っぽいんですけども、 .NET では、0以外の開始インデックスを持つ配列を作る機能があります。 上記の例は、-4 開始。

ということで、先ほどの「配列長は0以上」を前提にしたコードがいいのかどうか、という話になったりもするんですが。 どうも、T[] 型にキャストすることができなくなるから大丈夫みたいです。

// 開始インデックス 0 を明示して CreateInstance
// それを string[] にキャスト。
// これは問題なく動きます。
var a1 = (string[])Array.CreateInstance(typeof(string),
    lengths: new[] { 4 },
    lowerBounds: new[] { 0 });
 
// 0 以外を指定した上で、全く同じく string[] にキャスト。
// InvalidCastException が発生。
var a2 = (string[])Array.CreateInstance(typeof(string),
    lengths: new[] { 4 },
    lowerBounds: new[] { -4 });

ちなみに、例外メッセージは以下のような感じ。

txt Unable to cast object of type 'System.String[*]' to type 'System.String[]'.

String[*] 型… だと… つまるところ、開始インデックスが型情報の一部… (実際には、「0開始のやつ」と「0じゃないやつ」の2つの型しかなくて、1開始と-1開始みたいなやつはどちらも T[*] 型になって、同じ型みたいです。)

配列だけ特殊対応してもらってていいなぁ… C++ の template みたいに、C# のジェネリクスでも型引数に整数を渡したいのに… (できない)

まあ、それはさておき、とりあえず、.NET の T[] で書かれる配列のインデックスは0以上の整数と思っていいみたいです。

最上位ビットを流用

配列のインデックスをフィールドに持つような型がちらほらあります。

Memory

例えば、Memory<T>構造体がそうなんですが、これの中身は以下のような感じ。 (_objectには配列、もしくは、MemoryManager<T>型が入ります。)

public readonly struct Memory<T>
{
    private readonly object _object;
    private readonly int _index;
    private readonly int _length;
}

ここで、_index_length、つまり、配列のインデックスと長さは絶対に負にならない保証があります。 負にならないということは、intの内部構造的には、最上位ビットが常に0です。

ということで、Memory<T>構造体では、この最上位ビットを別の用途に流用することで最適化していたりします。 具体的には、_objectの中に実際に何が入っているかを弁別するために_lengthの最上位ビットを使っていたりします (配列がpinned済みかどうか(GCHandleType.Pinned指定でGCHandle.Allocされているかどうか)を記録しています)。

Index

C# 8.0 では、以下のような構文で、 配列の一部分をSpan<T>として切り出すことができるようになります。

int[] array = { 1, 2, 3, 4, 5 };
var sub = array[1..^1]; // 先頭から1 ~ 末尾から1 の範囲
 
// 2, 3, 4
foreach (var x in sub)
{
    Console.WriteLine(x);
}

この、インデックスの値が先頭からなのか末尾からなのかを表すために、 以下のような Index 構造体が追加されています(主要部分のみ抜き出し)。

public readonly struct Index
{
    private readonly int _value;
 
    public Index(int value, bool fromEnd)
    {
        _value = fromEnd ? ~value : value;
    }
 
    public int Value => _value < 0 ? ~_value : _value;
    public bool FromEnd => _value < 0;
}

これも、配列のインデックスは0以上という前提で、 「末尾から」の方を負の数で表すことで、追加の bool フラグを持たないようにしています。

JIT Intrinsics

$
0
0

.NET Core 2.1 では、いくつか、JIT 時の特殊対応によるパフォーマンス改善を行っています。

そういう「特殊対応」を intrinsic (固有の、内在的な、内因的な、本質的な)と呼びます。 「JIT 時の特殊対応」であれば「JIT intrinsic expansions」(固有展開)とか「JIT intrinsics」(s が付くことで名詞化してる。economics とかの s と同じ)と言います。

Intrinsic 属性

JIT 時特殊対応をしているクラスやメソッドには Intrinsic 属性が付いています。 この属性を参照しているものを検索することで、どこで特殊対応が行われているかを追うことができます。

Vector

記憶にある限り、Intrinsic 属性が最初に使われたのは Vector<T> 構造体(System.Numerics名前空間)です。

この型は SIMD 演算を行うためのもので、

  • SIMD に対応している環境では、SIMD 命令を使った実装に差し替える
  • そうでない場合、vector1.X * vector2.X + vector1.Y * vector2.Y + ... というような通常の C# コードを使う

と言うような処理をしています。

.NET Core 2.1 での最適化

.NET Core 2.1 でいくつか intrinsic なものが増えています。 そのうちいくつかを紹介。

EqualityComparer.Default

Devirtualize 処理で説明した通り、.NET Core 2.1 ではEqualityComparer<T>.Defaultに対して具象型を返す用ような最適化が入っています。

なので、例えば、EqualityComparer<int>.Default.Equals(1, 2)みたいなコードは、.NET Core 2.0 よりも、2.1 の方が1桁高速です。

Enum.HasFlag

Enum.HasFlag も .NET Core 2.1 で intrinsic な最適化が掛かったものの1つです。

HasFlag相当の処理は、具体的な列挙型がわかっていれば以下のような書き方ができます。 ただの & と0比較なので、かなり高速です。

static bool HasFlag(A x, A y) => (((int)x) & ((int)y)) != 0;

ところが、任意の列挙型に対して使えるようにしようとすると途端に面倒になります。 Enumクラス(System名前空間)のHasFlagメソッドで機能としては提供されているんですが、 このHasFlagメソッドはむちゃくちゃ遅いです。

そこで、.NET Core 2.1ではJIT時に特殊対応するようにしました。 HasFlagメソッドを見たら単なる&に置き換える処理が掛かっています。 「Enum.HasFlag でのボックス化」で説明しているように、 .NET Core 2.0以前と2.1以降で実行速度に20倍以上の差があります。

Span のインデクサー

Span<T>構造体のインデクサーにも JIT 時特殊対応が入っています。

これは、配列のarray[i]と同じような最適化です。 「配列のインデクサー」で説明したように、配列のインデクサーには

  • 何もしなければ、array[i] のところに暗黙的な範囲チェックを追加する
  • 明示的な範囲チェックがあれば、余計な範囲チェックの追加はしない

と言うような処理が掛かっています。 これと同じことを、.NET Core 2.1ではSpan<T>のインデクサーに対しても行っています。 特殊対応なしだと、Span<T>のインデクサーは常に範囲チェックを必要としますが、 .NET Core 2.1 以降だと、必要に応じて範囲チェックの削除が行われます。

静的な typeof/sizeof

$
0
0

JIT Intrinsicsで少し触れましたが、 .NET Core 2.1ではEnum.HasFlagに対する最適化が掛かります。 .NET Core 2.0と2.1でEnum.HasFlagの実行速度が1桁違うわけですが、 古いランタイムでも何とかする手段がなくもないです(ただし、Unsafe)。

今日はそんな、.NET Core 2.0以前でも使える最適化の話。

定数最適化

例えば、以下のようなコードを考えます。

static int X()
{
    if (true) return 1;
    else return 0;
}

if の条件式が定数なので、これは C# のコンパイル時に最適化が掛かって、 return 1だけが残ります。if相当のコードは出力されません。 このように、コンパイル時に確定している値や条件分岐などは、きれいさっぱり消えることがあります。

JIT 時定数

中には、C# コンパイル結果としては定数にならないものの、 JIT のタイミングでは定数と判明して、最適化が掛かるものがあります。

ジェネリック型引数に対するtypeof(T)sizeof(T)はまさにそういう「JIT 時に定数になるもの」です。 例えば以下のようなコードは C# コンパイラーは条件分岐を生成しますが、 JIT 時の最適化が掛かって、条件式が一致している行だけを残して消えてくれます。

static long MaxValue<T>()
{
    if (typeof(T) == typeof(byte)) return byte.MaxValue;
    else if (typeof(T) == typeof(short)) return short.MaxValue;
    else if (typeof(T) == typeof(int)) return int.MaxValue;
    else if (typeof(T) == typeof(long)) return long.MaxValue;
    // お好みで、sbyte, ushort, uint, ulong もどうぞ
    else throw new InvalidOperationException();
}

Enum.HasFlag の代わり

ということで、この手の分岐を書いて、ジェネリックな HasFlag を書いてみましょう。

using System;
using System.Runtime.CompilerServices;
 
public static class EnumExtensions
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool UnsafeHasFlag<T>(T x, T y)
        where T : unmanaged, Enum
    {
        if (Unsafe.SizeOf<T>() == 1) return (Unsafe.As<T, byte>(ref x) & Unsafe.As<T, byte>(ref y)) != 0;
        else if (Unsafe.SizeOf<T>() == 2) return (Unsafe.As<T, ushort>(ref x) & Unsafe.As<T, ushort>(ref y)) != 0;
        else if (Unsafe.SizeOf<T>() == 4) return (Unsafe.As<T, uint>(ref x) & Unsafe.As<T, uint>(ref y)) != 0;
        else if (Unsafe.SizeOf<T>() == 8) return (Unsafe.As<T, ulong>(ref x) & Unsafe.As<T, ulong>(ref y)) != 0;
        else { Throw(); return default; }
    }
 
    private static void Throw() => throw new InvalidOperationException();
}

このコードで、非ジェネリックな場合の、

static bool HasFlag(A x, A y) => (((int)x) & ((int)y)) != 0;

みたいなコードとそこまで差がない性能が出ます。 さすがに最適化をちょっと阻害されるみたいで全く同じとは行きませんが、 少なくとも Enum.HasFlag みたいに1桁遅くなることはありません。 せいぜい数割差です。

Unsafe.SizeOf<T>() は内部的に sizeof(T) を呼んでいるだけです。 単にジェネリック型引数に対して掛けるようにしただけ。 (C# 7.3 移行で unsafe コードであれば、普通にジェネリック型引数に対しても sizeof(T) を掛けるようになりました。一方、C# 7.2 以前だと Unsafe.SizeOf<T> メソッドが必須です。)

先ほどの説明の通り、sizeof(T)はJIT時定数になるので、 このUnsafeHasFlagメソッドは、ちゃんと1行だけ残して残りのサイズが違うコードはきれいさっぱり消えます。 この最適化は結構昔から掛かっているものなので、.NET Core 2.0以前でも働きます。 (と言っても、Unsafeクラスが対応している必要があるので、.NET Framework 3.5とかでは動かせません。 Unsafe.As相当のILコードを自分で書けば使えますが…)

ちなみに、Unsafe.As<T, byte>(ref x) の方は変数の型を無理やり変更するもので、通常の C# ではどうやっても書けません。 メソッドの中身は IL で書かれています。 (また、Intrinsic属性が付いているので、おそらく .NET Core 2.1移行ではJITレベルでの最適化も何か掛けていそうです。)

書記素分割/Unicode カテゴリー判定

$
0
0

なんか、昔作ったGraphemeSplitterC++方面のUnicodeがらみのブログから参照されてたので、ちょっと補足。

UNICODE TEXT SEGMENTATION

「書記素って何?」って話は詳しくは昔書いた記事でも見てもらうとして。 とりあえず、「人間が見て1文字と思うようなもの」を指して書記素(grapheme)といいます。複数の Unicode コードポイントが結合しまくるので、可変長。

いつも例に出すのが家族絵文字(👩🏻‍👦🏼👨🏽‍👦🏾‍👦🏿👩🏼‍👨🏽‍👦🏼‍👧🏽👩🏻‍👩🏿‍👧🏼‍👧🏾とか)ですが、1書記素で11コードポイント、UTF-8で41バイトになったりします。

で、問題は、書記素の機械的な判定方法。 コンピューター上でもちゃんと書記素単位で処理してくれないと、人間の感覚からすると「backspace/delete を押すたびに文字が変わる」みたいな変な感じになります。

Unicode 標準としては、「あくまで参考。もっといいアルゴリズムにしてもらってもいいけど」という但し書き付きですが、以下のようなドキュメントがあります。

書記素の区切り(grapheme cluster boundary)だけじゃなくて、単語区切り(word boundary)や文区切り(sentence boundary)についても言及。

基本的には、「このカテゴリーのコードポイントの後ろにこのカテゴリーが来たら繋げろ(あるは、そこで区切れ)」というルールが示されていて、そのルール自体は割と単純です。カテゴリーさえわかっていれば。 自分が書いた実装でも、コメント・空行を除けば16行。

コードポイントのカテゴリー

真の問題はカテゴリー判定。

Unicode では、コードポイント1つ1つにいろいろな属性が定義されています。 例えば、C# でGetUnicodeCategoryで取れるやつは「general category」というやつで、 「UnicodeData.txt」(;区切り)の3列目に定義があります。

UnicodeData.txt の中身を見れば何がきついかわかっていただけると思います。 こいつ、(Version 11 時点で)32292行もあります。 何らかの計算式があるとかではなく、愚直にテーブル。 そりゃまあ、それしかやりようがないのはわかりますが…

UnicodeData.txt 1個でもでかいのに、さらに追加で別の定義ファイルを参照せざるを得ない処理なんかもあったりします。 書記素区切りはその1つで、GraphemeBreakProperty.txt内のデータが必要になったり。

この問題は別に絵文字とか書記素分割だけのものでもなくて、 例えば ToLower/ToUpperの実装とかでも問題になります。

テーブルの引き方(自分の実装)

GetUnicodeCategoryで取れるカテゴリーだけで判別できるんだったら楽なんですけどね。 自分が書いたコードが3千行近くなった理由は、GraphemeBreakProperty.txt で定義されたカテゴリーが必要だったからです。

当たり前ですけど、こんなのコード生成で作ってます。

実行速度とか生成されるDLLサイズとかを比較するために数パターンのコード生成をやってみていて、全部 switch case に展開したやつ(約2万行)とかもあったりします(コンパイルするだけで1分くらいかかります。)。結局、まあ、二分探索でやるパターンを採用したのが上記の3千行近いコード。

テーブルの引き方(.NET Core の実装)

.NET 標準のGetUnicodeCategory とか GetNumericValueとかも、 やっぱりテーブルを引く実装になっています。 テーブルのデータは以下のコード中にあり。

13万文字以上もあるものを愚直にテーブル化するわけにもいかないので、11:5:4ビットに区切った3段テーブルになっています。 (同じカテゴリーが連続していることが多いので、こういう分け方をするとデータ量が減る。 それでも23KBほどのサイズ。)

もちろんこいつもコード生成。 UnicodeData.txt からこのテーブルを生成するコードも coreclr 内にあります。

バージョン

テーブル実装のなお悪いところは、バージョンが変わるとテーブル自体を作り直すしかないところでして。

例えば先ほどの CharUnicodeInfoData.cs ですが、Unicode 11 にアップデートした時のプルリクエストがこちら:

まあ、「Files changed」で差分を見てみてください。結構な分量。

しかも、Unicode、ほとんどの場合は「追加」なんですが、 たまーに破壊的変更もやるんですよね。 Unicode 標準に追従すると、そのフレームワークも破壊的変更を起こすことが。

JavaC#やられてますが、Unicode のカテゴリー変更のあおりを受けています。

そうなると、指定したバージョンの Unicode 文字カテゴリーを取れる API も欲しいところなんですが… 1バージョン辺り23KBとかのサイズになるわけで、それを10以上あるバージョンすべてで持つのも結構な負担です。

char と CharUnicodeInfo

ちなみに、char.GetUnicodeCategoryCharUnicodeInfo.GetUnicodeCategory で結果が違うという邪悪なおまけつき。

どうも、昔からある char の方の実装は Unicode 4.0 がベース、 CharUnicodeInfo は Unicode 5.0 がベース(最近 11.0 ベースに更新)だそうです。

char の方を「破壊的変更になるし変えれない」とか中途半端にやった結果こうなったとか。 しかも、完全に Unicode 4.0 のままなんじゃなくて、 Latin-1 の文字だけ 4.0 の時のカテゴリーのままで、残りは更新されていそうという

International Components for Unicode

そんな感じで、Unicode のカテゴリー判定は結構つらい作業です。 なので、OS に ICU が入ってることを期待して、それを参照するのがいいのかも… (自前で ICU のバイナリを同梱しようとすると20MBを超えます。)

書記素、単語、文、行の区切りの実装もあります。

ちなみに、Windows 10 には標準で ICU が組み込まれてるそうです。

Span利用による最適化

$
0
0

このブログではたびたび「.NET Core 2.1 上で動かすだけで、アプリ側には何も手を加えなくても 2.0 の頃より1・2割高速になる」みたいな話をしています。

今月に入ってからは、DevirtualizationみたいなJIT時の最適化手法や、 逆にもっと小手先の細かな最適化の話も書いてきました。 .NET Core 2.1 ではこういういろいろな最適化が入っているんですが、 その中でも一番パフォーマンス改善に効いていそうなのがSpan<T>構造体の導入です。

Span<T>構造体自体の説明は何度かしていますが、 Span<T>を使ってどういう修正をしているかについてはあんまり書いていないので、 今日は実例をいくつか挙げていこうかと。

ヒープ使用量の削減

Span<T> を使うと速くなる理由は単純で、 ヒープの使用量を減らせるからです。

  • string.Substring などで新しい文字列を作らなくて済む
  • stackallock で、一時バッファーにヒープを使わなくて済む
  • ネイティブ メモリを直接読めるようになったことで、マネージ配列にコピーしなくて済む

いずれも、unsafe コードでポインターを使えばこれまでも十分に実現できたものです。 しかし、安全性・生産性を犠牲にしたコードは書くのも使うのも神経を使うので大規模には導入しにくですし、 ガベコレ都合の制限もあって、 Span<T> なしでは難しい最適化です。

Span<T> もいろいろと制限の掛かった特殊な型(ref struct)ですが、それでもポインターよりは適用可能な範囲が広いです。

Substring

.NET の string.Substring は、新しい string 型インスタンス(もちろんヒープを使う)を作ってそれを戻り値に返します。

下手に仮想呼び出しが増えるよりは、無駄にヒープを使っちゃう方が高速だからそういう作りなんですが、 Span<char> があればヒープを使わず似たようなことができます。

ということで、SubstringAsSpanにちまちま変更していくようなプルリクエストが。

Substring に限らず文字列操作がらみはかなりSpan<T>の恩恵を受けていて、 倍以上速くなったメソッド何かもあるみたいです。

stackalloc

極々短い範囲で、小さいデータを持っておくだけの一時バッファーを必要とすることは結構あります。 そんな時、これまでだと配列(ヒープを使う)を使っていたんですが、 Span<T> があれば stackallocが安全になるので、 ヒープ利用を避けることができます。

以下の修正では、固定長で2文字のcharのためにnew char[2]していたものをstackallocに置き換えています。

//before
char[] chars = new char[2] { highSurr, lowSurr };
 
//after
ReadOnlySpan<char> chars = stackalloc char[2] { highSurr, lowSurr };

ただし、.NET の実装では、メモリのスタック領域は固定長で 1MB くらい(確か)なので、 あんまり大きなデータをスタックに置こうとすると簡単に stack overflow を起こしたりします。 先ほどのような固定長で短いデータはいいんですが、可変長の場合にはひと工夫必要です。

具体的には、要するに「一定サイズ以下の時にだけstackallocを使う」という分岐を挟むだけなんですが。 以下のプルリクエストなんかはわかりやすいです。

以下のような条件演算子は結構頻出です。

Span<byte> datetimeBuffer = ((uint)length <= 16) ? stackalloc byte[16] : new byte[length];

ちなみに、以下のような型もあります(今のところ internal ですが)。 StringBuilder相当の処理を、 初期バッファーをstackalloc、その後容量を増やすときにはArrayPoolを使う実装。 これも、「一定サイズ以下の時にだけstackallocを使う」最適化の一種です。

ネイティブ メモリを直接

Span<T> を使うと、は配列でも、stackalloc で確保したスタック領域でも、 ネイティブ メモリでも共通処理が書けます。 なので、ネイティブ相互運用時に、 C# 側で一時配列を確保してネイティブ コードにポインターを渡す以外に、 ネイティブ側からポインターを返してもらってそれを C# 側でSpan<T> を介して処理するということもできます。

これも、一時バッファーの確保が不要になるのでパフォーマンス改善につながったりします。

最近だと、ML.NET内で、 TensorFlorとの相互運用でもネイティブ メモリの読み書きに Span<T> を使っていたりします。

Viewing all 482 articles
Browse latest View live