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

将来のメモリ管理

$
0
0

今日はまたちょっと将来の話。 リリース時期・本当にリリースされるか未定の機能で、メモリ管理がらみの話をまとめて。

ヒープ確保の負担を減らしたい

メモリの管理方法にはスタックヒープがあって、 一般的にはスタックの方が高速です。 スタックの方が制限がきついので、遅くてもしょうがなくヒープを使う場面がでてきます。

ヒープ管理は結構大きな負担なので、これを減らせば結構なパフォーマンス改善になります。 いくつか方向性があって、以下のような最適化が考えられます。

  • ヒープ管理自体を賢くする
  • ヒープを避ける
    • 手作業でヒープを避ける
    • 自動でヒープを避ける
      • → Object Stack Allocation (後述)

ガベージ コレクション

一般的に、こまごまと個別に処理をするよりも、ある程度まとまった単位でごっそり処理する方が効率がよかったりします。 ガベージ コレクションというメモリ管理手法はまさにそんな感じで、 「ごみはしばらくほったらかし」→「定期的にごっそりまとめてゴミ集め」とかやっていて、 これがスループット的には結構有利だったりします。 「まとめてゴミ集め」の瞬間に負荷が集中するという問題はあるものの、 トータルでみると低負荷なメモリ管理手法です。

メモリ管理が手動(alloc したものは開発者が責任をもって free しないといけない)な言語でも、内部的にガベージ コレクション的な挙動をしているものがあるくらいです。 gperftoolsの tcmalloc なんかはそうみたいです。 小さいオブジェクトの場合だけですが、freeした瞬間に即座にその領域を解放するのではなくて、 メモリが足りなくなってきたときにまとめて解放処理をします。

ガベージ コレクション以外のヒープ管理手法は、 たびたび提案されては、 そのたび「世代別 GCに性能で勝てなかった」的な結論に達したりします。 .NET でも、Project Snowflakeという研究プロジェクトがありました (Rustみたいに、メモリの所有権をはっきりさせればヒープ管理が速くなるんじゃないかという原理に基づく提案)。 結構賢そうなことをやっているんですが、それでも結論は「利益がコストに見合わない」でした。

Arena Allocation

Project Snowflake に代わって1つ有望視されているのは、 Arena Allocation という手法です。

Protocol Buffers の C++ 版がこの手法によるメモリ管理を提供しているんですが、それを .NET にも導入できないかという調査をしているみたいです。 まだあんまりドキュメントがなく、QConSFの登壇で軽く紹介された程度ですが。

これも、「ある程度まとまった単位でごっそり処理する方が高効率」という原理に則ったものです。 以下のように、「ごっそり消す」タイミングを明示するような方式。 メモリ放棄のまとまった単位を指して arena (舞台、競技場、界)と呼んでいます。

Arena arena = new Arena();
using (arena.Activate())
{
    // この内側で new したものは「arena」内に確保される
}
// arena 内のオブジェクトは arena の Dispose 時にまとめて解放
arena.Dispose();

この方式はトランザクションのスコープがはっきりしているものに対して有効です。 Protocol Buffers が採用していることからわかるように、シリアライズ・デシリアライズが好例です。 シリアライズの途中でしか使わない一時的なバッファーを Arena 中に確保して、 シリアライズ完了時にまとめて解放すれば効率よくゴミを片付けられます。

Object Stack Allocation

クラスのインスタンスは基本的にはヒープ上に確保するものなんですが、 「メソッド内で完結している」(引数にも渡さないし、戻り値にも返さない)という状況に限って、 スタック上に領域確保しても問題なく動いたりします。

そこで、JITコンパイラーが「メソッド内で完結している」かどうかを判定して、 完結していればクラスのインスタンスであってもスタック上に確保する最適化手法(Object Stack Allocation)があります。 「メソッド内から逃げ出していないかを解析する」という意味で、Escape Analysis と呼ばれたりもします。

Java SE では Java SE 6 の頃から Escape Analysis を実装しています。 Go なんかは最初から Escape Analysis ありきで作られています。 (要するに、結構昔からある最適化手法。) それがこの度、.NET にも入りそうです。

Java と比べてずいぶん採用が遅いですが、 要は、C# は値型を持っているのでそもそも手作業でヒープを避ける手段があるからです。 挙句、「Span<T> 利用による最適化」で説明したような手動最適化がの方が断然効果的なので、そっちの方が優先されています。 (Escape Analysis は「メソッド内から逃げ出していない」という条件が思いのほか厳しいので、適用できる割合はそんなに高くない。)

「コンパイラーとかランタイムが頑張るよりも手作業の方がまだまだ高効率」という話なので、 あんまり夢はない感じ… (Span<T>も、「ガベコレで追えるものを増やす」という作業は必要だったので、 ランタイムが何もしていないわけではないんですが。)


Unsafe クラス(保証外)

$
0
0

今日は Unsafe クラスがらみの話で、 特にきわどい(動作保証外)やつ。 .NET Core 2.0 ~ 2.1 くらいでは動くことを確認していますが、 仕様として保証がなく、古いランタイムや将来、また、Mono などの他の .NET 環境で動く保証がないものです。

メモリレイアウトが同じもの

まず、元々 unsafe コードを使ってできるし、 Unsafeクラスを使っても動作保証があるものから説明。

ポインターを使ったり、Unsafe.Asメソッドを使うと、 全然違う型・C# では本来変換できない型同士の間で強制変換ができます。 強制しているだけなので、それがちゃんと意味あるコードになるかどうかは unsafe、 すなわち、書いている人の責任の範疇になります。

どういう場合なら大丈夫かというと、要するに、 メモリ上でのフィールドなどのレイアウトが同じ場合です。 例えば、以下のような、サイズが同じで参照型を含まない構造体同士は強制変換しても大丈夫です。

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
 
// 構造体サイズが4バイトになるようにフィールドを並べる
// この場合は別に StructLayout 属性がなくても4バイトになるものの、
// サイズをピッタリ調整したい場合には明示した方がいいかも。
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct A
{
    public byte X;
    public byte Y;
    public short Z;
}
 
// int 1個なので当然4バイト。
struct B
{
    public int X;
}
 
public class Program
{
    static void Main()
    {
        // サイズが同じで参照型を含まない構造体間での強制変換は、
        // 普通にポインターを使ってできる操作なので
        // unsafe ではあってもまだ動作保証はある。
        B b = new B { X = 0x01020304 };
        A a = Unsafe.As<B, A>(ref b);
 
        // 4, 3, 102
        Console.WriteLine($"{a.X}, {a.Y}, {a.Z:x}");
    }
}

保証外な利用方法

Unsafe.Asメソッドを使うと、 こういった強制型変換を参照型に対しても行えます。

ただ、これは動作保証がないようです。 少なくとも .NET Core 2.1 では動いているんですが、 将来にわたってもそのまま動くかと言われると何も保証されていません。

共変クラス

C# の変性はインターフェイスとデリゲートに対してしか働かないわけですが、それを強制的にクラスに対しても適用できたりします。

// string → object の代入が合法なんだったら…
string s = "abc";
object o = s;
 
// Task<string> → Task<object> も OK にしてほしい
Task<string> ts = Task.FromResult("abc");
 
// 実際は無理
// Task<object> to = ts;
 
// が、Unsafe.As ならできてしまう。
Task<object> to = Unsafe.As<Task<string>, Task<object>>(ref ts);
 
// await でちゃんと "abc" が取れる
var result = await to;
Console.WriteLine(result);

ただ、これは Task<TResult>クラス(System.Threading.Tasks名前空間)のTResultが戻り値にしか使われていないから大丈夫なのであって、 例えば以下のように、読み書き両方できるとまずいです。

using System;
using System.Runtime.CompilerServices;
 
class Box<T> { public T Value; }
 
public class Program
{
    static void Main()
    {
        // string → object の代入が合法なんだったら…
        Box<string> s = new Box<string> { Value = "abc" };
        Box<object> o = Unsafe.As<Box<string>, Box<object>>(ref s);
 
        // 読み出しはまだ大丈夫。"abc" が表示される。
        Console.WriteLine(o.Value);
 
        // 書き込みはアウト。
        o.Value = 10;
        // ダメなことをやっちゃったあとなので、何か動作がおかしい。
        // 最悪の場合死に至るのでダメ、絶対!
        Console.WriteLine(o.Value);
    }
}

また、stringobject が大丈夫だから Task<string>Task<object> も大丈夫だったのであって、互換性がない型同士での Task<T> 間の変換はもちろんダメです。

using System.Runtime.CompilerServices;
using System.Threading.Tasks;
 
// 無関係のクラス
class C1 { }
class C2 { }
 
// A, B は同じ4バイト
// C は1バイト
struct A { public int X; }
struct B { public short X, Y; }
struct C { public byte X; }
 
public class Program
{
    static void Main()
    {
        // ヤバい(無関係のクラス)
        Task<C1> c1 = Task.FromResult<C1>(null);
        Task<C2> c2 = Unsafe.As<Task<C1>, Task<C2>>(ref c1);
 
        // 保証外だけどギリ動く(サイズが同じ)
        Task<A> a = Task.FromResult(new A());
        Task<B> b = Unsafe.As<Task<A>, Task<B>>(ref a);
 
        // ヤバい(サイズが違う)
        Task<C> c = Unsafe.As<Task<A>, Task<C>>(ref a);
    }
}

シグネチャが同じデリゲート

デリゲートは、引数・戻り値の型が完全に一致していても、 別個に定義したものは別の型扱いを受けます。

そして、引数・戻り値の型が完全に一致しているデリゲート型は山ほどあります。 例えば以下のような。

  • IValueTaskSourceAction<object> を使用。object引数、void戻り値。
  • TimerTimerCallback を使用。object引数、void戻り値。
  • SynchronizationContextSendOrPostCallback を使用。object引数、void戻り値。

そして、これらのデリゲート間の変換では、以下のように new が挟まってしまって、無駄にメモリを食います。

using System;
using System.Threading;
using System.Threading.Tasks.Sources;
 
class MyValueTaskSource : IValueTaskSource
{
    private SynchronizationContext _context;
    public void GetResult(short token) { }
    public ValueTaskSourceStatus GetStatus(short token) => ValueTaskSourceStatus.Succeeded;
    public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
    {
        // こういう書き方は無理。
        // _context.Post(continuation, state);
 
        // こうなる。
        _context.Post(continuation.Invoke, state);
 
        // ↑これは意味的には↓と同じ。1段 new が挟まってて、ヒープも確保される。
        // _context.Post(new SendOrPostCallback(continuation.Invoke), state);
    }
}

でも、Unsafe.Asメソッドを使えば無駄な new なしで強制変換できます。

// でも、これで行けたりする。
_context.Post(Unsafe.As<Action<object>, SendOrPostCallback>(ref continuation), state);

引数・戻り値の型が一致している限りには、 少なくとも .NET Core 2.1 とかでは動きます (再三いうけども動作保証があるわけじゃない)。

using System;
using System.Runtime.CompilerServices;
using System.Threading;
 
public class Program
{
    static void Main()
    {
        Action<object> action = x => Console.WriteLine(x);
        SendOrPostCallback callback = Unsafe.As<Action<object>, SendOrPostCallback>(ref action);
 
        callback("abc"); // ちゃんと Console.WriteLine("abc") が呼ばれる
    }
}

静的な型と動的な型

ちなみに、Unsafe.Asメソッドでの共生型変換を、互いに無関係なクラスでやってしまうと結構変な動作になります。 以下のように、全然無関係なメソッドが呼ばれてしまうことがあり得ます。 (仮想呼び出しが狂います。本来参照すべきものと違う仮想テーブルをひいちゃうので当然。)

using System;
using System.Runtime.CompilerServices;
 
class A
{
    public void M() => Console.WriteLine("A non-virtual M");
    public virtual void X() => Console.WriteLine("A virtual X");
}
 
class B
{
    public void M() => Console.WriteLine("B non-virtual M");
 
    // 仮想テーブル的に、A.X と同じ場所にポインターが入る
    public virtual void Y() => Console.WriteLine("B virtual Y");
}
 
public class Program
{
    static void Main()
    {
        A a = new A();
        B b = Unsafe.As<A, B>(ref a);
 
        // non-virtual なメソッドは静的な型(B)に基づいて呼ばれる。
        // なので、これは普通に B.M が呼ばれる
        b.M(); // B non-virtual M
 
        // virtual なメソッドは動的な型(A)に基づいて呼ばれる。
        // 型の強制変換のせいで変な挙動に。
        // 仮想テーブル上、B.Y と同じ位置に A.X のポインターがあるので、
        // B.Y を呼んだつもりが A.X が呼ばれる。
        b.Y(); // A virtual X
    }
}

この例ではたまたまクラッシュせずに動作しますが (というか、ならないように気を使って書いています)、 無神経にやるとまずクラッシュします。

Unsafe クラス(保証のある利用例)

$
0
0

Unsafe クラス(保証外)」ではわざわざ動作保証のない相当に邪悪なコードを紹介しました。

とはいえ、別に Unsafeクラスを使った瞬間に動作保証がなくなるわけではありません。 単に、開発者の裁量に任されるというだけで、正しく使えば問題は起こしません。

例えば、Unsafe.Asメソッドは型チェックをせずに型を強制変換するメソッドですが、 最初から(Asメソッドよりも前に予めチェックして)型がわかっているなら何も問題ありません。

Union 型

例として、「A または B のどちらか」を表す型を作ってみましょう。 単なる例にそんなに凝っても仕方がないので、今回は「string または char[]」で作ります。

is 演算子実装

素直に実装すると以下のようになります。

using System;
 
public readonly struct StringOrCharArray
{
    private readonly object _value;

    public StringOrCharArray(string s) => _value = s;
    public StringOrCharArray(char[] array) => _value = array;

    public ReadOnlySpan<char> Span
        => _value is string s ? s.AsSpan() :
        _value is char[] a ? a.AsSpan() :
        default;
}

見てほしいのはSpanプロパティの部分です。 この中で使っているis演算は、 実際のところ、以下のような as + null チェックと等価です。

var s = _value as string;
if (s != null) ...

で、as 演算子は IL 的には isinst 命令になってます。

isinst 命令は要は実行時型情報を調べる命令です。 実行時型情報と言っても、動的コード生成をしない(単に型を調べるだけ)ならそこまで高コストではありません。 なので、動的コード生成みたいに「静的なコードに比べて2桁遅い」みたいな事態にはなりません。

型弁別用の enum 値

しかし、今日は「ちょっとのコスト」も避けようという話なので、 この isinst 命令を消すことを考えます。

object 型のフィールドに加えて、型弁別用の enum 値を別途持ってみることにします。 ただ、素直な実装をしてしまうと「コスト避け」の試みは失敗します。

using System;
 
public readonly struct StringOrCharArray
{
    public Discriminator Type { get; }
    private readonly object _value;
 
    public StringOrCharArray(string s) => (Type, _value) = (Discriminator.String, s);
    public StringOrCharArray(char[] array) => (Type, _value) = (Discriminator.CharArray, array);
 
    public ReadOnlySpan<char> Span
    {
        get
        {
            // せっかく Type を見て switch してるのに…
            switch (Type)
            {
                default: return default;
                // この2行のキャストが余計。
                case Discriminator.String: return ((string)_value).AsSpan();
                case Discriminator.CharArray: return ((char[])_value).AsSpan();
            }
        }
    }
}

enum 値を見て switch していますが、分岐の先で結局キャストしています。 キャストの方は caltclass 命令になるんですが、 この命令は内部的に isinst 命令と大差ないみたいで、実行時間もほとんど同じです。

これがこのクラスの失敗理由で、 「せっかく事前に enum 値で型を判定してるのに、castclass 命令で改めて型チェックをしてて、単に2重の負担がかかってるだけ」 という状態になっています。 結果的に、先ほどの is 演算子実装よりもちょっとだけよりも遅くなります。

Unsafe 実装

ということで、Unsafe。 先ほどの Span プロパティを以下のように書き換えます。

using System;
using System.Runtime.CompilerServices;
 
public readonly struct StringOrCharArray
{
    // 先ほどと同じところは割愛
 
    public ReadOnlySpan<char> Span
    {
        get
        {
            // せっかく Type を見て switch してるんだから
            switch (Type)
            {
                default: return default;
                // キャストを Unsafe.As で置き換えれば高速。
                case Discriminator.String: return Unsafe.As<object, string>(ref Unsafe.AsRef(_value)).AsSpan();
                case Discriminator.CharArray: return Unsafe.As<object, char[]>(ref Unsafe.AsRef(_value)).AsSpan();
            }
        }
    }
}

Unsafe.Asメソッドは型チェックをすっとばしてるので高速です。 名前通り unsafe ではありますが、今回の場合、事前に enum 値で型を調べているので問題は起こしません。

この実装であれば、当初目的である isinst 命令を避けることができます。 前述の通り桁違いな速度差が出るわけではないんですが、 元の is 演算子実装より数割程度速くなります。

ベンチマーク: DiscriminatedUnion

フィールドが増えてる

今回の例では、型チェックの負担は減りますが、 代わりにフィールドが1つ増えています。 構造体サイズも倍になってしまい、コピーのコストが発生してしまいます。 (Spanプロパティのアクセスよりも、変数のコピーの頻度が圧倒的に多い場合、むしろ遅くなる可能性があります。)

ということで使いどころには注意が必要です。 ただ、実装によってはこのコストは避けれます。 例えば、標準ライブラリ中のMemory<T>構造体(System名前空間)は以下のような構造になっています。

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

Memory構造体は、以下のような前提で、フィールドを増やさずに isinst 命令を避けたりしています。

  • _index(配列の開始インデックス)も_length(そこから何要素抜き出すか)も、負にはならない
  • 負にならないのなら、最上位ビットが 1 になることはあり得ない
  • その最上位ビットを、型弁別用に使う

仮想テーブルの中身をのぞき見

$
0
0

しばらくやってた Unsafe シリーズですが、今日は特に凶悪な奴です。

割かし最近なんですが、coreclr にこんなプルリクが出ていました。

これがまあ、なかなか凄いコードを含んでいます。仮想テーブルの中身を覗いて、「特定のビットが立っていたら配列」みたいなコードを書いています。

該当箇所

まず、仮想テーブルのポインターを取得

private static IntPtr GetObjectMethodTablePointer(object obj)
{
    return Unsafe.Add(ref Unsafe.As<byte, IntPtr>(ref JitHelpers.GetPinningHelper(obj).m_data), -1);
}
  • Managed なオブジェクトのアドレスを取得
  • その場所の1ワード手前に仮想テーブルへのポインターが入っているはず

で、それを使って「配列かどうか」を判定。

internal static unsafe bool ObjectHasComponentSize(object obj)
{
    return *(int*)GetObjectMethodTablePointer(obj) < 0;
}
  • 仮想テーブルの最初の4バイトはヘッダーになっている
  • ヘッダーの最上位ビットは「クラスが可変長かどうか」のフラグになっている
  • .NET のクラスで可変長なのは配列と文字列だけ

とまあ、今現在の実装としてはこれで確かに「配列、もしくは、文字列かどうか」を判定できます。

もちろん実装依存

当然ですが、今現在の実装としてできるからといって、将来もそうと言う保証はありません。 仕様として明言されているわけではなく、凄くきわどいコードです。

ギリギリ許されているのは、「coreclr 内の internal コードなので、もしランタイムに手を入れて仮想テーブルの実装が変わるようならその時に併せてここも直せばいい」という感じです。 coreclr の外で真似していいコードではないでしょう。

このプルリク内でも、「一旦はこれでマージしちゃっていいけど、Unsafeクラスを使った実装じゃなくて、ちゃんとランタイム側で判定用の intrinsic な API を提供すべき」という話の流れにはなっています。 さすがにいずれは消えると思われます。

段階コンパイル (Tiered Compilation)

$
0
0

今日は .NET Core 2.1 の頃に実装されて(有効にするには設定が必要)、 .NET Core 2.2 からは既定で有効になるランタイムの最適化の話。

(※ Preview の頃にいったん規定で有効になったものの、結局、リリースでは opt-in に戻した模様。後述する gen0 最適化問題とかがあるせいかも。)

.NET Core 2.1 の頃に以下のようなブログが出ていました。

これも今までそんなにここで取り上げていないので、 .NET Core 2.2 が出た今、「在庫処分」的に取り上げようかと思います。

JIT コンパイルの段階化

Java とか C# みたいな Just-In-Time コンパイル方式の言語では、常に以下のようなジレンマを持っています。

  • 中間コードからネイティブ コードに実行時にコンパイルするので、あんまりコンパイルに時間をかけるとかえって遅くなる
  • かといってコンパイル時の最適化がしょぼいと、出てくるネイティブ コードが遅い

長期的に動かすのであれば、最適化に時間をかけてでも良いネイティブ コードを生成する方が得になったりはします。 ただし、それは最終的にトータルでは速くなるという話で、起動にかかる時間は最適化を頑張ろうとするほど遅くなります。 また、そんなに頑張って最適化した結果が報われるような「よく通る経路」は、全体の一部分でしかありません。

そこで、以下のように、段階的な最適化を考えます。

  • 初めて呼ばれるメソッドは一律「最適化なし」でJITコンパイルする
  • 呼ばれた回数をカウントして、一定数を越えたら「最適化あり」でJITコンパイルしなおして差し替える

こういう手法を段階コンパイル(tiered compilaition)と言います。

coreclr の段階コンパイル

段階コンパイルは、例えば Java は Java 7 の時に導入したそうで、そんなに目新しい手法でもありません。 .NET Core でも、2016 にはプロトタイプ実装があったそうです。 ただ、.NET はもともと NGEN (事前ネイティブ化)や MulticoreJit (並列コンパイル)などで起動時間の短縮に取り組んできていました。 それに、Span<T>構造体による最適化とか、違う角度でのパフォーマンス改善などもいろいろあって、 なかなか段階コンパイルの優先度が上がらなかったみたいです。

それでも、.NET Core 2.1 には組み込まれて、.NET Core 2.2 ではついに既定動作として段階コンパイルが有効になりました。

技術的には以下のような実装になっているそうです。

  • 肝となるのは、実行時に、すでにJITコンパイル済みのコードを改めてJITコンパイルしなおして差し替える機能
    • それ単体で CodeVersionManager という名前がついていてドキュメントがある
    • 段階コンパイル以外にも、あまり使われていないコードを破棄することで省メモリ化したり、診断ログを採るためのコードを動的に差し込むのに使えます
  • 初期状態を Tier0、頻繁に呼ばれていて最適化したいコードを Tier1 という名前で分類
    • ただ、現状では、元々デバッグ用にある最適化のオン・オフをそのまま使って、Tier0 なら最適化なし、Tier1 なら最適化ありでコンパイルしているだけ
  • 差し替えは並列動作可能
    • メソッド A から別のメソッド B を呼んでいる最中に、A が Tier1 に昇格・再 JIT した場合、B からの戻り先アドレスに補正をかけるような処理がちゃんと働く

今後

上記のブログが書かれた頃から、 さらに改善も始まっているみたいです。

計画としては例えば、メソッドが呼ばれた回数のカウントは今の実装はあまり効率的なものじゃないことがわかっているそうなので、そこは直したいとのこと。 あと、Tier1 コンパイルはシングル スレッドで行われているそうなので、ここを並列化したいという話もあります。

Tier0 最適化

単純な「Tier0 なら最適化なし」という実装に問題があることも分かっていて、修正が必要です。

例えば、ジェネリックなメソッド M<T> で、以下のように、T が値型かどうかを判別するハックがあったりします。

void M<T>()
{
    if (default(T) == null) Console.WriteLine("T は参照型");
    else Console.WriteLine("T は値型");
}

この C# コードで、T が値型のとき、IL 上は、default(T) == null のところでボックス化が起きるコードになっています。 default(T) を一度 object 型にした上で null と比較するような IL が生成されます。 しかし、最適化ありでJITコンパイルすると、このボックス化はちゃんと消える(それどころかJIT時定数扱いで条件分岐自体が消える)ので、 default(T) == nullT が値型かどうかを判別する最速の手段だったりします。

その一方で、今、Tier0 JIT ではこの最適化が働かないそうです。 その結果、Tier0 の時にはボックス化が発生して、かえって遅くなることがわかっています。 (それで、せっかく入れた最適化を戻されかけたこともあったりします。) なので、Tier0 でも、手間のかからない範囲の最低限の最適化はかけるべきだろうという話になっています。

AggressiveOptimization

ちなみに、NET Core 3.0 では、段階コンパイルを制御するためのオプションが追加されます。

今でも、MethodImpl属性(System.Runtime.CompilerServices名前空間)を使ってメソッドの最適化を制御できます。 例えばこれに、MethodImplOptions.AggressiveInliningオプションを指定すると、通常よりもインライン化されやすくなります。 .NET Core 3.0 では、このMethodImplOptions列挙型にAggressiveOptimizationが追加されていて、 このオプションを指定すると最初から Tier1 扱いでJITコンパイルするようになるみたいです。

Hardware Intrinsics

$
0
0

今日は、おそらく .NET Core 3.0 で正式リリースとなるであろう最適化の話。 Hardware Intrinsics といって、特定 CPU の専用命令を利用するための機能の話になります。

元々は .NET Core 2.1 の頃に作業が始まっているんですが、2.1 リリースのタイミングには間に合いませんでした。 しかし、内部的な対応はすでに入っていて、daily ビルドなパッケージを参照すれば、今現在の .NET Core 2.1 でも利用可能です。 というか、ドキュメントはすでにあります

CPU 専用命令

いろいろなプログラミング言語で書かれたプログラムを比較したとき、 傾向として言うと、C 言語や C++ で書かれたものが最速です。

C# は、これら C や C++ と比較してどこがボトルネックでしょう。 印象としてはガベージ コレクションが遅そうに思われるかもしれませんが、案外、別のところにも原因があります。 (C# でもヒープ アロケーションを避けるコードは書けます。 それに、ヒープをどうしても避けれない場合だけに限定していうと、 ガベージ コレクションによるヒープ管理はものすごく高速です。)

高速化の行き着く先は、特定の CPU の専用命令をどれだけうまく使えるかになったりします。

例えば、32ビット整数の中から、特定のビットだけを抜き出すことを考えてみます。 普通に C# で書くと以下のような感じ。

struct SingleView
{
    public uint Value;
 
    /// <summary>
    /// Value のうち、23~31ビット目の値を抜き出す。
    /// </summary>
    public uint Exponent
    {
        get => (Value & 0x7F800000) >> 23;
        set => Value = (uint)((Value & ~0x7F800000) | ((value << 23) & 0x7F800000));
    }
}

AND とか OR とかシフト演算がいくつか必要です。

ところが、これ、たいていの CPU で1命令で実行できる命令があったりします。 x86 CPU だと BEXTR 命令ARM だと UBFX 命令というのがそれです。

理想をいうと、先ほどの AND とシフトな C# コードから、ちゃんと最適化でこれらの専用命令に翻訳されてほしいんですが、そんなにうまく行かないことが多いです。

そこで行き着く先は、「インライン アセンブラを書かせろ」となったりします。 実際、速いといわれている C/C++ コードは、CPU 専用命令を使ってガチガチに最適化していたりします。

実のところ、C/C++ と比べたときに C# (や、Java, Go, Swift 辺りの「そこそこ速い」言語)が遅い理由の結構な割合が、こういう専用命令利用に関する部分だったりします。

Intrinsics

ということで、C# 内にもインライン アセンブラを書きたいという要望はあるんですが。

しかし、「C# の中で別の言語を保守する」というのは、コンパイラーを作る側にとっても使う側にとってもかなりのハードル・足かせになります。 そこで最近よく取られる手法が、「intrinsic 関数の提供」です。

JIT Intrinsics」でも書きましたが、 intrinsic というのは固有の、内在的な、内因的な、本質的なという意味の単語で、 概ね、「内部的に特別扱いして最適化しているもの」という意味で使われています。

そして、intrinsic 関数(あるいは単に intrinsics)というのは、

  • 普通に関数(C# だと静的メソッド)としてライブラリ提供する
  • その関数を見たら特定の CPU 命令に置き換える

というようなもののことです。

例えば C++ でも、有名なものでは、Intel Intrinsics というものがあります。 名前通り Intel CPU 向けのものですが、Visual C++, GCC, clang など、Intel 製以外の C/C++ コンパイラーでも大体利用できます。 mmintrin.h とかで検索してもらうとサンプル コードがすぐに見つかると思います。 以下のような感じで、普通の C++ コードを書くと、それが特定の Intel CPU 命令に置き換わります。

#include <immintrin.h>
// 中略
__m128 c = _mm_mul_ps(a, b);

いわゆる SIMD 演算というやつで、 複数の積和演算を1命令で実行するので、うまく使えば数値計算が4~8倍速くなったりします。

ただし、注意点もあります。 特定 CPU の専用命令を使うための手法なので当然なんですが、 特定の CPU に依存します。 上記の Intel Intrinsics であれば当然 Intel CPU でしか動きません。 同じ Intel 系の CPU でも、世代を追うごとに命令がどんどん追加されているわけで、 古い CPU では対応していない命令が大量にあります。

その結果どうなるかというと、ガチガチに最適化するなら #ifdef だらけになります。 例え古い CPU のサポートを切ったとしても、Intel 系と ARM 系の2種類は保守が必要になったりします。

.NET でも Hardware Intrinsics

ということで、 .NET Core 2.1 くらいの頃から、.NET にも Hardware Intrinsics を入れたいという話が出ます。

実際、実は内部的にはもうその対応が入っていて、以下のパッケージを参照すれば .NET Core 2.1 で Hardware Intrinsics を使えます。

現状は、nuget.org からは落とせません。 MyGet (daily ビルド用の CI サーバー)からのみ取得できます。 また、正式リリースされた暁には Experimental が外れて、System.Runtime.Intrinsics パッケージになると思われます(もしかしたら、X86 と Arm で別パッケージになるかも)。

例えば、最初に出した「特定のビットだけを抜き出す」コードは以下のように書けます。

using System.Runtime.Intrinsics.X86;
 
struct SingleView
{
    public uint Value;
 
    public uint Exponent
    {
        get
        {
            if (Bmi1.IsSupported) return Bmi1.BitFieldExtract(Value, 23, 8);
            else return (Value & 0x7F800000) >> 23;
        }
        // set 割愛
    }
}

他にも、先ほど挙げた Intel Intrinsics 相当のメソッドもあります。

ちなみに、ここで出てくる IsSupported プロパティは JIT 時定数になります。 このコードは、JIT が掛かるタイミングで、 この CPU 命令セットを持っている環境なら if 側、 持っていない環境なら else 側だけが残ります。

なのでパフォーマンス的にはかなりいいものに仕上がるんですが、 見ての通り、同じ意味のコードを2回書く必要があります。 もちろん、ARM 系 CPU にも対応したければ3回に。

要するに、C/C++ でよくある「ガチガチに最適化するなら #ifdef だらけ」が、C# でも書けるようになります… 大変さと引き換えに、数倍高速なコードが手に入ります。

null かどうかを判定

$
0
0

C# では、無効な値として null をよく使うので、 「値が有効かどうか」「無効だったら何もしない」みたいな判定のために null チェックを結構書きます。 で、C# の null は「全ビットが0」で表されるので、通常、null チェックは非常に軽い処理(単なる 0 との比較)になります。

通常は。

問題は、== 演算子のオーバーロードがある場合。 その場合、x == null は、演算子(を表すメソッド)の呼び出しになります。 もしその == 演算子がインライン展開できないものだった場合、「単なる0比較」と比べると大幅に遅いコードになります。

なので元々、== 演算子のオーバーロードがある場合には、x == null ではなくて、 (object)x == null や、ReferenceEquals(x, null) を使えという話もあったりします。 ですが、これらはちょっと長ったらしくてつらい。

一方、C# 7.0 でパターン マッチングが入って、定数パターンを使った x is null であれば、 たとえ演算子オーバーロードがあったとしても、必ず「単なる0比較」で null チェックが掛かります。

そこで最近、== 演算子をオーバーロードしちゃっているクラスに対して x == null しているところを、x is null に置き換える最適化を掛けることが多かったりします。

そして、coreclr内で大々的にこの最適化をやろうとした痕跡が。

日付的に、ホリデーシーズンの余暇でやってみたんですかね。

ただ、あまりにも数が多過ぎてちまちま置き換える作業は結局断念したみたいです。 400ファイル、4千行とかですからね、差分。

で、別の解決策として挙がっているのがこちら。

== 演算子オーバーロードの実装の方に手を入れて、

  • インライン展開が掛かるサイズに収める
  • x == null (右辺が null )の時には x is null だけが残る構造にする

となるようにしたという修正。 == 演算子の実装は、具体的には概ね以下のような構造です。

public static bool operator ==(MyClass left, MyClass right)
    => (right is null) ? (left is null) : right.Equals(left);

最後が right is null を先に判定して、後ろが right.Equals(left) なのがポイント。 null == x だと最適化は掛からない構造。 (C# ではあまり見ませんけども、「リテラルは左辺にあるべき」派の人はご注意を。)

あと、「null じゃないのに null を自称する邪悪なクラス」なんかにはこの最適化は使えないので、そういう残念なやつはあきらめましょう…

Visual Studio 2019 Preview 2

$
0
0

なんか、Visual Studio 2019 Preview 2が出てますね。

リリースノート上は、.NET 関連はまた「リファクタリング機能が増えたよ」みたいな感じのアナウンス。

あとは、自分が手元で確認してみた感じ、Preview 1の頃から3つほど C# 8.0 の実装が増えてました。

  • 再帰パターン
  • using の改善
  • 静的ローカル関数

動作確認で使ったコード: Demo/2019/Csharp80/Preview2

再帰パターン

これは Preview 1 で入ると思ってたのに入らなかったというくらいなので、 前に、sharplab.ioで動作確認しながら書いた以下の2つのブログほぼそのまま。

一応、0引数・1引数での Deconstruct ができるようになったりしているみたいです。

using System;
 
struct X
{
    public void Deconstruct() { }
    public void Deconstruct(out int x) => x = 0;
    public void Deconstruct(out int x, out int y) => (x, y) = (0, 0);
}
 
class Program
{
    static void Main()
    {
        var x = new X();
        Console.WriteLine(x is ());      // 0引数
        Console.WriteLine(x is var (_)); // 1引数のだけは、() 式とかキャストとかとの弁別のために var 必須
        Console.WriteLine(x is (_, _));  // 2引数
    }
}

using の改善

2つほど。

  • ref struct に限り、IDisposable インターフェイスを実装していなくても、パターン ベースでDisposeメソッドを呼んでくれるようになった
  • using var で、ローカル変数のスコープに紐づいたリソースの破棄(Dispose メソッド呼び出し)ができるようになった

はい、残念なお知らせ。パターン ベースでのDispose呼び出しがref struct限定になりました。 そうしないと破壊的変更を起こす可能性があってやむなく限定したそうです。

using System;
 
// インターフェイスなし、ref なし
struct A { public void Dispose() { } }
 
// インターフェイスあり
struct B : IDisposable { public void Dispose() { } }
 
// ref あり
ref struct C { public void Dispose() { } }
 
class Program
{
    static void Main()
    {
        using var a = new A(); // ダメ
        using var b = new B(); // 元々 OK
        using var c = new C(); // C# 8.0 で OK に
    }
}

静的ローカル関数

ローカル関数に static 修飾を付けることで、ローカル変数のキャプチャをしないということを明示できるようになります。

// ローカル関数に static を付けると、ローカル変数をキャプチャできなくなる。
static int a(int x) => 2 * x;
 
// 以下のコードは2行目の n のところでエラーに。
int n = 0;
static int b(int x) => n * x;

Preview 1 からのその他の修正

Async streamsはいまだに動きません… これは、たぶん、 .NET Core 3.0 の方の Preview 2が来れば解消される気がします。

あと、null許容参照型は、以下のような変更が掛かってそう

  • プロジェクト全体に対して null 解析をオンにするためのオプションが以下のように変更されてそう
    • 旧: <NullableReferenceTypes>true</NullableReferenceTypes>
    • 新: <NullableContextOptions>Enable</NullableContextOptions>
  • 解析が走るタイミングが変わっていそう(たぶん)
    • 旧: 常時
    • 新: ファイルを開いているとき

bool 型の false, true, それ以外

$
0
0

これまで(C# 7.3 まで)、C# の switch ステートメントで bool 型を使う場合、以下のように、default 句が必須になることが多々ありました。

static int X(bool b)
{
    switch (b)
    {
        case false: return 0;
        case true: return 1;
        default: return -1;
    }
}

bool 型には falsetrue しかないはずなのにこれはおかしいと言われ続けていたんですが、C# 8.0 では default 句が要らなくなるというか、default 句を絶対に通らなくなるよう、コード生成の仕方を変更するみたいです。

今日はこの辺りの、要するに「false でも true でもない bool 値」の話。

サンプルコード: BoolExhaustiveness

bool とは

ドキュメント上

まず、ドキュメント上、bool がどうなっているかというと…

大体は2つの値だけを取れる型として説明されています。

実装上: Boolean 構造体

その Boolean 構造体(System 名前空間)の内部実装がどうなっているかというと、

  • 1バイトの構造体
  • true の内部表現は 1
  • false の内部表現は 0

です。 1バイトだけども0と1しか必要としないので、残り254個の値は基本的には使われません。

0 でも 1 でもない bool を作る

普通にリテラルの true, false や、== などの条件式から bool 値を得る限り、本当に0と1以外の値は発生しません。

ただ、C# は unsafe な手段を使って任意に値を書き換えれちゃうので、無理やりやると 0 でも 1 でもない bool 値を作れます。

具体的にはいくつか書き方があるんですが、1つ目は素直にポインターを使うもの。

unsafe bool toBool(byte b) => *((bool*)&b);
Console.WriteLine(toBool(2));

もう1つは、Unsafe クラスを使う書き方。 これもまあ、書き方が違うだけでポインターと大差ないです。

bool toBool(byte b) => Unsafe.As<byte, bool>(ref b);
Console.WriteLine(toBool(2));

最後に、StructLayout を使う(C 言語の union 風な使い方する)方法。 LayoutKind.Explicit は、ポインター並みに変なことができちゃう機能なので、 そもそも unsafe コードなしで使えること自体が疑問視されていたりもします。 要するに、実質 unsafe。

static void Main()
{
    bool toBool(byte b)
    {
        Union u = default;
        u.Byte = b;
        return u.Boolean;
    }

    Console.WriteLine(toBool(2));
}

[StructLayout(LayoutKind.Explicit)]
private struct Union
{
    [FieldOffset(0)]
    public byte Byte;
    [FieldOffset(0)]
    public bool Boolean;
}

0 でも 1 でもない bool を使うとどうなるか

x86 などの CPU では、条件分岐命令が以下のような方法で実現されています。

  • 直前の命令の結果が 0 になったら立つフラグが CPU 内に存在する
  • そのフラグを見て分岐する

要するに、「0 かどうか」しか見ません。 この意味では、「true とは 0 以外の全ての値を指す」と言えます。

C# の if ステートメント

.NET の中間言語もそういう挙動をします。 brtrue 命令ってのを持ってるんですが、 こいつは「value が 0 でなければ分岐」という挙動。

C# の if ステートメントはこの命令(もしくはその逆の brfalse)に変換されるので、 「0 以外の値」は全て true 扱いになります。 実際、前述の方法で作った「中身が2のbool値」を if に渡すと true 側に分岐します。

using System;

class Pointer
{
    static void Main()
    {
        unsafe bool toBool(byte b) => *((bool*)&b);

        Branch(false);     // if (false)
        Branch(true);      // if (true)
        Branch(toBool(2)); // if (true)
    }

    static void Branch(bool b)
    {
        if (b) Console.WriteLine("if (true)");
        else Console.WriteLine("if (false)");
    }
}
if (false)
if (true)
if (true)

C# 7.3 までの switch ステートメント

問題はここからなんですが…

if ステートメントとは違って、(C# 7.3 までの) switch ステートメントは中身の値を見ます。 すなわち、普通の true と、「中身が2のbool値」は別の値という扱い。

これが、冒頭のコードで default 句が必須になる理由です。 実際、case true を通らないようなコードを書けます。

static void Main()
{
    // 0 → false
    // 1 → true
    // それ以外 → if (b) は通るんだけど、switch (b) { case true: } は通らない(C# 7.3 までは)変な値になる。
    for (byte i = 0; i < 3; i++)
    {
        Console.WriteLine($"value = {i}");
        Branch(Pointer(i));
        Branch(UnsafeAs(i));
        Branch(UnionStruct(i));
    }
}

/// <summary>
/// false (0) の時は何も表示されない。
/// true (1) の時は if(b) switch(b) の両方が表示される。
/// 「それ以外の値」を作って渡すと、if(b) だけが表示される。
/// </summary>
static void Branch(bool b)
{
    if (b) Console.WriteLine("    if(b)");
    switch (b) { case true: Console.WriteLine("    switch(b)"); break; }
}

型 switch

ちなみにこの「中身の値を見て分岐」挙動は、case が全部定数の場合(= 古き良き昔からある switch) の場合だけの挙動です。

C# 7.0 から入った、パターン マッチングを使った switch(いやゆる「型 switch」)の場合には brtrue 命令が使われるようになって、if ステートメントと同じ挙動になります

using System;

class TypeSwitch
{
    static void Main()
    {
        Branch(0);
        Branch(1);
        Branch(2);
    }

    static unsafe void Branch(byte x)
    {
        var b = *((bool*)&x);

        Console.WriteLine($"value = {x}");
        Console.Write("    traditional switch: ");
        switch (b)
        {
            case false:
                Console.WriteLine("false");
                break;
            case true:
                Console.WriteLine("true");
                break;
            default:
                // 0でも1でもないbool値の時にここに来る
                Console.WriteLine("other");
                break;
        }

        Console.Write("    type switch: ");
        switch (b)
        {
            case false when true:
                Console.WriteLine("false");
                break;
            case true:
                Console.WriteLine("true");
                break;
            default:
                // 絶対ここは通らない
                Console.WriteLine("other");
                break;
        }
    }
}
value = 0
    traditional switch: false
    type switch: false
value = 1
    traditional switch: true
    type switch: true
value = 2
    traditional switch: other
    type switch: true

マーシャリング

ちなみに、P/Invokeを使う際には、マーシャリング時に「0でも1でもないbool値」をtrue(内部的に1のbool値)に置き換える処理が掛かるみたいです。

例えば、以下のような Rust コードを lib.dll 中で定義しておいて、

#[no_mangle]
pub extern fn id(x: i8) -> i8 { x }

これを C# 側から以下のように呼び出します。

using System;
using System.Runtime.InteropServices;

class Program
{
    static void Main(string[] args)
    {
        // 素通し。当然、2。
        byte a = Id(2);
        Console.WriteLine(a);

        // 素通しじゃなくて、bool で値を受け取り。true。
        bool b = ToBool(2);
        Console.WriteLine(b);

        unsafe
        {
            // 内部表現を見てみると、1 になってる。
            byte b1 = *(byte*)&b;
            Console.WriteLine(b1);
        }
    }

    /// <summary>
    /// rust 側の id 関数は i8 を素通しするだけ。
    /// それを DllImport で呼んでるので、このメソッドも素通し。
    /// </summary>
    [DllImport("lib.dll", EntryPoint = "id")]
    private static extern byte Id(byte x);

    /// <summary>
    /// マーシャリングで、byte な戻り値を bool で受け取ることができる。
    /// ただ、この場合、素通しではなくて、ちゃんと 戻り値 != 0 で bool に変換されているみたい。
    /// </summary>
    [DllImport("lib.dll", EntryPoint = "id")]
    private static extern bool ToBool(byte x);
}

id関数の戻り値は i8 (C# でいう sbyte)ですが、マーシャリング時に bool への変換をしてくれます。 変換の仕方は、!= 0 になっているみたいで、「0 でない値」だったら普通の true (内部的に1のbool値)が返ってきます。

C# 8.0 での switch ステートメントの変更

まあ、要するに、switch ステートメントだけがきもいです。

たびたび「case falsecase true があれば default 要らないだろ」と言われ続け、 そのたびに「内部的に false でも true でもない値があり得るから」という回答が返って来続けていたんですが。

この度、「ドキュメント上も 『truefalse の2値』と明記されているんだから、それ以外の値を想定して非効率なコードを生成するのはおかしいだろ」という突っ込みがあって、「それは確かに」的な空気になったみたいです。

また、C# 8.0 では switchも入るので、網羅性のチェック(「truefalse で全パターン網羅している」という判定)をしたい需要が高まったので、ついに折れて、bool に対する switch の挙動を変えることにしたみたいです。

using System;

class Program
{
    static void Main()
    {
        Console.WriteLine(X(false)); // -1
        Console.WriteLine(X(true)); // 1

        unsafe
        {
            byte x = 2;
            bool y = *(bool*)&x;
            Console.WriteLine(X(y)); // C# 7.0 までは 0 だった。C# 8.0 で 1 になるように。
        }
    }

    static int X(bool b)
    {
        switch (b)
        {
            case false: return -1;
            case true: return 1;
            default: return 0;     // C# 7.0 までは何も言われなかった。C# 8.0 で「到達できないコード」警告出るように。
        }
    }
}

内部的には if 相当のコードへの置き換えです。

ちなみに、Visual Studio 2019 Preview 2だと、「LangVersion を 7.3 以下にしてても新しい方の挙動になってしまう」というバグがあったりします。 バグ認定はされていて、正式版までには「C# 8.0 以上にした場合だけ新しい挙動になる」に変更されるはずです。

ピックアップRoslyn 2/10 変数のshadowing、関数ポインター、実行時nullチェック、Index/Rangeの仕様変更

$
0
0

しばらくちょっと忙しくて紹介できてなかった話をいくつかまとめて。

匿名関数の変数 shadowing

こないだの VS 2019 Preview 2 から、 1段外側の変数と同じ名前で、 ローカル関数内の引数・変数を宣言できるようになったみたいです。

外側の x を隠すので shadowing と呼ばれます。

static int M()
{
    int x = 1;
 
    // C# 8.0 で、1段外側の変数の x と同名の引数が使えるように
    int m(int x) => x * x;
 
    return m(x);
}

きっかけとしては、静的ローカル関数が入ったからみたいです。 要するに、

  • 外の変数をキャプチャしてるのかどうかぱっと見で分かりにくくなるのは怖いから許してなかった
  • 静的ローカル関数ならキャプチャすることを許さないのでその問題は解消する
  • とはいえ、静的ローカル関数でだけ shadowing を認めるのも気持ち悪い

という流れ。 この決定自体は結構前(去年の9月10日)にやってたみたいです。

ちなみに、現時点ではローカル関数のみ。ラムダ式では shadowing されません。 けども、1月16日の Designs Meetingで、ラムダ式でも認めよう、クエリ式でも検討してみようという話が出ていたり。

関数ポインター

C# でデリゲートではなく生で関数ポインターを使いたいという話があったわけですが。 最近ちょっと検討が進んだみたいで、ちょっと文法が具体化してきました

func* int(string); みたいな書き方になるみたいで、 unsafe コード必須。

実行時の null チェックの挿入

C# 8.0 で導入される予定のnull 許容参照型は、 基本的にコンパイル時のチェックであって、 (unsafe とかを使って)コンパイル時に拾えないような null が来ても、実行時には何もしません。

一方で、実行時の null チェックを挿入するような簡易文法も足したいという話が出ているようです。

これまで、C# では結構以下のようなコードを書いたと思います。

static int M(string x)
{
    if (x == null)
    {
        throw new ArgumentNullException(nameof(x));
    }
 
    return x.Length;
}

これを、以下のように書くだけで同様の実行時 null チェックを挿入するようにしたいというものです。

static int M(string x!)
{
    return x.Length;
}

ただ、null-forgiving (コンパイル時チェックも実行時チェックもなくす)の x! と、この実行時 null チェックの x! で同じ書き方なのが結構気持ち悪く… その辺りはまだちょっと悩んでいるみたいです。

Index 型、Range 型の仕様変更

Range 構文の内部実装として使われるIndex構造体とRange構造体ですが、 coreclr 側の API レビューの結果、実装がだいぶ変更されそうです

Index構造体

  • Start, End 静的プロパティ追加
  • int GetOffset(int length) メソッド追加

Range 構造体

  • FromStartStartAtに、ToEndEndAtにリネーム
  • Create(Index start, Index end) 静的メソッドは削除(普通にコンストラクターを使う)
  • OffsetAndLength GetOffsetAndLength(int length) を追加
    • OffsetAndLength(int offset, int length) な構造体

ここまでは確定。 で、これらを使う側(配列とか Span<T> とか)側は、 Range 構造体を受け付けるオーバーロードを足すのではダメなんじゃないかという話も。理由は大体、

  • int版とIndex版、(int offset, int length)版とRange版の2重保守が大変になる
  • JIT時最適化を掛けにくくなる

みたいな感じ。 これに対して、以下のように int 引数なメソッドに属性を付けて、

interface ISomeCollection<T>
{
    [IndexMethod]
    T this[int index] { get; set; }
 
    [RangeMethod]
    ISomeCollection<T> Slice(int start, int length);

    int Length { get; }
}

以下のようなコードを、

ISomeCollection<T> x;
var y = x[^1];
var z = x.Slice(1..^1);

コンパイラーが以下のように置き換える実装を提案しています。 (ただし、LengthCount をどうやって取るかが課題。)

var y = x[^1.GetOffset(x.Length)];
var (offset, length) = 1..^1.GetOffsetLength(x.Length);
var z = x.Slice(offset, length);

LangVersion default の変更

今、LangVersion のデフォルト値(default)は、「最新のメジャー バージョン」になっています。 要するに、現在(最新は 7.3)のところ、default を指定すると 7.0 が選択されます。

これを、今更なんですが、以下のように変える pull request が通っていたり。

  • latest はこれまで通り「最新 (マイナー バージョン含む)」
  • default は latest と同じ意味、つまり、最新
  • 「最新のメジャー バージョン」を表す latestMajor を追加
  • 「プレビュー版」を表す preview を追加

Visual Studio 2019 Preview 3

$
0
0

Visual Studio 2019 Preview 3 出てますね。

C# がらみは特にアナウンスもないんですが、Roslyn の 16.0.P3 マイルストーンを見るに、大体は IDE がらみと null 許容参照型がらみを中心としたバグ修正っぽいです。

Preview 2 からあんまり期間が開いていませんし、元からバグ修正のみな予定だったのかも。

switch 式のバグ

その割に、switch 式を書くと IntelliSense が狂って最終的に Visual Studio がフリーズするバグが増えちゃってるみたいですが… (コンパイルはできる。あくまで IDE だけの問題。)

ちょうど最近、「C# によるプログラミング入門」以下に C# 8.0 の話を書き足し始めていて、今週パターンの話を書き終えて、次はswitch式かなぁとか思っていたところなんで、このフリーズはタイミングが悪すぎる…

その他細かい修正

まあ、バグ修正の範囲内ですが、C# の言語機能にも多少変更がありました。

サンプル: Csharp80/Preview3

Visual Studio 2019 RC と Preview 4

$
0
0

Visual Studio 2019 がリリース候補版(RC) になりました。

リリース チャネルとプレビュー チャネル

同時に、Visual Studio 2019 Preview 4 も出ています。 これまで Preview 版を使っていた人は単にアップデートを掛ければ Preview 4 になります。 一方で、RC の方は、その後正式リリース版にそのままアップデートできます。

出た時期的に、RC と Preview 4 は内容的には大差ないと思います。 (といいつつ、なんか RC の方だと C# 8.0 がちゃんと使えなかったり、.NET Core 3.0 を参照できなかったりしてるんですが… 自分の手元だけなのか、.NET Core 3.0 の更新が来れば治るのか、本格的にバグなのかは不明…)

この2つは、単に配信チャネルの差。 要するに、

  • RC は、正式リリースが出た際にはそのまま正式リリース版にアップデートできる
    • その後も、正式リリースなものだけがアップデートとして流れてくる
    • これが「リリース チャネル」
  • Preview 4 は、Visual Studio 2019 (16.0)正式リリース後も、16.1 Preview 1 みたいなやつが流れてくる
    • これが「プレビュー チャネル」

という状態。 もちろん、Visual Studio 2017 との共存もできるので、インストーラーが以下のような状態になります。

VS Installer 2019 RC

C# がらみ

基本的には Preview 2の頃から機能としては変わっていません。 その辺りは Preview 3の時と同じ。

RC になったわけですから、Visual Studio 2019のリリース版でも、使える機能は今あるものまでとなります。

C# 8.0 Preview

これまでにも書いていますが、Visual Studio 2019 リリース時点では、C# 8.0 の扱いは「プレビュー版」です。 言語バージョンに 8.0 か、後述する preview を明示的に指定しないと使えません。

まだ実装すらされていない機能もたくさんありますし、 .NET Core 3.0 依存な機能はこれから変化する可能性もまだかなり高いです。

(1) 例えば、割かし需要がありそうな機能でまだ実装がないものは

(2) 実装はあるものの、 .NET Core 3.0 依存なので今使うのはちょっと怖いかもしれないものは

(3) 実装があって .NET Core 3.0 依存もないものは

という感じ。

私見ですが、(3) のやつはプレビュー版であってもそんなにリスクなく使えると思います。

null 許容参照型はまだまだいろいろ作業が残っている節があるので、 「後からどんどん警告が増える」というのは覚悟して使う必要があるかもしれません。 その他は、これからそこまでいきなり大きく仕様変更があるとは思えないので、割かし使っても平気かと思います。

.NET Core 3.0

まあ、とりあえず、C# 8.0 を試したいなら .NET Core 3.0 の更新も待った方がいいかも…

今また、Visual Studio だけリリースして、それに対応する .NET Core 3.0 がまだ出ていないので、Ranges がコンパイル エラーを起こしたりします。 (僕が気づいた範囲ではその Ranges の問題だけですが。)

バグはだいぶ取れてる

前に書いた、

とかのバグは治っていました。

まあ、RC ということは、特に問題が出なければそのままリリース版になるものですからね。 Visual Studio 2019 リリース版の時点でもまだ「C# 8.0 はプレビュー版」という扱いであっても、さすがにフリーズとかクラッシュとかの問題は残さないようです。

言語バージョン

言語バージョンの仕様がちょっと変わっているので注意が必要です。

補足: ↓これのこと。csproj を直接編集するなら LangVersion タグになっているやつです。

言語バージョン

これまでの挙動は

  • 無指定(あるいは default を指定): 「最新のメジャー バージョン」になる
    • 要するに、ずっと C# 7.0 だった
  • latest 指定: 「最新のバージョン(マイナー バージョン含む)」になる

今後の挙動は

  • 無指定(あるいは default を指定): 「最新のバージョン(マイナー バージョン含む)」になる
  • latest 指定: 「最新のバージョン(マイナー バージョン含む)」になる
    • これは今まで通り
    • プレビューなものにはならない
  • latestMajor 指定: 「最新のメジャー バージョン」になる
    • 今までの default の挙動が欲しければこれを使う
  • preview 指定: 「プレビューを含めて最新」になる
    • C# 8.0 を今試したければ、8.0 の直接指定に加えて、これを指定してもいい

となります。

これまで無指定でやってきた人は、いきなり C# 7.3 になるのでご注意を。 破壊的変更はほぼないはずなので、それで問題は起こさないと思いますが、一応。

.NET Core 3.0 Preview 3

$
0
0

こないだ出た Visual Studio 2019 RC1 / Preview 4に対応したバージョンの .NET Core 3 Preview が出たみたいですね。

.NET Core 3.0、最近なんかずっと、Visual Studio より結構遅れてのリリースですよね… おかげで、C# 8.0 のコードがエラーになってたり(今回は、Range構造体の仕様変更が原因)。

Preview 3 での更新点

さらっと内容まとめると、

  • .NET Core 3.0 の正式リリースは今年後半っぽい。詳しくは今年の build
  • 細かいインストールは上書きになる。パッチ バージョンの百のけたが同じものを同一視
    • 3.0.100 があるときに 3.0.101 を入れると 100 は消える
    • 3.0.200 を入れると 3.0.100 とか 3.0.101 とかは残る
  • Docker がらみ、おかしかったの/不便だったの修正
  • Visual Studio 2019 RC1 / Preview 4 の C# 8.0 の Range がちゃんと動くように
  • .NET Standard 2.1 を使えるように
  • F# 4.6 に。dotnet fsi コマンドで F# Intaractive 起動
  • AssemblyDependencyResolver とか NativeLibrary とか、native 相互運用してる人にはうれしそうなもの追加
  • WPF/WinForms とか Entity Framework とか更新
  • 他は、Preview 1のときPreview 2のときの告知を見て

とか。

C# 8.0

ちなみに、TargetFrameworknetcoreapp3.0netstandard2.1 にすると、 デフォルトの状態で C# 8.0 になるみたいです。 TargetFramework だけで、ビルド ツールも .NET Core 3.0 のものに切り替わる模様。 (.NET Core 3.0 が正式リリースされた暁には C# 8.0 も正式リリースになる予定なので、 .NET Core SDK 3 にとっては C# 8.0 がデフォルトになります。)

ということで、ターゲットが .NET Core 3.0 か .NET Standard 2.1 のときは、特に <LangVersion>preview</LangVersion> とか書かなくてもよくなります。 (netcoreapp2.2 とかがターゲットだとやっぱりこのタグが必要。あくまで新しいターゲットの時だけの切り替わり。)

RC1 の方だと「プレビューの .NET Core SDK を使用」オプションをオンにしないといけないのだけはご注意を。

.NET Standard 2.1

やっと、 .NET Standard も 2.1 になったわけですが。詳細は以下のページで見れます。

大体は、「.NET Core の方に追いついた」って感じの API 追加です。

.NET Core 3.0 がらみも結構ちゃんと入っています。 C# 8.0 に必要な Range/Index や、IAsyncEnumerable/IAsyncDisposable も含む。 でも、.NET Standard 的にはバージョン 2.1。

ただ、Hardware Intrinsicsは入れないとのこと。 なんせ、Intrinsics はランタイムのサポートがあって初めて成り立つものなので、「どのランタイムで動かすかわからない .NET Standard にとっては意味がないから」とのこと。

ちなみに、追加が多いのは以下の辺り。

  • Span<T> がらみ
    • System とか System.IO とかの更新点は半分くらいは Span がらみ
  • System.MathF とか、浮動小数点数がらみの追加結構あり
  • これまで NuGet 提供だったものがいくつか標準入り
  • 動的コード生成(System.Reflection.Emit) の辺りは丸々新規追加
    • もちろん、動的コード生成できないプラットフォームはあるわけで、その辺りはRuntimeFeature クラスIsDynamicCodeCompiled, IsDynamicCodeSupported で判定
  • System.Drawing にやたらと差分が多いのは、たぶん SystemColors をサポートしたせい
    • enum 値1個1個が「追加 API」にカウントされてそう

ピックアップ Roslyn 3/12

$
0
0

Visual Studio 2019 (16.0)が RC までいってちょっと落ち着いたのか、csharplang にちょっと動きが。 (もう、次に C# 8.0 絡みの新機能実装がマージされるとしたら 16.1 になるので、C# チーム的には今ちょっと落ち着ける時期のはず。) Designe Notes 3件追加。

いくつかの話題はすでに個別の issue が立っています。

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

半分くらいはインターフェイスのデフォルト実装がらみ。 base 呼び出しをどうしようかという話と、アクセシビリティをどうしようかという話。

base

base 呼び出しって言うのは以下のようなやつのこと。

using System;
 
class A
{
    protected virtual void M() => Console.WriteLine("A");
}
 
class B : A
{
    // この、B の M は後から足したり消したり
    //protected override void M() => Console.WriteLine("B");
}
 
// A, B とは別アセンブリにあるとして
class C : B
{
    // C から基底クラスの M() を呼ぶ
    protected override void M() => base.M();
}

この書き方で、C.M から基底クラスの M を呼び出せるわけですが、

  • 基底クラスを1つずつたどっていって、最初に見つかったやつが呼ばれる
    • コンパイル時に B.M があったけど、実行時に読み込んだものからは消えていたら A.M が呼ばれる
    • コンパイル時に B.M はなかったけど、実行時に読み込んだものには足されていたら B.M が呼ばれる

みたいな挙動です。 これは C# の仕様というか、 .NET ランタイム(IL 命令)のレベルでそういう仕様だそうです。

で、インターフェイスの場合はダイアモンド継承があり得るので、この仕様便りだと、規定をたどっていく経路が複数あって困る。 なので、base(B).M() みたいに、具体的にどの型の M を呼びたいのかを明示できる構文が導入される予定です。これについて、

  • この書き方、(C# 8.0 でこれから実装される)インターフェイスのデフォルト実装だけじゃなく、クラスに対しても認める
  • フィールドだろうがなんだろうが、この base(BaseClass).Member みたいな書き方が使える
    • ただし、(overrideさえなければ)元々 this.Member でアクセスできるものに限る
  • base(B).M() と書いたら B だけを見る。実行時に消えて時たら実行時エラーを起こす
    • B になければ B から上をたどって探す」みたいなのは現状の .NET ランタイムでは不可能
    • 将来的に、 .NET ランタイム自体に改修を入れる余地はある

とのこと。

アクセシビリティ

アクセシビリティpublic とか private とかのこと。

C# 7.3 までのインターフェイスは無条件に全部のメンバーが public (明示的に指定はできない)でしたが、 デフォルト実装とともに、アクセシビリティの指定ができるようになります。 これまでも、publicprotectedprivate は提供するつもりでしたが、 残りの internalprotected internalprivate protected も提供することに決めたそうです。

あと、インターフェイスの明示的実装、↓みたいなやつもあるわけですが。

interface I
{
    void M();
}
 
class A : I
{
    void I.M() { }
}

デフォルト実装が入ることで、「インターフェイスが基底のメンバーを明示的実装」という状況が発生します。 この場合、その明示的実装は protected 扱いにするそうです。 (前節の「base は基底をたどって最初に見つかったものを呼ぶ」挙動との兼ね合いだそうです。)

null 許容参照型

値型の default

null 許容“参照型”と言っているわけですから、名前通り、参照型に関する機能です。 でも、じゃあ、クラスとか参照型を含む構造体が絡んだとき、default(T) はどうするんだという問題が残ります。 (default 既定値は 0/null での初期化になります。nullが絡む。)

なんか今のところ、var y = default(参照型をフィールドとして持つ構造体) みたいなのに対する警告は出さないみたいです。 (もちろん、「null を認めてないつもりのものに null が混ざる」という落とし穴でしかないので、相当な妥協。)

ただ、C# 8.0 では無理としても、構造体の “defaultability” については今後取り組みたい姿勢はある模様。 3年前(roslyn リポジトリ側に文法に関する提案も混ざってた頃含む)に自分が書いた「default を認めない値型を作らせてくれ」という issue:

が急に championed (C# チームの誰かが興味を持って取り組む)状態に変わりました。 ある意味こいつは「値型版の null 許容参照型」です。

利用調査

null 許容参照型のフロー解析をオンにしてどうなるか、 それなりの規模なライブラリを調べてみたみたいです(作者に直接聞いたのか、クローンしてきて自分たちでやってみたのか、ブログとかを見ただけなのかとかはわからず)。 Telegram botとかJilとか。 そこで起きてた問題のまとめ。

  • メンバー定義で困ることが多い。メソッド内部での問題はむしろ少ない
  • インターフェイス側の定義を変えたときの、実装を全部変えて回る作業がやばい
  • 初期化子での初期化を前提としているものに対して警告が消せない
  • コンストラクター連鎖 (A() : this(0) { } みたいなやつ)もフロー解析しきれてない
  • null 許容値型(既存の、値型に対する ?) が null 許容参照型との挙動差でよく問題起こす
  • 診断のクオリティがまだまだ。ジェネリックな型でよく混乱するし、特にタプルに対してつらい
  • 自動 code fix 機能欲しい

プロパティの get/set に Obsolete

プロパティの get/set アクセサーには、それぞれ属性が付けれます(メソッド扱い。AttributeTargets.Method が入ってる属性だけ)。 でも、ObsoleteConditionalCLSComliant の3つは get/set に付けることは禁止されていました。

で、まあ、Obsoleteだけは認めてもいいんじゃないかという話に。 (あと、Xamarin iOS のやつかな、たぶん、Deprecated 属性も。) ConditionalCLSComliant は今後もノータッチとのこと。

params と文字列補間の効率

提案ドキュメントの背景説明に「MSBuildログ最小限にしてもstringで236MBメモリ食っててその半分がFormatがらみ」とか書かれてて、あっ、はい…そうですよね…

文字列の整形絡みは一時的な小さい文字列インスタンスが大量にできちゃって、結構遅かったりします。 なので、自分も結構仕事で、string.Format とかを避けて、stackalloc したりプールした char[] とかを使って自前で文字列整形することがあったり。

あと、params が必ず配列を new しちゃうのも、string.Format を重たくしている原因。

ということで、corefxlab の方で文字列整形をアロケーションなしでできないか、みたいな実験コードが出ていまして。

C# 側で対応しないといけないこともあるので Design Meeting でも議題に。 概ね、

  • paramsSpan<T> を認めて、スタックに値を置いて可変長引数呼び出ししたい
  • 値型のボックス化避けたいから Variant 型作るか

みたいな話。

まあでも、付いたコメント的には

  • なんで stackalloc に参照型使えないの? → それを認めようとするとガベコレのコードがだいぶ複雑になる(パフォーマンスにも悪影響)
  • Span<TypedReference> を認められるようにしようよ

みたいな感じ。

switch 式の優先度

今回の Designe Notes にはないんですけど、もう1個、割と最近立った提案 issue:

現時点 (Visual Studio 2019 RC でのことなので、たぶん、RC が外れても)での switchの結合優先度は関係演算子(== とか)と同じだそうです。 関係演算子って結構優先度が低くいですし、 & よりは上で + よりは下みたいな位置です。

  • x switch { ... } + 1 みたいなものが、} + 1 の方を先に見ちゃってエラーに
  • a + b switch {...}+ が先だけど、a & b switch {...}switch が先

とかいう嫌な状態。

なので、プライマリな優先度(x.M(). とか、[] とかと同じ)に変えようかという提案。

大体、x switch { ... } が後置き演算子みたいな見た目ですからね。 [] とか ++ とか . とか、後ろに置くものは大体がプライマリ。 それに合わせた方が自然だろうという意図もあり。

Index/Range の実装再考

もう1つは C# 側じゃなくて、corefx 側から出ている要望。

今現在の仕様だと、C# の姿勢としては、

  • ^a とか とか a..b とかの構文はそれぞれ Index/Range 構造体を作るだけ
  • x[a..b] みたいなのを使いたければ、コレクション クラスの実装側に Index/Range を受け付けるオーバーロードを増やせ

です。

これに対して、要するに、

  • インデクサーを持つコレクション クラス全部に対してオーバーロードを増やして回るのは大変だし、パフォーマンスが出ない実装になることがある
  • それよりは、collection[i.GetOffset(collection.Count)] みたいな、int のオーバーロードを使うコードに展開する実装に変えてほしい

という流れ。 まあ、そりゃ、コレクション作ってる側からするとそうですよね。 corefx (中の人同士)ですらそうなんだから、ましてコミュニティ実装なコレクションだとなおのこと。

ピックアップRoslyn 4/2: corefx 側とのミーティング

$
0
0

3/25 に、corefx 側の design review チームと C# チームで C# 8.0 絡みの折衝をしていたみたいです。その議事録が上がりました。

当然、corefx (.NET の基本ライブラリ)依存があったり、corefx 側での作業が必要になる機能の話になるので、話題は以下の3つ

  • null 許容参照型 … ライブラリ側にどうやって null 許容注釈をつけて、どうやってリリースしていくか
  • Index/Range … ライブラリ側に Index/Range 型引数のオーバーロードを足すんじゃなくて、パターン ベースに変えたい
  • 非同期ストリームasync foreach で、GetAsyncEnumerator にどうやって CancellationToken を渡すか

null 許容参照型

null 許容参照型は後付けで検証を足すので、オプション指定したり #nullable ディレクティブを書いたりしない限り、検証が有効になりません(でないと既存コードを壊す)。

で、検証をオンにして初めて「null 許容かどうかの注釈」(nullability annotation)が入るわけですが、この null 許容注釈をどうやってリリースしていくかが問題になります。

かつて、案としては「sidecar 配布」って呼ばれる方式も検討されていました。 これはバイクにサイドカーをくっつけるみたいに、既存の dll/exe に後付けで(別ファイルで)注釈を足せるようにしたいという方式です。 でも、そういう特別な処理を保守するのは、得られるメリットよりもコストの方が大きいだろうということで、検討から外れたみたいです。

となると、corefx 自体にちまちまと #nullable ディレクティブを足して、ちょっとずつ注釈を入れていくことになります。 corefx みたいな大規模なもの対してに一斉作業はできないわけで、 .NET Core のアップデートのたびにちょっとずつ注釈が増えて、使ってる側にはちょっとずつ警告が増えます。

null 許容参照型が普及しきるまでの過渡期には、この「ちょっとずつ警告が増える」を覚悟しておく必要があります。 (その覚悟ができないなら、null 許容参照型を有効化するオプションは指定できない。 というのを、C# チームや .NET チームが周知しておかないとまずい。)

Index/Range

これは以前にも取り上げているんですが、

  • 今まで
    • C# 側ではあくまで、^i みたいな書き方から Index 構造体を、i..j みたいな書き方から Range 構造体を作るだけ
    • IndexRange を受け取つけるオーバーロードはライブラリ側の責任で作る
  • これから/変えたい内容
    • パターン ベースで、intのインデクサーや Slice メソッドに展開
    • Index 型の i に対して x[i] みたいなのを、x[i.GetOffset(x.Length)]
    • Range 型の r に対して x[r] みたいなのを、var (offset, length) = r.GetOffsetAndLength(x.Length); x.Slice(offset, length)

みたいな話。 csharplang 側と corefx 側にそれぞれ issue が立っていたりしますが、この日に決まった話みたいです。

非同期ストリーム

await foreach の展開結果では GetAsyncEnumeartor(CancellationToken cancellationToken) メソッドが呼ばれるようになります。 これまで、この、引数の CancellationToken に値を渡す手段がなくて、問答無用に default (キャンセルはしない)が渡っていました。

で、結局、プロパティの set やイベントの add/removevalue 変数(暗黙的に定義済みになってる)と同様に、cancellationToken という名前の暗黙的な変数を用意して、それを介して渡そうかという流れになっているみたいです。


Visual Studio 2019 GA

$
0
0

Visual Studio 2019 の発表イベントに合わせて、Visual Studio 2019 が GA (general available)になりました。

そういえば、Visual Studio も「GA」って言い方するようになったっぽいですね。 今まで「正式リリース」を指して RTM (Release To Manufacturing、ソフトウェアが CD とか DVD で売られてた頃の名残)とか言っていたんですが。

まあ、どうせドキュメントにもしばらく RTM と GA が混在すると思いますが。

インストール

相変わらず日本語検索で「Visual Studio 2019」だけで GA 版のインストーラーにたどり着くのがちょっと大変臭く…

同僚は「Publickey経由で行けばダウンロードできた」と言ってました。

自分は先月すでに RC (Release Candidate)版をインストールしていたので、アップデートの自動配信で GA 版にアップデートされました。

言いたいことは RC の時点で言った

毎度恒例なんですけども、「言いたいことは RC 版の時に行っちゃったので、GA 版で今更言うことはない」状態です。 RC ってのはそういうもの(バグ修正を除いて変更はしない)なので。

ということで、詳細は以前の RC 版のブログ

を参照してください。

Visual Studio の配信チャネル

先日の RC の時点でも書きましたが、今、Visual Studio Installer は「RC → GA」配信のチャネルと、Preview のチャネルに分かれています。

今は、

  • RC だったもの → GA にアップデート
  • Preview の方 → 16.0.0 Preview 5.0 が配信される(GA 相当と同じ機能)
  • ついでに、Visual Studio 2017 の方にも 15.9.11 の配信あり

という状態。 Preview の方にはいずれ 16.1 Preview 1 の配信があると思います。

ちなみに、C# 8.0 にはいくつか 16.1 で初めて実装される機能があるんですが、 「Language Feature Status」を眺めている感じ、それが提供されるのは 16.1 Preview 2 かららしいです。 Preview 1 時点ではおそらく何の変化もありません。

C# 8.0 有効化したった

ということで、自分にとっては「言うことは先月言った。インストールも RC からのアップデートだけ」という状態なわけですが。

一応、職場でチーム全員が Visual Studio 2019 になったのをいいことに、 C# 8.0 を仕事でも有効化してしまいました。

いくつか破壊的変更も報告されていますが、職場のコードで踏み抜いた変更はなかったです。

ただし、その際には以下の注釈付き。

状況:

  • Visua Studio 2019 (16.0)が GA (general available)になったけど、C# 8.0 は preview という扱い
  • LangVersion preview とかを指定しないと使えない
  • 正式リリースは .NET Core 3.0 に合わせるはずで、 .NET Core 3.0 は「今年後半」とだけ言われてる
  • preview と言っても、現状で実装されてる機能は機能ごとにほんと安定度が違うので、使ってよさそうなのとダメそうなのが明確に分かれてる

以下の機能は遠慮なく使おうかと:

switch 式だけは注意が必要で、16.1 で演算子の優先順位が変わる予定。単品でだけ使う方が無難(x + y switch { ... } みたいな書き方は避ける)。

.NET Core 3.0 依存な機能(Rangeとか非同期ストリームは当然使えないし、null 許容参照型もまだ手を出さない。

ピックアップRoslyn & Visual Studio 16.1 Preview 1

$
0
0

Visual Studio 16.1 Preview 1

Visual Studio 16.1 Preview 1 が来てたことに今更気付くなど…

先日のVisual Studio 2019 GA の話で書いた通り、これまで「Visual Studio 2019 Preview」をインストールしていた人のところには 16.1 Preview 1 が配信されているはずです。 GA 版にかまけてしばらく Preview チャネルの方を見てなかった… (4/10 の配信)

Language Feature Statusによれば、16.0 から 16.1 Preview 1 での差分は以下の1点のみです。

ジェネリックな構造体でも条件さえ満たせばポインターとか stackalloc とか使えるようになりました。 C# によるプログラミング入門にも反映させてあります。

using の話とか非同期ストリームの話とか、先に埋めた方がいいだろと思うものもちらほらあったりはするんですが、なかなか手付かずに…

Design Notes

また一気に大量アップロード…

ちょっと一気に来過ぎて自分も大筋しか見れてないんですが… 興味の引かれたところだけ抜粋:

switch

  • switch 式の優先度変えたいって
    • 今: 比較演算子の辺り。< とかの近く
    • 変更後: 単項演算子の直後。インデクサー、キャスト、await とかよりは下で、掛け算の * とかよりは上。
  • やっぱ {} が暗黙的に「null ではない」の意味なの混乱しそう
    • でも、is での挙動と合わせないと変だし
  • 将来の or パターンのために、今、case X or みたいなのは警告出すべき?
    • 今もう動くコードなので、破壊的変更になっちゃうから無理そう

非同期ストリーム

  • 非同期イテレーターへの CancellationToken の渡し方どうしよう?
    • 引数に所定の属性を付けたら、生成されるイテレーターに伝搬するようにしたい

??= 演算の戻り値の型

以下のコードを書いたとき、 c の型はどうあるべきか

int? b = null;
var c = b ??= 5;
  • 今(16.1 Preview 1)の実装は int? になる
    • b ??= 5' とb = b ?? 5' が同じ意味になるように
  • 今後、int になるように変える

Index/Range

  • 先日書いた「パターン ベースにする」という話、確定
  • Length があればまずそれを、なければ Count を調べる
    • Length の戻り値が int でなければそれは無視して Count を調べる
    • Count の実装が O(1) じゃない場合の心配とかはしない(それは .NET のデザイン ガイドライン違反)
  • [Range r] なインデクサーよりも、Slice(int start, int length) の方が優先される
  • インスタンス メンバーしか追わない(拡張メソッドは調べない)
  • int 以外のインデクサーは認めない(暗黙の型変換とかは追わない)し、引数も1個だけのしか調べない
  • Sliceintが2引数のものしか調べない

null 解析(null 許容参照型)

  • 到達不能なコード(return の後ろとか)の null 解析はしない
  • 匿名型のメンバーの nullability は追うべき? → yes
  • 以下のコード、実装側で区別できなくて困らない?
interface I
{
    void Foo<T>(T value) where T : class;
    void Foo<T>(T? value) where T : struct;
}

→ 困る。実装側に where 制約を付けて区別できるようにしないといけない。

  • dynamic な変数の nullability は追う? → yes

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

  • reabstraction は認めるか
    • abstract を付けて、デフォルト実装があるメソッドを再び「派生クラスでの実装が必須」の状態に戻す話
    • もし実装が楽そうならやるべき理由はある。実現性を要調査 → 十分できそう
  • デフォルト実装ないから object のメンバー、特に MemberwiseClone のアクセスを認めるか
    • たぶん yes。でも問題起こしそう。object の protected メンバーはアクセスできないようにするかも → できなくするほうがだいぶ好ましそう
  • クラス同様、partial メソッドは暗黙的に private? → yes

Better Obsoletion (Obsolete 属性をよりよくしたい)

C# 側ではなくて dotnet/designs の方に出ている話ですがもう1個。

.NET の基本ライブラリには、廃止予定にしてしまいたい(Obsolete 属性を付けてしまいたい) API がもう結構大量にあるわけですが。 今の Obsolete 属性の仕様だと「全部抑止」か「全部警告」の all or nothing で困るという話。

「ある特定の Obsolete 属性は無視して、それ以外の Obsolete 属性はちゃんと警告になる」みたいなグルーピング機能が欲しく、そのために Obsolete 属性に DiagnosticId プロパティを追加しようという提案になっています。

新しい属性を作ることも考えたけど、そしたら皮肉なことに Obsolete 属性自体に Obsolete 属性がついてしまうという問題。

ピックアップRoslyn 4/27 & Visual Studio 16.1 Preview 2

$
0
0

Visual Studio 16.1 Preview 2

Visual Studio 16.1 Preview 2 が来てますね。

C# 8.0、Preview 1 からの差分だと、以下の2つの機能が追加されました。

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

インターフェイスに、実装を持った関数メンバー(メソッド、プロパティ、インデクサーなど)を持てるようになりました。

どちらかというとライブラリ実装者向けの機能で、多くの開発者(大体はライブラリは使う側)にとってはそこまで大きなインパクトのある機能ではないんですが。 先日のイベント登壇でも話した通り、この機能の重要な点は、「.NET ランタイム側の修正が必須」という部分です。

古いランタイムでは動かない機能追加というのがいつ以来かというと、C# 2.0、つまり、2005年以来14年ぶりの出来事です。

状況的に言うと、

  • .NET Core が単なる「.NET Framework からの移行期」をやっと超えた
  • .NET Core なら新しいランタイムを採用しやすい
    • side by side インストールができるので、新旧ランタイムの混在ができる
    • インストーラーも小さい
  • WPF や WinForsm など、Windows 限定機能も .NET Core 3.0 で動くようになって、 .NET Framework にこだわる必要性が減った(少なくとも新規案件では採用理由がない)

という感じで、ランタイム側の修正がやりやすい状況がやっと来ました。 この3・4年の間、「それは C# コンパイラーだけでは無理だから採用できない」と言われて実装されてこなかったような機能が、今後は採用される可能性が高まっています。

readonly 関数メンバー

in 引数を使ってもコピーが発生する場合」で説明している「隠れたコピー」(hidden copy)を回避するための機能です。

関数メンバーに readonly 修飾を付けることで、その実装内でフィールドを書き換えていないという保証をします。結果的に、「書き換わったら困るので防衛的にコピーしてからメソッドを呼ぶ」みたいな挙動がなくなって、パフォーマンスが向上します。

とりあえず、フィールドの書き換えをしていないメソッドとかには readonly を付けておけばいいです。 特に、構造体に対してはこの隠れたコピーがよく問題を起こすので、意外と重要な機能です。

ただ、ref readonly と似てて、ちょっとした語順の差で別の機能になったりするので注意が必要です。 readonly ref T M<T>() なら readonly 関数メンバーで、ref readonly T M<T>() なら参照戻り値です。

Design Notes

また2件、C# Design Notes 追加。

null 許容参照型(NRT)がらみの話3件と、switch 式の「target-typed」の話、非同期イテレーターへの CancellationToken の渡し方の話。

null 許容参照型がらみ

  • finally ブロック内での null チェックの結果は、その後ろでも有効かどうか
    • 有効であるべきなんだけど、フロー解析が複雑になる(= コンパイル時間が増える)
    • 簡単にできる範囲でフロー解析に手を加えることに
  • partial 定義で nullability が違う指定をしてしまったとき、どう扱うべきか
    • 違う nullability 指定をしてたらエラーにする
  • ジェネリック型引数では MaybeNull 属性が必要そう
    • T FirstOrDefault<T>(IEnumerable<T>) の戻り値が代表例で、制約なしのジェネリックなので T? とは書けないけど、null を返す可能性があるものに付ける

switch 式がらみ

switch 式に関しては、

byte M(bool b) => b switch { false => 0, true => 1 };

みたいに書いたとき、この 0, 1 をちゃんと byte と判定できるようにしたいという話。 (現在(16.1 Preview 1時点)では、switch の戻り値が int になって、int から byte への暗黙の型変換はダメと怒られる。) 戻り値側からの型推論なので「target-typed」。

同じことは条件演算子 b ? 0 : 1 でもやりたいけども、既存のコードを壊さないようにするために、switch 式ほどは自由度効かなさそう(それでも、影響を最小限に抑えられそうな範囲で検討中)とのこと。

非同期イテレーターがらみ

ちょっと前からの決定事項なんですが、 非同期イテレーター(awaityield returnの混在)に対する CancellationToken の渡し方は、引数に属性を付けることでやりたい、(属性名は本決定してないけど仮に)以下のような書き方をするという話があります。

async IAsyncEnumerable<T> M<T>([DefaultCancellation] CancellationToken cancellationToken)
{
    ...
}

これに関して、

  • 非同期イテレーターになっている実装自体でないものに属性が付いていると警告を出す
    • 要するに、abstract で IAsyncEnumerable<T> を返しているメソッドとかに属性が付いていると警告になる
  • 複数の CancellationToken 引数に属性が付いていても警告は出さない
  • 1つもCancellationToken 引数に属性が付いていない場合は警告を出す

みたいにしたいみたい。

.NET 5、Visual Studio 16.1 Preview 3

$
0
0

今年の build、思ってたよりも .NET がらみが盛沢山…

Windows TerminalとかVisual Studio Onlineとかの方がさらにインパクト強そう? ですけど、 .NET がらみもだいぶ。 まあ、3.0 が今年こそ見えてきましたからね。

  • Introducing .NET 5
    • .NET Core 4 は Framework 4.X と紛らわしいから欠番にして、次は「5」
    • 徐々に .NET Core に一本化して、名前も「.NET」に
    • これからは年に1回、毎年11月にメジャー リリースする
      • 2019/9 に .NET Core 3.0
      • 2019/11 に .NET Core 3.1
      • 2020/11 に .NET 5、以降毎年6, 7, 8, ...
        • 5 以降、Long Term Support は偶数番だけ
  • Announcing .NET Core 3.0 Preview 5
    • Preview 4 との差分
    • 差分のみだけど今回結構大きそう
    • 令和対応とか混ざっててちょっと受ける
  • Visual Studio 2019 version 16.1 Preview 3
    • IntelliCode が GAだって
      • 何を持って GA なんだろう… 「16.1 Preview 3 からはオプション指定なしでデフォルトでオン」の意味?
    • IntelliSense、 using してない名前空間の型も補完候補に出てくるように
      • IntelliCode のおかげで候補が賢くなってるおかげか、候補が多過ぎてつらい問題はそこまでなさそう?
  • https://online.visualstudio.com/ 発表
    • 今のところ上記 URL は発表ブログに転送
    • 将来的には「ブラウザー版 Visual Studio Code」になる予定
      • 今もう、実は try.dot.netとかが Blazor ベースで動いてたりするので、布石はあった
      • Blazor 自体も「プレビュー」に(早期開発版フェーズは通過)
    • 名前の再利用やめろ… Surface といい docs といい…
      • ALM とか Team Services とか DevOps とか呼ばれてるやつの名称が「Visual Studio Online」だった時期がある

「11月って言うと、確かにホリデーシーズン直前で、アメリカ製品はその時期に出るものが一番安定してるけど」とか、「build で毎年11月とかそんなタイトな公約掲げちゃって大丈夫?」とかは思ったり思わなかったり。

まあ、この辺りは他の人がまとめてるので概要のみにして。

C# 8.0 in Visual Studio 16.1 Preview 3

最近はすっかり C# 中心の話ばっかりなわけですが。

今回の Preview 3 では、新機能は特に増えていないんですが、ちょこっと修正があります。 前々から「.NET Core 3.0 依存だから安定してない」って言っていた機能に、予定通り変更あり。

  • 非同期イテレーターに CancellationToken を渡せる手段ができました
    • 例を gist にアップロード: EnumeratorCancellation.cs
    • EnumeratorCancellation 属性を付ければいいらしい
    • 後述しますがちょこっと挙動変更される予定がすでにあり
  • Index/Range がらみのコード生成結果変更
    • 2月にブログにした通り
    • 変更内容に関する提案ドキュメント: Index and Range Changes
      • 旧仕様: コレクション側が Index/Range 構造体を受け付けるオーバーロードを用意
      • 新仕様: C# コンパイラーが Index.GetOffset を呼んで int に変換

ピックアップ Roslyn 5/7

で、3日ほど前に1個、C# の Design Notes のアップロードがありました。

base(T)

一昨日、インターフェイスのデフォルト実装の話を書いたところなんですよ。 その中には base(T) アクセスの話もあります。 これを書いてから上記の Desing Notes に気づいたわけですが。

base(T)、C# 8.0 からは外して、ランタイムのサポート込みで C# 9.0 に回したい」ですって。

書き直さなきゃ…

非同期イテレーターのキャンセル

この話は前述のgist に上げた例にも書いてあるんですけども。 以下のような非同期イテレーターを書いた場合、

async IAsyncEnumerable<int> X([EnumeratorCancellation]CancellationToken ct = default)

CancellationToken は、X(ct) というのと、X().WithCancellation(ct) というの、どちらで書いても最終的に X の引数に渡ってきます。

で、両方指定できちゃう。X(ct1).WithCancellation(ct2) が合法。 この時どういう挙動をすべきかという話です。

  • 現状: ct2 が優先で上書きされる
  • 提案: ct1ct2リンクさせたものを新たに作る

CancellationToken だけじゃなくて CancellationTokenSource にも依存するとか、 コード生成が複雑になるとか、 CancellationTokenSource を持つためのフィールドも増えてメモリ的にも優しくないとか、 デメリットもあるんですが、さすがに上書き挙動はまずそう。

トリアージュ

buildで、.NET Core 3.0 のリリースが今年の7月にRC、9月にGA、11月に3.1と明言されたわけですが。 C# 8.0 はこれに追従する予定です。 要するに C# 8.0 にもスケジュールが切られました。 逆算すると、「C# 8.0 に何を入れて何を入れないか」の決断のタイムリミットが今。

ということで、なんかむっちゃ仕分けされてました。 仕分け結果は csharplang 内の GitHub Projectにも、 Roslyn 内の Language Feature Statusにも反映済み。 多少この2つに不整合があるんで、たぶんまだもうちょっと整理中のはずです。

前々から「8.0 タグが付いてるけど怪しくない?」みたいに思ってたものはやっぱり外れました。 急ぎ、自分用のタスク リストにも反映。

ローカル関数に対する属性

非同期イテレーター、属性ベースで CancellationToken を渡すようになったわけですが。 そこで「ローカル関数にも属性付けれないとまずいじゃない」という話になったみたいです。

ということで、それも実装予定に。

ピックアップRoslyn 5/19: dotnet-try, .NET Core 3.0 パフォーマンス、null 許容参照型の仕様改善

$
0
0

この1週間ほど、build で発表したことを改めてブログ化したものが投稿されたりとか、build が終わって落ち着いたところで本業に戻ったと思われる投稿とかがたくさんありました。

そのうち3つほど紹介

  • dotnet-try
  • .NET Core 3.0 でのパフォーマンス改善
  • C# Design Notes 2件追加(どちらも null 許容参照型がらみ)

dotnet-try

以下のようなコマンドで簡単にインストールできるツール(.NET Global Tool という仕組み)として、 dotnet-try というものが公開されました。

dotnet tool install --global dotnet-try

C# でいわゆる interactive workshop (ドキュメント中に直接コードが埋め込まれてて、実行結果が見れるような講習資料)を簡単に作れるようにしようというもの。 以下のような仕組み。

  • 普通に dotnet コマンドでコンパイルできるプロジェクト一式を書く
  • Markdown 中の ```cs 行に、その C# コードを参照するオプションを付ける
  • プロジェクトを置いたフォルダー中で dotnet try ツールを起動する
  • dotnet try 自体が Web サーバーになっていて、ブラウザーが起動して localhost アクセスで書いたドキュメントが表示される
  • ドキュメント中の C# コードは構文ハイライト表示されるし、実行ボタンがあって結果を出力できる
    • Blazorを使ってるっぽい

Try .NET Online

2017年以降、 .NET 関連のドキュメントはdocs.microsoft.com 上にあるわけですが、 その中ではサンプルの C# コードをブラウザー中で実行して結果を見れる機能があります。 これを指して「Try .NET Online」と言っています。

初期はほとんどの処理をサーバー上でやっていて常に通信してコードや実行結果を表示していました。 それを徐々に Blazor と Web Assembly 化しているそうで、 今はだいぶクライアント上での処理になっているみたいです。 (まだ完全にオフラインではなくて、コンパイルはサーバー側でやっていそう。)

Try .NET Offline

今回リリースされた dotnet-try ツールは、 「Try .NET Online」と同じドキュメントをローカル環境で書いて試せるという仕組みで、 「Try .NET Offline」と言っています。

普通の C# プロジェクトを作ってそれを参照する仕組みなので、 ちゃんとコンパイルできることが確認を取れているコードの一部分を参照して、 その部分の実行結果が正しく表示されます。

全部オープンソースです。

現状、とりあえずアルファ リリースみたいです。フィードバック募集中。

そういう段階のツールなので、利便性はまだまだあまりよくありません。 作った interactive workshop の共有みたいなところまでは至っていなくて、 現状だと作る側も自前で GitHub ででも公開してもらって、 見る側も自前で以下のようにコマンドラインでツール起動してもらう作り。

git clone 公開先
dotnet try cloneしてきたリポジトリ

.NET Core 3.0 のパフォーマンス向上

.NET Core 2.0.NET Core 2.1の時にもブログがありましたが、.NET Core 3.0 でも改めて「こんな最適化をやったよ」というまとめブログが上がりました。

今回はまたものすごい長大な内容…

長くてまじめに読むのは大変ですが、概ね、以下のような感じ。

  • Span<T>とかMemory<T>自体を最適化した
  • Span<T>Memory<T> を使うものを引き続き増やした
  • Hardware Intrinsicsを全面的に適用開始
    • バイナリ操作とか文字列操作は本当に2~4倍高速化
    • ただし、現状は Intel CPU のみ(AVX2 が有効な環境でだけ2~4倍高速化)
    • ARM 系はまだまだこれから
  • 非同期処理の最適化
  • その他、細かい最適化も大量
    • T? から Value で値を取ってたところを GetValueOrDefault に置き換えて回ったり
    • new T[0]Array.Empty<T>() に置き換えて回ったり

Design Notes 2件(null 許容参照型がらみ)

どちらも null 許容参照型がらみで、 だいぶ具体的な話。 先日、「そろそろ C# 8.0 に入れる機能を決めるタイムリミット」という話をしましたが、 これもその一環だと思います。 そろそろ null 許容参照型の仕様も固めないとまずい。

#nullable の書き方

null 許容参照型(破壊的変更にならないように opt-in)を有効化するにあたって、 以下の2つの視点があります。

  • annotations: ライブラリを提供する側として、null の許容・非許容のアノテーションを公開するかどうか
  • warnings: ライブラリを使う側として、コード解析をして null 参照に対して警告を出すかどうか

移行段階としては、どちらか片方だけを有効化したいことがあります。

  • 差し当たってアノテーションだけは付けたいけど、中身の警告を全部消す作業まで手が回らない
  • 差し当たって警告は出してほしいけど、自分が公開している API にまでは責任を持てないのでアノテーションは付けたくない

結果、指定できるオプションは以下のようにまとめたいとのこと。

#nullable (enable | disable | restore) [ warnings | annotations ]
  • enable、disable で有効・無効を切り替え
    • restore は1つ前のディレクティブの状態に復元
    • その後ろに何も書かなければ annotations も warnings もまとめて切り替え
  • その後ろに warnings、 annotations を付けることで、どちらか片方だけを切り替え

ソースコード中の #nullable ディレクティブ(その行移行の局所的に作用)と同じように、 csproj 中に書くタグ(プロジェクト全体に作用)も、以下のようにしたいみたいです。

  • タグ名はシンプルに Nullable にする
    • 今は NullableContextOptions とかいう長い名前
  • オプションの値も enable、disable、annotations、warnings にする

アノテーションの付け方

前節の #nullable enable を指定した状態では、基本的に以下のようなアノテーションの付け方になります。

  • 参照型 T に対して単に T と書くと非 null
  • T? と書くと null 許容

ただ、これだとどうしてもうまくいかない場合があって、 そういうとき用に属性でのアノテーションを足したいという話。

単純な事前・事後条件

T と書いていても null 許容に、T? と書いていても非 null に変えたい場合があります。

  • 制約未指定の T がジェネリック型引数の場合、値型と参照型で T? の意味が違うせいで T? と書けないので属性に頼るしかない
  • プロパティで、get は非 null だけど set は null 許容とかにしたい
  • 参照引数で、in/out 片側だけを null 許容にしたい
    • null を渡してもいいけど、メソッドを呼んだあとはその変数が null じゃなくなる保証あり
    • null を受け取れないけど、メソッド内で null を書き込む可能性あり

そこで、以下の属性を用意

  • 事前条件
    • [AllowNull]: T と書いていても、入力として null を受け付ける
    • [DisallowNull]: T? と書いていても、入力として null を受け付けない
  • 事後条件
    • [MaybeNull]: T と書いていても、出力として null を返す可能性がある
    • [NotNull]: T? と書いていても、出力として null を返さない

相互依存のある事後条件

TryParse とか IsNullOrEmpty とか、何かメソッドを呼んだ結果 null かどうかが確定するものがあります。 こういうとき用に、以下のような属性を用意。

  • [MaybeNullWhen(bool)]: 戻り値が true/false の時に限り、指定した引数が null になる可能性がある
  • [NotNullWhen(bool)]: 戻り値が true/false の時に限り、指定した引数が null でない保証がある

「引数が null のときだけ戻り値も null」みたいなこともあります。 そのための属性もあり。

  • [NotNullIfNotNull(string)]: 指定した引数の nullability と戻り値の nullability が一致

コンパイラーによる特殊対応

x == y とか書くと、x の nullability が y に伝搬します。 x が非 null なことがわかっているなら、y も非 null で確定。

== はいいとして、それと同様の効果があるメソッドがいくつかあります。

  • Object.ReferenceEquals
  • Object.Equals
  • IEqualityComparer<T>.Equals
  • EqualityComparer<T>.Equals
  • IEquatable<T>.Equals

こいつらは、数が限られているし、これら以外のメソッドで等値判定をすることはほとんどないので、コンパイラーにハードコードで実現したいそうです。 ちゃんと、x.Equals(y) で nullability が伝搬するものの、それは Object.Equals を特別扱いすることで実装。

同じく、Interlocked.CompareExchange も特別扱いで nullability 伝搬するそうです。

フローがらみの属性

今ある「確実な初期化ルール」でもそうなんですが、到達できない場所のフロー解析はされません。

using System;
 
class Program
{
    static void Main()
    {
        int x;
        return;

        // 本来「x を初期化せずに使っちゃダメ」と怒られるようなコード。
        // 別にエラーにならない。その代わり、「return のせいでここには絶対来ないよ」警告が出る。
        Console.WriteLine(x);
    }
}

nullability のフロー解析もこれと同じ挙動になります。

そこで困るのが、以下のような状況。

using System;
 
class Program
{
    // 例外を出すのでこのメソッドより後ろは絶対に実行されない。
    static void Throw() => throw new Exception();
 
    static void Main()
    {
        int x;
 
        // 絶対に戻ってこない。
        Throw();
 
        // 以下の2行もアプリケーション クラッシュになるのでここより後ろは実行されない。
        System.Diagnostics.Debug.Assert(false);
        Environment.FailFast("fatal error");
 
        // でも、現状だとコンパイラーがそれを知るすべがな。
        // なので「到達できない」警告じゃなく「未初期化」エラーに。
        Console.WriteLine(x);
    }
}

ということで、以下のような属性も用意。

  • [DoesNotReturn]: メソッドを呼んだ時点でそこより後ろは実行されない
  • [DoesNotReturnIf(bool)]: 指定した引数が true/false の時、そこより後ろは実行されない
Viewing all 482 articles
Browse latest View live