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

小ネタ 「deconstruct」という単語

$
0
0

今日も、小ネタなような、C#7思い出話なような。

C# 7で、分解という機能が入ったわけですが、英語だと deconstruction という単語になります。

分解という機能のおさらいですが、以下のような書き方でタプルなどの型のメンバーを抽出できる機能です。

var (x, y) = tuple;

これ、他のプログラミング言語だと、destructuring とか呼ばれたりしています。 といっても、文法上正式に destructuring と呼ばれているわけではないんですが(大体の言語は文法上は単に「pattern」とか呼ばれる機能)… まあ、解説ページなんかでは destructuring と呼ばれます。

で、今日、何が言いたいかというと、

  • deconstruct : デコンストラクト
  • destructuring: デストラクト

並べるとわかりますかね。 「con」の有無。 deconstruct destructuring。

用語として多少もめてたりするみたいです。

  • 他の言語に合わせてdestucturingであるべきじゃないか。
  • construct (con(共) + struct(築))の逆はdestruct (de(脱) + struct(築))じゃないのか
  • でも、destuctureだとデストラクターと紛らわしくないか。
  • C#のデストラクターって、あれ、実際にはfinalizerだし。誰だよ、destructorって名前にしたやつ

みたいな雰囲気。

日本語だとたぶん、僕みたいに「分解」とかに訳しちゃうんでそんなに変でもないんですが、 英語だとdestructorとdeconstructionが並ぶことになるんで気持ち悪いみたいですね。

まあ、デストラクターも分解も、どちらもコンストラクターの逆ではあります。

コンストラクターの仕事

ちなみに、コンストラクター、初期化子、デストラクター、分解の例をまとめて挙げると以下のような感じ。

constructor, initialiser, destructor, deconstructionの例

そもそも英語でdestroyの名詞形がdestructionなのが良くないかも。 「destruct」で切った場合、それはdestroy(破壊)のことなのかdestructure(脱構造化)のことなのかそもそもわからず。

それに、C#で破棄用の構文をデストラクターと呼ぶのはC++から持ってきた言葉なわけですけど、 これ、やっぱりJavaみたいにfinalizerって呼んでおくべきだったのかも。 デストラクターに関しては、分解が絡まなくても元々名前には悩んでいるみたいです。

  • .NET的には finalizer って呼び名になってる。destructor って呼び名はC#だけ
  • ECMAに出しているC#仕様書上は finalizer って呼び名になっているらしい
  • MSDN上に出してるC#仕様書は destructor になってる
    • 今、ECMA版とMSDN版の統合を考えてるんで、なおのこと問題に
  • Roslyn の API 中でも destructor という単語を使ってる
    • これがあるんで、単純に文章上だけの変更ってわけではなくて、ソースコードにも影響あり

とりあえず、現状のC#チームの希望的には以下のような雰囲気です。

  • 分解は deconstruction のまま(de + con 付き)
  • デストラクターって呼び名は微妙なので変えることも視野に入ってるみたい

小ネタ 構文糖衣と、そうではない構文と

$
0
0

構文糖衣が多い言語

C#は構文糖衣が結構多い言語です。 構文糖衣(syntax sugar)っていうのは、要するに、「定型的な長くて面倒なコードにはなるけども、原理的にはその構文がなくても全く同じ意味のコードが書ける」というような類の機能です。

例えばクエリ式がわかりやすいですが、以下の3つの式は全く同じ意味になります。

from x in data where x > 2 select x * x
data.Where(x => x > 2).Select(x => x * x)
Enumerable.Select(Enumerable.Where(data, x => x > 2), x => x * x)

ということで、C#の機能を説明するとき、結構、「こういうコードと同じ意味になります」というような文章を書くことは多いです。

特に、ここで例に挙げたクエリ式は、式 → メソッド呼び出し → 必要があれば拡張メソッドの静的メソッドへの展開 と、2段階の変換を行う機能です。 いうなれば、「2段階」構文糖衣。

構文糖衣でない文法

逆に、C#に対して後から追加された構文の全部が全部単なる構文糖衣ではなく、コンパイル結果まで覗いてみる必要のものもちらほらあります。

もちろん、C# 2.0の時のジェネリックの追加のように、.NET Frameworkのランタイム自体に手を入れることで実現した機能もあります。

一方で、「ランタイムのレベル(型システムや中間言語(IL: Intermediate Language)の仕様レベル)では元々機能としてあるけども、 C# からは使えなかったものを使えるようにした」 というような機能もいくつかあります。 代表的なのは以下の2つでしょう。

ジェネリックの変性

ジェネリックの変性(variance)は割かしわかりやすい例でしょうか。 C#上の構文糖衣でどうこうできるわけではなくて、.NETのメタデータのレベルで処理されています。

例えばC# 4.0以降で以下のようなコードを書いたとしましょう。型引数Tに、共変であることを示すout修飾子を付けています。

interface IWrapper<out T>
{
    T Value { get; }
}

逆アセンブルしてみると以下のようになっています。 ILのレベルでも、Tの前に+という記号が入っていますが、これが共変を表すフラグです。

.class interface private abstract auto ansi IWrapper`1<+ T>
{
} // end of class IWrapper`1

このフラグは、IL的には .NET 2.0の頃からありましたが、 C#からこのフラグをいじれるようになったのはC# 4.0(.NET 4と同世代)からです。

まあ、何にしても「以前のC#であればこう展開されます」的な説明はできない機能です。

参照戻り値

最近だと参照戻り値なんかもそうです。

ILって実は安全性を損なうきわどい書き方をやりたい放題で、 C#のレベルで制限を掛けて安全性を担保していることが結構あります。 参照戻り値は、

  • これまで安全性を担保するための解析がしんどかったから使えなかった、
  • C# 7では、C#コンパイラーが賢くなったからその解析ができるようになって、認められるようになった

というものなわけです。 ILはそんな安全性とか気にしないので、昔から参照戻り値を使えました。

例えば、C# 7で以下のようなコードを書いたとします。

static ref int RefMax(ref int x, ref int y)
{
    if (x >= y) return ref x;
    else return ref y;
}

コンパイル結果は以下の通り。&が参照を表す記号です。

.method private hidebysig static int32&  RefMax(int32& x,
                                                int32& y) cil managed
{
  // コード サイズ       10 (0xa)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldind.i4
  IL_0002:  ldarg.1
  IL_0003:  ldind.i4
  IL_0004:  blt.s      IL_0008
  IL_0006:  ldarg.0
  IL_0007:  ret
  IL_0008:  ldarg.1
  IL_0009:  ret
} // end of method Program::RefMax

このコードは、.NET 1.0の頃から書けました。 もっとも、int32& (C# 7でいうref int)の戻り値を受け取る手段がなかったので、実際のところ書いてもC#からは使えないコードになります(例えばこのコードをVisual Studio 2017でコンパイルして、その結果のDLLをVisual Studio 2015から参照した場合、このメソッドはクラスのメンバー一覧情報のところに表示されません)。

ちなみに、参照に対する操作は、実のところポインターに対する操作とまったく同じになります。 例えば、以下のようなunsafeなコードを書いてみます。 ポインターになっただけで、やっていることは先ほどの参照を使ったコードとまったく同じです。

unsafe static int* RefMax(int* x, int* y)
{
    if (*x >= *y) return x;
    else return y;
}

こちらのコンパイル結果は以下のようになります。&*に変わった以外の部分は一字一句たがわず、先ほどのコードと完全に一致しています。

.method private hidebysig static int32*  RefMax(int32* x,
                                                int32* y) cil managed
{
  // コード サイズ       10 (0xa)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldind.i4
  IL_0002:  ldarg.1
  IL_0003:  ldind.i4
  IL_0004:  blt.s      IL_0008
  IL_0006:  ldarg.0
  IL_0007:  ret
  IL_0008:  ldarg.1
  IL_0009:  ret
} // end of method Program::RefMax

ldindはload indirect (間接ロード)の略で、 ポインターや参照ごしに値を取ってくる命令です。 ポインターと参照でまったく同じ命令を使います。

おまけ: Unsafe

余談となりますが、ILを使えば、自己責任で安全でないコード書き放題だという例にも触れておきます。

今、System.Runtime.CompilerServices.Unsafeとかいう名前からしてunsafeなライブラリがあったりします。

このパッケージ中にあるUnafeクラスは、3日目に紹介したSystem.Memoryの中で使われています。 というか、元々はSystem.Memoryパッケージ内にあったコードを、これ単体で使えるだろうと切り出した結果がSystem.Runtime.CompilerServices.Unsafeパッケージです。

ソースコードも以下のGitHubリポジトリで公開されているので中身を覗いてみると…

完全にILで書かれています

ポインターと参照を相互に変換するメソッドなんかもあったりするんですが、 以下のような感じで、実はほぼ素通しです。 ロード(ldarg)して、即リターン(ret)。

.method public hidebysig static void* AsPointer<T>(!!T& 'value') cil managed aggressiveinlining
  {
        .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
        .maxstack 1
        ldarg.0
        conv.u
        ret
  } // end of method Unsafe::AsPointer

    .method public hidebysig static !!T& AsRef<T>(void* source) cil managed aggressiveinlining
  {
        .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
        .maxstack 1
        ldarg.0
        ret
  } // end of method Unsafe::AsRef

ちなみに、conv.u命令は、ネイティブ(CPUの種類に応じて32bitか64bitか切り替わる)符号なし整数への変換命令です。 ポインター = ネイティブ符号なし整数。

小ネタ プリミティブ型

$
0
0

.NETには「プリミティブ型」とかいうものがあるんですが、 何をもってプリミティブと言えるのか、 どういう型がプリミティブ型なのかというと、 なんかよくわからない存在です。

Type型にIsPrimitiveというプロパティがあって、こいつがtrueを返すものがプリミティブ型なんですが。 以下のコードを見ての通り、どういう基準なのかがパッと見でわからず。

using static System.Console;

class Program
{
    static void Main()
    {
        WriteLine(typeof(int).IsPrimitive);     // true
        WriteLine(typeof(bool).IsPrimitive);    // true
        WriteLine(typeof(double).IsPrimitive);  // true
        WriteLine(typeof(object).IsPrimitive);  // false!
        WriteLine(typeof(string).IsPrimitive);  // false!
        WriteLine(typeof(decimal).IsPrimitive); // false!
        WriteLine(typeof(System.IntPtr).IsPrimitive); // true!
    }
}

primitive

primitiveという単語の意味は、原始的とか基本的とかそんな意味なわけですが。 C#とかのプログラミング言語において「原始的」っていうのは、内部的に専用命令などを持っていて、 ユーザー定義のクラスや構造体ではできない何らかの特別扱いを受けているという意味になります。

特にC#の場合には、C#のレベルではなく、.NETランタイム的に、専用のIL (中間言語、Intermediate Language)命令を持っているかどうかが1つの基準なんですが… それにしても分類は結構あいまいで、よくわからなかったりします。

元々はJavaから

元々はJavaから来ているんですかね。Javaの場合は割かしプリミティブ型がはっきりしています。 以下の3つが一致していて、これこそがプリミティブ型です。

  • intとかbooleanみたいに、専用のキーワードがある
  • 言語的に許される唯一の値型
  • 中間言語(bytecode)的に専用命令を持ってる

C#というか.NETでよくわからなくなる理由は、

  • objectstringdecimalにも専用のキーワードがある
  • 構造体や列挙型があるのでいくらでも値型を作れる
  • 専用命令を持っているって意味ではstringもちょっとだけ命令を持っている

というあたり。

.NETのプリミティブ事情

.NET だと、

  • IsPrimitivetrueな型は以下の通り
    • Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, Single
    • C#のキーワードになってる型から、
      • decimal, object, stringは除く
      • IntPtr, UIntPtrを加える
  • objectstringはキーワードになってるけど参照型
  • 構造体や列挙型があるので、いくらでも値型を作れる
  • stringはプリミティブ型とかと比べると大して専用命令ない
    • けども、ないわけじゃない
    • 命令ではなくメモリの確保の仕方で言うと、stringは他のクラスと比べて相当特殊
      • この意味で言うと配列もかなり特殊
  • decimalに至っては全く専用命令ない
    • decimalが特別なのはC#的にリテラルがあることくらい
    • IL 的にはリテラルすらない、完全に普通の構造体

ということで、何が何やら。まあ大まかに言うと、プリミティブ型 = 「中間言語(IL)的に専用を持っていて、かつ、値型」ですかね。

IL的な扱い

せっかくなので、IL 的な扱いも見てみますか。 以下のようなコードをコンパイルしてみます。

class Program
{
    static void Main()
    {
        M(1, 2);
        M("a", "b");
        M(1.23m, 2.71m);
    }

    static int M(int x, int y) => x + y;
    static decimal M(decimal x, decimal y) => x + y;
    static string M(string x, string y) => x + y;
}

int

まずはintの場合。 メソッドM(int, int)の中身が

  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  add

M(int, int)を呼び出す側が

  IL_0000:  ldc.i4.1
  IL_0001:  ldc.i4.2
  IL_0002:  call       int32 Program::M(int32,
                                        int32)

という感じです。 足し算用にaddという専用命令があったり、 定数読み込みのためにldc.i4.1 (load constant 4バイト整数の1という意味)という命令があったりします。

string

続いてstring メソッドM(string, string)の中身が

  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  call       string [mscorlib]System.String::Concat(string,
                                                              string)

M(string, string)を呼び出す側が

  IL_0008:  ldstr      "a"
  IL_000d:  ldstr      "b"
  IL_0012:  call       string Program::M(string,
                                         string)

です。 連結のためには特に命令を持っているわけではなく、+演算子はConcatメソッド呼び出しに置き換わります。 一方で、値の読み込みのためにldstr (load stringの意味)命令は持っています。

微妙なライン… C#的には組み込み型(stringっていうキーワードがあり、リテラルとかが用意されてる特別な型)だし、 IL的にはプリミティブ型ではない、という割にはldstr命令とか持ってる…

decimal

最後にdecimal。 メソッドM(decimal, decimal)の中身が

  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  call       valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Addition(valuetype [mscorlib]System.Decimal,
                                                                                                valuetype [mscorlib]System.Decimal)

M(decimal, decimal)を呼び出す側が

  IL_0018:  ldc.i4.s   123
  IL_001a:  ldc.i4.0
  IL_001b:  ldc.i4.0
  IL_001c:  ldc.i4.0
  IL_001d:  ldc.i4.2
  IL_001e:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32,
                                                                     int32,
                                                                     int32,
                                                                     bool,
                                                                     uint8)
  IL_0023:  ldc.i4     0x10f
  IL_0028:  ldc.i4.0
  IL_0029:  ldc.i4.0
  IL_002a:  ldc.i4.0
  IL_002b:  ldc.i4.2
  IL_002c:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32,
                                                                     int32,
                                                                     int32,
                                                                     bool,
                                                                     uint8)
  IL_0031:  call       valuetype [mscorlib]System.Decimal Program::M(valuetype [mscorlib]System.Decimal,
                                                                     valuetype [mscorlib]System.Decimal)

です。 どこにも専用命令がないどころか、リテラルの1.23m2.71mすらも、 new decimal(123, 0, 0, false, 2)の意味のコンストラクター呼び出しに置き換わっています。 加算も、op_Additionメソッド呼び出しです。

正直なところ、decimalがC#的に特別扱いを受ける理由はリテラルだけだったりします。 あくまでC#上の特別扱いであって、IL上は他の構造体の扱いとまったく同じです。 なので、decimalはプリミティブではない。

もしかすると、C++みたいにユーザー定義リテラルが書ければ、 decimalなんていう組み込み型は要らなかったかもしれません。

小ネタ privateメンバーはAPIの一部か

$
0
0

ことの発端

なんかぐらばくさんとこので、エラーになるはずのコードがPCLなプロジェクトでだけビルド通ってしまって問題になってたらしい。

要点を抜き出すと以下のような感じ。

using System;

struct DateTimeWrapper
{
    DateTimeOffset t;

    public DateTimeWrapper(int i)
    {
        // t を初期化しないとコンパイル エラーになるはず
        // でも、なぜか PCL プロジェクトではエラーにならない
    }
}

本来ダメなはずのコードが、PCL プロジェクトでだけコンパイルできてしまうという問題。 「ちゃんと初期化しないと怒られるはず」というのが常識のC#でこれをやられると、ほんと見つけられないバグになったりします。

プロジェクトの種類によって挙動が変わる謎の不具合…

csprojの中身を見てみても、どうも最終的に同じコンパイラーを使っていそう。 軽くProcess Explorerを眺めてみても、 ちゃんと同じコンパイラーが動作していそう。 コンパイラーが同じなのに、なぜ同じコードのコンパイル結果が変わってしまうのか、 謎は深まるばかり…

原因は参照アセンブリ

で、調べてみたら、どうも、参照しているアセンブリが違うせいみたい。

参照アセンブリ

問題の話をする前にまず簡単に、アセンブリの種類について補足。 今、NuGetとかでライブラリを参照すると、開発時と実行時で別のDLLが参照されたりします。

  • 実装アセンブリ: 実際に動くコードが入っているDLL。実行時に参照されるのはこれ。
  • 参照アセンブリ: APIサーフェスだけが入っているDLL。開発時にはこっちが参照される。

これは、開発環境と実行環境が違っても問題なく開発できるようにするための処置です。

元々は .NET Framework 3.5の頃に、 クライアント プロファイルっていう、クライアント上では使わない機能を削ったバージョンの .NET Frameworkインストーラーを用意したことが発端で、 「開発環境ではつかえたクラスが、実行に TypeLoadException を起こした」みたいな自体を回避するために作られた仕組みです。 その後、PCLでも同様の手法が使われるようになりました。

要するに、

  • 実行環境の数だけ、開発環境にも別バージョンの .NET のインストールが必要になる
  • それをすべてインストーラーに同梱していたらインストーラー サイズが大きくなりすぎる
  • コンパイルに必要な情報(APIサーフェス)だけ残して、メソッドの中身とかはごっそり削ったバージョンのDLLを用意して、開発環境ではそのDLLを参照する

みたいな仕組み。 ここで言うAPIサーフェスっていうのは「APIとして外に公開されている表層の情報」という意味あいです。 見えない部分は削ってしまえと。

どこまでが API サーフェスか

ここでちゃんと考えないといけないのが、どこまでを API サーフェスとみなすべきか。 すなわち、「開発時に参照するだけならどこまでの情報を残す必要があって、どこまでを削って大丈夫か」という話です。

publicやprotectedなメンバーはわかりやすくていいでしょう。外から見えるので、当然APIサーフェスに含まれるべきです。

ちょっと微妙なラインがinternalで、本来は外から見えないはずですが、 InternalsVisibleTo属性なんてものもあるので、 外から見える可能性が残ります。 なので、APIサーフェスになりえます(InternalsVisibleTo属性があるときだけでいいんですが、参照ライブラリに残す必要があります)。

そして、private。 privateメンバーは、外から参照する手段がありません。 (リフレクションを使うと取れたりはしますけども、コンパイル時には関係ない話です。) なので、APIサーフェスとはみなされない… はず…?

と思いきや、privateメンバーがコンパイルに影響する場面が1つだけあります。 それが、構造体のprivateフィールド。

構造体のprivateフィールド

いくつか、構造体のprivateフィールドがコンパイル結果に影響を及ぼす例を挙げてみましょう。

確実な初期化

C#では、構造体のフィールドは、コンストラクター内で必ず初期化しないといけない、初期化するまでは他のメンバーを呼べないという制約があります。 初期化忘れによるバグを防ぐ意図があります。

でも、空っぽの構造体は初期化しなくてもいいらしい。

struct EmptyStruct { }
struct Integer { private int _x; }

struct DefiniteAssignement
{
    EmptyStruct _e;
    Integer _i;

    DefiniteAssignement(int i)
    {
        // 中身があるものは初期化必須
        _i = new Integer();
        // 一方で、EmptyStruct みたいに空っぽのものは初期化不要
    }
}

中身の有無によって挙動が変わります。

ポインター型

基本的に、GC管理下のオブジェクトのポインターを作るのは危険です。

そこで、C#では以下の条件を満たす型(非管理型(unmanaged type)と呼びます)でだけポインターを作ることを認めています

  • 参照型ではない
  • ジェネリックではない
  • 上記2条件を再帰的に満たす(フィールドに1つ含まない)

例えば、もし仮にこの条件を満たさない(GC管理下にある)型のポインターを作れたとします。 そうすると、以下のような問題のあるコードが書けてしまいます。 (そうならないように、赤線の部分をコンパイル エラーにしている。)

using System.Runtime.InteropServices;

// 参照型を含む構造体
struct Wrapper { object _obj; }

class ManagedPointer
{
    public unsafe void X()
    {
        // Wrapper みたいに内部的に参照型のフィールドを持っている型は、本来はポインター化できない
        // sizeof 取得も本来はできない

        // unmanaged なメモリを確保
        // AllocHGlobal で取得したメモリ領域は初期化されている保証がない
        // 実行するたびに違う値が入ってる
        var p = Marshal.AllocHGlobal(sizeof(Wrapper));
        Wrapper a = *(Wrapper*)p;

        // ここで GC が発生したとすると、
        // GC が TaskAwaiter 中の Task のフィールド(未初期化)を参照する
        // 未初期化(= 意味のないランダムな値)な参照先を見に行こうとして死ぬ

        Marshal.FreeHGlobal(p);
    }
}

こちらも、メンバーに参照型を含んでいるかどうかを追うのに、構造体の中身を追う必要があります。

再帰レイアウト

構造体の中にそれ自身の型のフィールドを持とうとすると、当然ですが無限再帰を起こします。 無限に再帰する構造体のレイアウトなんて決定できない(オーバーフローする)ので、当然禁止事項です。

struct Container<T>
{
    public T Item;
}

struct RecursiveLayout
{
    // 無限再帰するので、この構造体はレイアウトが確定できない
    Container<RecursiveLayout> _x;
}

再帰していないかどうかを調べるために、構造体の中身の情報が必要です。

privateフィールドを残していない問題

この、「構造体は、中身のprivateフィールドの情報も残さないとまずい」というのに気づいたのは、 参照アセンブリの仕組みを導入したのよりもちょっと後です。 リリースまでには気づいてなくて、リリース後に不具合報告を受けて気づいたようで。

PCLプロジェクトから参照しているいくつかの参照アセンブリが、構造体のprivateフィールドまで削除してしまっていて、問題を起こします。

ということで、本題に戻りますが、PCLプロジェクトでだけ起こせる問題の数々。 以下のコード、本来はコンパイル エラーになるべきですが、PCLではコンパイルできてしまします。

1つ目。確実な初期化に漏れるケース。

using System;

struct DefiniteAssignment
{
    // DateTimeOffset には中身があるはずなのに…
    DateTimeOffset _x;

    public DefiniteAssignment(int n) { } // PCL ではエラーにならない
}

2つ目。ポインター化できるかどうかの判定をミスるケース。

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

class ManagedPointer
{
    public unsafe void X()
    {
        // TaskAwaiter は内部的に Task クラスのフィールドを1個だけ持っている
        // 本来はポインター化できない
        var p = Marshal.AllocHGlobal(sizeof(TaskAwaiter));

        // PCL ではエラーにならない
        TaskAwaiter a = *(TaskAwaiter*)p;

        // ここで GC が発生したとするとまずい

        Marshal.FreeHGlobal(p);
    }
}

3つ目。無限再帰なレイアウトを作れてしまうケース。

struct Container<T>
{
    public T Item;
}

struct RecursiveLayout
{
    // 無限再帰するので、この構造体はレイアウトが確定できない
    Container<RecursiveLayout> _x; // PCL ではエラーにならない
}

どれも結構まずいんですが、今のところ、これがPCLではコンパイルできてしまっています。 DateTimeOffsetKeyValuePairTaskAwaiterなどの構造体で、PCLが参照している参照アセンブリでは中身がごっそり削られているのが原因。

この問題を踏む可能性

この問題ですが、根本的には「参照アセンブリを作るときに消しちゃいけないところまで消しすぎた」というのが原因なわけで、 参照しているもの次第で起こるかどうかが決まります。

問題が起きるケース:

  • PCL を使っていて、上記のDateTimeOffset などを参照する
  • 同様に、.NET Standard 向けのライブラリ プロジェクトでも、該当する型を参照すると問題が起きる

起きないケース:

  • アプリなど、実行アセンブリを直接参照しているもの
  • 実装アセンブリを直接提供しているライブラリなら問題が起きない
    • ValueTaskValueTupleは実装アセンブリしか提供していないので、こいつらでは問題は起きない

問題への対処(検討中)

とりあえず、どこの問題かというと参照アセンブリを作るツールになります。

今は構造体のすべてのprivateフィールドを削ってしまっている挙動を、以下のように変更する必要があります。 (フィールドをすべて残すのではなく、以下のルールにするのは参照アセンブリのサイズ削減のため。 C#コンパイラーが誤動作しないようにするにはこのルールで十分。)

  • 1つでも値型のフィールドを持っていれば、int型など適当な型のフィールドを1個だけ作って含める
  • 1つでも参照型のフィールドを持っていれば、object型のフィールドを1個だけ作って含める
  • ジェネリックな構造体の場合は、ジェネリック型引数で与えられた型のフィールドは消さずに残す

今は、C# コンパイラー自身が実装アセンブリリと同時に参照アセンブリを作る機能を持っているみたいなので、 基本的にはC# チームの仕事かも。 (昔からそうだったわけではなくて、割かし最近、そういう機能を持った。 それ以前は、オープンになっていないツールで、標準ライブラリの参照アセンブリ作りをしてた。)

とはいえ、現在問題を起こしている参照アセンブリとかを、ちゃんと治ったバージョンのツールで生成しなおして、 パッケージをNuGetサーバーに上げなおす作業は、たぶんC# チームの範疇外。

問題を起こす状況も限られているし、複数のチームが絡んでいるしで、ちょっと修正には時間が掛かりそうな雰囲気…

小ネタ string型のマーシャリング

$
0
0

数値や文字列の内部形式は、プログラミング言語ごとに違っています。プログラミング言語をまたいで値を受け渡しするには、その間に変換処理が必要になります。その変換処理のことをマーシャリング(marshalling: 整列する(特に、指揮官の指示で整列、集結、先導されるような意味あい))と言います。

無変換転送

といっても、変換処理はそれなりに重たい処理なので、異なるプログラミング言語間でも揃えられる限りには同じ形式を使って、そのまま値を渡せるようにしたくなるものです。C#では、Windows APIが使っている内部形式と揃えた形式にすることで、マーシャリング時の変換処理を極力減らしていたりします。

数値型は比較的簡単です。何せ、C#が動く環境は大体Little EndianのCPUですし、C#コンパイラーはアラインメントにも気を使った仕様になっていています。この辺りが一致しているなら、たいていの数値型は他のプログラミング言語にそのまま渡すことができます。こういう、そのまま渡せる型のことをblittable型というようです(blitはboundary block transferの略語から派生した「生データ転送する」という意味の単語)。

文字列のマーシャリング

問題は文字列です。文字列は、数値と同じくらい汎用的に使われるものですが、その内部形式は数値程単純ではありません。文字コードはどうなっているのかや、文字列の長さの管理などが、プログラミング言語ごとに異なります。

で、C#の文字列がどうなっているかというと、Build Insiderの記事で書きましたが、COMのBSTR型互換です。そして、BSTR型も、C言語やC++でよく使われるUTF-16のnull終端文字列互換です。Windows APIはCやC++で書かれていて、たいていがnull終端文字列なので、ネイティブ側がUTF-16 (wchar_t*)を使っている限り、実は、C#側から変換なしで文字列を渡すことができます。

変換なしでというか、ポインターがそのまま渡ります。例えば、以下のようなネイティブ コードがあったとします。受け取った文字列をすべて「a」の文字で上書きしてしまう関数です。

extern "C"
{
    // UTF-16 null終端文字列
    __declspec(dllexport) void __stdcall FillA16(wchar_t* str)
    {
        for (auto p = str; *p; p++)
        {
            *p = L'a';
        }
    }

    // ANSI null終端文字列
    __declspec(dllexport) void __stdcall FillA8(char* str)
    {
        for (auto p = str; *p; p++)
        {
            *p = 'a';
        }
    }
}

これを呼び出すC#コードは以下のようになります。

using System;
using System.Runtime.InteropServices;

class Program
{
    // 対 UTF-16。無変換で(ポインター渡しで)呼び出せる。
    // CharSetで指定している「Unicode」はUTF-16のこと。
    [DllImport("Win32Dll.dll", CharSet = CharSet.Unicode)]
    extern static void FillA16(string s);

    // 対 ASCII。変換が必要。
    [DllImport("Win32Dll.dll", CharSet = CharSet.Ansi)]
    extern static void FillA8(string s);

    public static void Main()
    {
        Console.WriteLine(GetValue());

        // 変換が必要な方。
        // コピーが書き換わるだけなので、s1 には影響なし。
        var s1 = "awsedrftgyhu";
        FillA8(s1);
        Console.WriteLine(s1); // awsedrftgyhu

        // ポインターで渡る方。
        // s2 はネイティブ コード側での書き換えの影響を受ける。
        var s2 = "awsedrftgyhu";
        FillA16(s2);
        Console.WriteLine(s2); // aaaaaaaaaaaa
    }
}

UTF-16なnull終端文字列に対してC#側から文字列を渡す場合、ポインター渡しになって、ネイティブ コード側での書き換えの影響を受けます。 一方で、相手がANSI文字列(char*)の場合には、変換処理が走って、別途メモリが確保されてコピーするので、C++側で書き換えた結果は元の文字列に影響しません。

補足: ANSIとUnicode

ちなみに、Windows的には、ANSI、Unicodeというのは以下の意味です。

ANSI:

  • 内部的にchar* (C++の1バイト文字列)
  • ANSIと言いつつ、ASCII互換でロケール依存の文字コードのこと
  • 要するに、日本語Windowsの場合はShift-JIS

Unicode:

  • 内部的にwchar_t* (C++の2バイト文字列)
  • UTF-16のこと
  • 昔(サロゲート ペアが生まれるまで)は、Unicode = UTF-16でした

小ネタ 引数の個数の上限

$
0
0

引数の個数に制限があること、ご存じでしょうか。 むやみに多くても実装上の無駄が大きかったりしますし、上限が決まっていたりします。

C#は意外と大きくて、最大で65536個まで行けます。要するに2バイト分。 ということで、以下のC#コードはコンパイル可能です。 1バイトで収まらない、0~256までの257個の引数。

class Program
{
    static void M(
int x0, int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, int x9, int x10, int x11, int x12, int x13, int x14, int x15,
int x16, int x17, int x18, int x19, int x20, int x21, int x22, int x23, int x24, int x25, int x26, int x27, int x28, int x29, int x30, int x31,
int x32, int x33, int x34, int x35, int x36, int x37, int x38, int x39, int x40, int x41, int x42, int x43, int x44, int x45, int x46, int x47,
int x48, int x49, int x50, int x51, int x52, int x53, int x54, int x55, int x56, int x57, int x58, int x59, int x60, int x61, int x62, int x63,
int x64, int x65, int x66, int x67, int x68, int x69, int x70, int x71, int x72, int x73, int x74, int x75, int x76, int x77, int x78, int x79,
int x80, int x81, int x82, int x83, int x84, int x85, int x86, int x87, int x88, int x89, int x90, int x91, int x92, int x93, int x94, int x95,
int x96, int x97, int x98, int x99, int x100, int x101, int x102, int x103, int x104, int x105, int x106, int x107, int x108, int x109, int x110, int x111,
int x112, int x113, int x114, int x115, int x116, int x117, int x118, int x119, int x120, int x121, int x122, int x123, int x124, int x125, int x126, int x127,
int x128, int x129, int x130, int x131, int x132, int x133, int x134, int x135, int x136, int x137, int x138, int x139, int x140, int x141, int x142, int x143,
int x144, int x145, int x146, int x147, int x148, int x149, int x150, int x151, int x152, int x153, int x154, int x155, int x156, int x157, int x158, int x159,
int x160, int x161, int x162, int x163, int x164, int x165, int x166, int x167, int x168, int x169, int x170, int x171, int x172, int x173, int x174, int x175,
int x176, int x177, int x178, int x179, int x180, int x181, int x182, int x183, int x184, int x185, int x186, int x187, int x188, int x189, int x190, int x191,
int x192, int x193, int x194, int x195, int x196, int x197, int x198, int x199, int x200, int x201, int x202, int x203, int x204, int x205, int x206, int x207,
int x208, int x209, int x210, int x211, int x212, int x213, int x214, int x215, int x216, int x217, int x218, int x219, int x220, int x221, int x222, int x223,
int x224, int x225, int x226, int x227, int x228, int x229, int x230, int x231, int x232, int x233, int x234, int x235, int x236, int x237, int x238, int x239,
int x240, int x241, int x242, int x243, int x244, int x245, int x246, int x247, int x248, int x249, int x250, int x251, int x252, int x253, int x254, int x255,
int x256
        )
    { }
}

確かJavaだと、256個までだったはずです。1バイト分。

こういう制限、Javaや.NETの場合、何によって制約されるかというと、中間コードの命令セットに依ります。例えば、.NETの場合だと、引数参照のために以下のような命令を持っています。

命令 op code 概要 命令サイズ
ldarg.0 02 最初の引数をスタックにロードする 1バイト
ldarg.1 03 2つ目の引数をスタックにロードする 1バイト
ldarg.2 04 3つ目の引数をスタックにロードする 1バイト
ldarg.3 05 4つ目の引数をスタックにロードする 1バイト
ldarg.s 0E <index> 1バイトのオペランドで指定したインデックスの引数をスタックにロードする 命令1バイト+オペランド1バイト
ldarg FE 09 <index> 2バイトのオペランドで指定したインデックスの引数をスタックにロードする 命令2バイト+オペランド2バイト

4つ目の引数まで(0~3番目)なら1バイトで参照できます。 4~255番目までなら2バイト、そして、それ以上になると4バイト必要になります。 というように、オペランドによってプログラム サイズがでかくならないように、よく使うものほど短く、そうでないものほど長くなるように、複数の命令が用意されています。

この場面で効いてくるJavaと.NETの最大の差は、中間コード(Javaの場合はbyte code、.NETの場合はILと呼ばれてるやつ)の命令長の差です。 .NETは可変長になっていて、多くの命令が1バイトですが、いくつか2バイト命令を持っています。 上記のldarg (2バイト オペランドの方)もその1つで、めったに使わないであろう命令に2バイトのコードを割り当てています。

一方、Javaのbyte codeは1バイト固定長の命令セットになっています。 使える命令は最大で256個ですし、無駄な命令はあまり入れたくありません。

まあ、引数の数が257個以上になるというのはほとんどないでしょう… と締めたいところですが、ごくまれに、「機械生成で作ったコードで257個超えてJavaでコンパイル エラーになった」なんていう恐ろしいことを言いだす人も見かけるので侮れません。 そういう人が実際にいたから、.NETはldarg命令を用意したんでしょうかね…

小ネタ C# と他の言語との差というと

$
0
0

C#で、「他の言語との差というと」とか「他の言語から来たばかりの人が書きがちなコード」みたいなことを聞かれた場合、まず何が思い浮かぶでしょう。

C#に馴れちゃってる人だと、LINQとかasync/awaitとかの機能が最初に浮かんだりします。でも、この辺りは「大きな機能」過ぎて、知ってるか知らないかの二択、1度知れば検索してすぐに解説が出てくる類で、かえって問題にならないという印象。 案外、困るのはもうちょっと細かい部分じゃないかと思います。

みたいなのが今日の話題。

辞書(ハッシュテーブル)の列挙

Dictionary<TKey, TValue>の列挙を、キーも値も両方使うのに、Keysを使ってやろうとする人が結構いるらしいという話を聞きます。要するに以下のような書き方。

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var dic = new Dictionary<string, int>
        {
            { "one", 1 },
            { "two", 2 },
            { "three", 3 },
        };

        foreach (var key in dic.Keys)
        {
            var value = dic[key];
            Console.WriteLine($"{key} => {value}");
        }
    }
}

C#のDictionaryはキーと値をまとめて列挙できる(IDictionary<TKey, TValue>インターフェイスがIEnumerable<KeyValuePair<TKey, TValue>>インターフェイスから派生している)ので、以下のように書けます。

        foreach (var x in dic)
        {
            Console.WriteLine($"{x.Key} => {x.Value}");
        }

得られる結果が一緒だからどちらでもいいと思うかもしれないですけど、パフォーマンスが結構違います。この手のコレクション(他の言語で言うところのmapとかHashtable)のインデクサー アクセスはそこそこなコストです。 この例みたいなのだと、Dictionary内の要素の数にもよりますが、前者のKeys越しの方が2~3倍くらい遅いです。

文字列中の文字の列挙

stringIEnumerable<char>なのも案外気付いていない人がいるとか。

var s = "aáαあ亜😀";

for (int i = 0; i < s.Length; i++)
{
    var c = s[i];
    Console.WriteLine(c);
}

C#だと大体はforeachで列挙します。

foreach (var c in s)
{
    Console.WriteLine(c);
}

というか、文字列からインデックス使って「N文字目」を取れると思うなよ

上記の例でも、foreachの書き方含め、絵文字が2文字に割れちゃって正しく文字コードを取れません。 C#で正しくサロゲートペアを正しく扱うのはいまだにちょっと面倒なんですが… いずれ、以下のように書けるようになるはずです。

using System;
using System.Text.Utf8;

class Program
{
    static void Main()
    {
        var si = new Utf8String("aáαあ亜😀");

        foreach (var c in si.CodePoints)
        {
            Console.WriteLine(c);
        }
    }
}

逆に、このUtf8Stringからは、インデックスを使って「N文字目」を取る手段はなくなっています。

Format("{0} {0}", x)

C# 6でinterpolationが入った今、あんまり使うものではなくなりましたが、string.Formatの呼び方に関して。

interpolation でも書けない書き方なんですけども、以下のように、同じインデックスを複数回使う書き方ができたりします。

Console.WriteLine("({0} + {1}) × ({0} - {1}) = {0}^2 - {1}^2", "x", "y");
// (x + y) × (x - y) = x^2 - y^2

わざわざ、以下のような書き方をしてしまう人をちらほら見かけるとか

Console.WriteLine("({0} + {1}) × ({2} - {3}) = {4}^2 - {5}^2", "x", "y", "x", "y", "x", "y");

printfだとこんな感じで書いてましたもんね…

文字列の + 演算

以下のようなコードをC#で書くと、結果はどうなるでしょう。

string s1 = "abc";
object s2 = null;
Console.WriteLine(s1 + s2);

選択肢:

  1. ぬるぽ(NullReferenceException発生)
  2. abcが表示される
  3. abcnullが表示される

答えは2番です。C#で、nullを文字列連結すると、空文字扱いになります。

Javaは3番になるんでしたっけ?nullが"null"に化けるっていう。

言われてみると、言語ごとに挙動が微妙に違ってちょっとめんどくさいですね、これ。

どっちもどっちというか、文字列連結に+演算子を使うって発想がまず、本当によかったのかどうかという疑問があります…

C#文化では、ガイドラインとして「演算子は、組み込み型のものと全然違う用途でオーバーロードするな」というものがあります。 となると、「組み込み型の+は足し算だろ、足し算として使えよ」と言われても仕方がなく。 「文字列連結は足し算といえるか」という命題ではあるんですが。 連結の結果、文字列長が足し算になるので足し算的な何かと言えなくもないですけど、きわどい。

ストリームの読み書きにシフト演算子(<<)を使われるよりは幾分かマシですけど、 文字列に対する+もやめといた方がよかったんじゃないかなぁ… 「顧客が本当に欲しかったものはinterpolationだった」説もありますし。

if (x)

これはC言語方面から来た人がたまーにやらかして、ほんとみんな迷惑するやつなんですが… operator trueとかに変な実装を入れてしまうことがあります。

やらかす人は「null関係の演算子」の回で話した 「nullじゃないのにx == nullがtrueになる」っていうコードとセットでやらかすんですが…

以下のようなコード。

class MyObject : IDisposable
{
    bool _isDisposed;

    public void Dispose()
    {
        // Dispose 後、もうこのオブジェクトは無効
        _isDisposed = true;
    }

    // 無効だったら if (x) { } で {} の中を通らなくする
    public static bool operator true(MyObject obj) => !obj._isDisposed;
    public static bool operator false(MyObject obj) => obj._isDisposed;
}

使う側は以下のような感じ。

static void M(MyObject obj)
{
    Console.WriteLine("----");
    if (obj) Console.WriteLine("有効");
}

C言語だとif (x)って結構書いてたましたもんね… boolって概念を持っていなくて、0以外の値は全てtrue扱い(nullは0)で。 間違えて意図しない条件をifの中に書いてしまうので良くないと言われています。

良くないから、C#ではわざわざ書けなくしたものでして… それをぶり返すようなoperatorを書かれると結構困惑します。

小ネタ Concurrent コレクション

$
0
0

.NET 4以来、System.Collections.Concurrent以下に、 Concurrentなコレクションがいくつか追加されました。

Concurrent、英単語の意味としては「同時に起こる」という意味の形容詞。 プログラミングにおいては、「複数のプログラムやスレッドから同時にアクセスされる」という意味で使われ、 「並行」とか「同時実行」とか訳されます。 たいてい、「Concurrentなんとか」みたいな名前のものは「同時実行があっても問題が起きない」という意味になります。

ただし、「問題を起こさない」って言ってもいろいろな意味があって、それぞれのコレクションの性質をちゃんとわかっておかないと困ったりします。 (.NET のSystem.Collections.Concurrentに限らず、たいていのプログラミング言語のたいていのライブラリで、Concurrentと名の付くものは同様の注意が必要です。)

ということで、今日はConcurrentDictionaryGetOrAddメソッドを例にとって挙動をちょっと説明。

GetOrAdd

こいつ: GetOrAdd(TKey key, Func<TKey, TValue>) valueFactory

名前通り、キーに応じた値がすでにあればその値を返し、なければ valueFactory を呼んで、新しい値を作って辞書に登録しつつ、その作った値を返します。

話を簡単にするために、まずちょっと、同時実行が必要ない状況で例を出しますが、以下のような挙動になります。

using System;
using System.Collections.Concurrent;

class Program
{
    static void Main(string[] args)
    {
        const int theKey = 1;
        var d = new ConcurrentDictionary<int, string>();

        // まず、GetOrAdd の同時実行が起こらない場合を見てみる
        // 普通の逐次実行なので、同時実行にはならない
        for (int i = 0; i < 4; i++)
        {
            var item = d.GetOrAdd(theKey, key =>
            {
                // インスタンス新規作成
                // 単一のキーでアクセスしているので1回限り
                Console.WriteLine($"Add: {i}");
                return i.ToString();
            });

            // 同じインスタンスが返ってきているか確認
            Console.WriteLine($"Get: {item}");
        }
    }
}
Add: 0
Get: 0
Get: 0
Get: 0
Get: 0

この例では、同じキーで何度も GetOrAdd を呼んでいます。 値の生成($"Add: {i}"と表示される部分)は最初の1回でしか通りません。

並列動作

このforループを並列化することを考えます。

ConcurrentDictionary の必要性

単に同時実行で問題を起こさないようにするなら、わざわざConcurrentDictionaryなんていう新しいクラスを作らなくても、 lockステートメントを掛ければ済む話です。

Concurrentを名乗らない普通のDictionaryを使って、 自前でlockを掛けるのであれば、例えば以下のように書けばいいでしょう。

static class DictionaryExtensions
{
    public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> d, TKey key, Func<TKey, TValue> valueFactory)
    {
        lock (d)
        {
            TValue value;
            if (!d.TryGetValue(key, out value))
            {
                value = valueFactory(key);
                d[key] = value;
            }
            return value;
        }
    }
}

このコードの何が嫌かというと、lock範囲が広すぎること。

  • Getにもlockが掛かる。新規追加(Add)の頻度が低い時に完全に無駄
  • Add のときに、valueFactory呼び出し中にもずっとlockが掛かっていて、valueFactoryの中身次第ではlock時間が長くなりすぎる

lockは、意外と重たい処理です。可能な限り避けて、可能な限り短くする必要があります。

ConcurrentDictionaryは、lock範囲を極力小さくすることで、パフォーマンス向上を図っているクラスです。

ConcurrentDictionaryの癖

ただし、ConcurrentDictionaryGetOrAddには少々癖があります。

ドキュメントをちゃんと読むと書いてあるんですが、

  • valueFactoryは複数回呼ばれる可能性があります
  • 返す値・辞書内に格納する値は必ず1つであることが保証されています

という挙動。

その結果、最初にあげた例で、forループをParallel.Forに変えて並列化すると、以下のような挙動をします。

using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        const int theKey = 1;
        var d = new ConcurrentDictionary<int, string>();

        // 並列動作
        // 並列なので、ループの中身が複数のスレッドで同時に動くことがある
        Parallel.For(0, 4, i =>
        {
            var item = d.GetOrAdd(theKey, key =>
            {
                // 同時に来られると、ここは複数回動く可能性がある
                Console.WriteLine($"Add: {i}");
                return i.ToString();
            });

            // Add が複数回動いても、Get で帰ってくる値は必ず単一の保証あり
            Console.WriteLine($"Get: {item}");
        });
    }
}

実行する環境によって/実行するたびに結果は異なりますが、一例としては以下のような実行結果になります。

Add: 0
Add: 3
Get: 0
Add: 1
Get: 0
Add: 2
Get: 0
Get: 0

(この環境では)Addは4回動いています。 しかし、戻り値として返っているのはそのうち1つだけで、Getのところに表示されている値は全部同じです。

癖の回避: Lazyとの組み合わせ

lockを減らすためとはいえ、ちょっと癖のある挙動です。この癖を回避したければ一工夫要ります。 その工夫として、別途、Lazyクラス(System名前空間)と組み合わせる方法があります。

以下のような書き方をします。

using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        const int theKey = 1;
        var d = new ConcurrentDictionary<int, Lazy<string>>(); // 値を Lazy<string> に変える

        // 並列動作
        // 並列なので、ループの中身が複数のスレッドで同時に動くことがある
        Parallel.For(0, 4, i =>
        {
            var lazy = d.GetOrAdd(theKey, key => new Lazy<string>(() =>
            {
                // 複数個の Lazy インスタンスが作られることはあるけども、
                // Lazy が作られただけでは valueFactory は呼ばれない
                Console.WriteLine($"Add: {i}");
                return i.ToString();
            }));

            // lazy 自体は単一のインスタンスが返る保証あり

            // この時点で初めて Add: の行が呼ばれる
            // Lazy のデフォルトの挙動では、valueFactory が呼ばれるのは1回限りの保証あり
            var item = lazy.Value;

            Console.WriteLine($"Get: {item}");
        });
    }
}

結果は以下のようになります。

Add: 0
Get: 0
Get: 0
Get: 0
Get: 0

値は0である保証はなくて、Add: 1とかAdd: 2が表示されることもありますが、少なくとも

  • Add:の行が表示されるのは1回限り
  • Add:の行とGet:の行で表示されている値は同じ

という保証はされています。

これで、GetOrAdd全体をlockするよりはだいぶパフォーマンスのよいコードになります。 特に、Addがほとんどなく、Get頻度が高い場合にはかなり顕著な差になるでしょう。


discards

$
0
0

書いた。

ということで、今日も「小ネタ」休みで「C#7思い出話」の方を書くことにします。

このページのタイトル

このページのタイトルはかなり悩んだ… 実質的にはdiscards(_を使った値の破棄)の話なんですけども。 discards単体だと入れる場所に悩み。

discardsを書ける場所はどこかと考えたら、「変数宣言する場所」なんですよね。 で、「将来的には変数宣言式になるはずだし…」とか、 「というかむしろ、先にアイディアがあったのは変数宣言式で、それを細切れで実装することになったのがC# 7の今の姿だし…」とか、 「とはいえ、C# 7ではまだ入らない機能をタイトルにつける?…」とか、 悩みましたが最終的にはこの構成になりました。

RCから変更ないといったな

14日のブログで書いていますが

11月のリリース候補版の時点でC# 7の全機能そろったといったな。あれは嘘だ。

今のC#って結構、「パターン マッチングという大きな目標を少しずつ実装していってる」って感じなんですが、 そこで迷うのが「どこまでをC# 7としてリリースするのか」という区切り。 当初は、C# 7に入ると言っていた機能、もっと少なかったんですよね。 気が付いたらだいぶ「あっ、それもC# 7に入れてくれるんだ!」みたいな感じに。

なんか、Visual Studio 2017自体、リリース スケジュールが伸びてる感じがするなぁ(あくまで個人の感想です)。 遅れる原因になりそうなものというと、主に.NET Coreがらみのツール類がネックになっていそうな予感がちらほら。

そして、最後の最後まで「まだ未定」になってたのがこのdiscards。 まあ、「既存コードの破壊的変更になりかねない機能」ではあったんで、最後の最後まで悩んだんじゃないかと思います。

wildcards から discards に

なんか、最近名前が変わったんですよね、こいつ。

元々の呼び名は、「どんなパターンにでもマッチするもの」という意味でwildcards(万能札)でした。 関数型言語なんかでは、この何にでもマッチするものを「ワイルドカード パターン」とか呼んでいるので、そこから来ています。 でも、万能と言いつつ、C# 7のやつは(int x, int _) = tuple;みたいな書き方ができます。 int型しか受け付けない、ただ単に値を無視したいだけの_なわけです。 全然ワイルドじゃない。

ってことで、なんかこじゃれたアイディアが降りてきたのがdiscards(破棄)。 discardsって、破棄の他にカード ゲーム用語で「捨て札」の意味があります。 というか、cardってついてることからわかる通り、むしろそっちが原義。 wildcardsからdiscardsへの、カード ゲーム用語由来の単語同士だし、 語感も似ててなんかよさそう! みたいな感じで気が付いたら呼び名が変わっていました。

* から _ に

discards用にどの記号を使うかも結構最近まで悩んでたんですよね。 最終決定したのは、以下のブログを書いた頃。

このブログでも書いている通り、*?か、それとも_にするかで悩んでいました。

そのリンク先のデザイン ミーティング議事録によれば、 決定したのは10月25日。 Visual Studio 2017のリリース候補版が出たのが11月16日なので、その高々2・3週間前にそういう決定しますか… という感じで。

まあ、間違いなくC# 7で一番の難産機能。 なので、個人的にはもしかしたらC# 7に入らないかなぁとか思ってたんですけどね。 なんとか間に合うようです。

小ネタ 小さなオブジェクトのスタック割り当て

$
0
0

今回は、C#や.NETの現在ある機能でも、近い将来入りそうな機能でもないんですが、ちょっとした最適化の話。小ネタというか、いつものピックアップRoslynの亜種というか。「Javaでやってるんだし.NETでも」的な要望にし対して、.NETでは事情が変わるよ、というネタ紹介。

小さくて、かつ、短い期間使われないオブジェクトは、たとえ参照型(通常はヒープ上にメモリ確保)であってもスタック上にメモリ確保してはどうかという案があったりします。

例えば、以下のようなコードを考えます。非常に小さい型Distanceがあって、ループの中でそのインスタンスがnewされています。

using System;
using System.Linq;

// ただの double なんだけど、次元をはっきりさせたいために専用の型を作る的なやつ
class Distance
{
    public double Value { get; }
    public Distance(double value) { Value = value; }
}

class Program
{
    static void Main()
    {
        var r = new Random();
        var segments = Enumerable.Range(0, 1000).Select(_ => new Distance(r.NextDouble())).ToArray();

        var sum = new Distance(0);
        foreach (var s in segments)
        {
            // ループの中で new されるのがまずい。
            // ヒープ確保が頻繁すぎる。
            sum = new Distance(sum.Value + s.Value);
        }

        Console.WriteLine($"{sum.Value}");
    }
}

Javaでは起こりうる問題で、この、ループの中のnewで、オブジェクトをヒープではなくスタック上に確保できれば結構なパフォーマンス改善が見込めます。で、そのためコード解析手法(Escape Analysisと言います)は論文になっているし、実際、Java 6で採用されているみたいです。

  • Java HotSpot™ Virtual Machine Performance Enhancements: Escape Analysis

ということで、この最適化は.NETでも有効かどうか。

とりあえず、coreclrチームの中の人曰く「気づいているし、議論もしている。real-worldな事例を挙げてもらえると優先度が上がるかも。人工的なサンプルであれば持っている。」だそうです。

まあ、.NETだとそんな頑張らなくても簡単な回避策があるんで。上記の例で言うと、Distanceクラスを構造体に変えるだけ。小さくて、かつ、短い期間しか使われないものは構造体にしますからね、最初から。なので、よっぽど大きなインパクトがあるreal-world事例がないと動いてもらえないかも、という感じです。

一応、上記提案ページで挙がった「事例」だと、以下のようなものがあります。

  1. enumerator (主にLINQ)
  2. 文字列処理
  3. immutableデータの構築
  4. new FileInfo(path).Existsみたいなの

どうかなぁ…これ…

1のLINQに関しては、そもそもEscape Analysisの対象にできないみたいです。メソッドをまたぐとダメ。インライン展開が掛からない限りは解析できないそうですが、LINQはインライン展開される類の処理ではないので無理。

2の文字列処理に関しては、Utf8Stringのような、低アロケーションな文字列型が開発中で、これが入れば負担が激減するかもしれません。 現状のstring型に対しては効き目があっても、将来的には要らなかったということになる可能性が高いです。

3は、多くが「構造体にすれば解決」になる気もします。

ってことで、この事例の中だと4くらいですかね、効き目がありそうなのは。 Escape Analysisをするにもオーバーヘッドは結構あるわけで、ちょっとメリットを得にくそう。

小ネタ 並列化

$
0
0

よく目にする話題だと思いますが、ここ10数年くらい、CPUの性能は高クロック動作化ではなく、並列化によって向上しています。 CPU律速になるような計算処理は、ガチガチに最適化するなら並列処理を考える必要が出てきます。

並列化によって高速化しやすい計算の例として、浮動小数点数のデータ列の積和演算を考えてみます。 例えば、以下のようなコードになります。

private static float SingleThreadScalar(float[] x, float[] y)
{
    var prod = 0f;

    for (int i = 0; i < N; i++)
        prod += x[i] * y[i];

    return prod;
}

並列化のレイヤー

まあ、一口に並列化と言っても、いくつかの手段があります。

  • 分散コンピューティング
  • CPU間並列
  • CPU内並列

分散コンピューティング

1つ目は、複数台のマシンに処理を分散する方法。

分散コンピューティング

要はビッグデータ(big data)とかHPC (high perfomance computing)とか言われている分野です。 どういうコンピューターを、どういうネットワークでつなげて、どう大規模に運用していくか、みたいな個人ではどうにもならないノウハウが必要なので、 並列処理コードを書くというよりは、大手クラウド業者から提供されているサービスを使って計算するという感じになります。

今回さらっと試して見せれるような面白いデータを持っているわけでもないので、今日はこのレイヤーには触れません。

CPU間並列

2つ目は、複数のCPUで一斉に計算する方法。 1台のマザーボードに複数のCPUを刺して使ったりするやつです(マルチCPU)。 あるいは最近のCPUであれば、同じ機能を有した「コア」が、1つのチップ内に複数並んで配置されていて、OSから見ると複数のCPUが刺さっているように見えます(マルチコア)。

また、ハイバースレッディング(hyper-threading)なんていう手法もあります。 これは、あるCPUコア内で、内部の演算器で暇になっているやつがある場合に、仮想的にもう1個CPUがあるように見せかけて暇な演算器を活用できるようにする仕組みです。

いずれにしても、それぞれ独立したCPUが、1つのマザーボードやメインメモリを共有して動いています。

マルチコア/マルチCPU

それぞれが独立して動いているので、CPUコア間での同期はそれなりの負担になります。 分散コンピューティングと比べると微々たるものですが、CPU内部で完結した処理と比べるとかなり遅いです。

複数のCPUを使って並列に処理するためには、C#だとスレッドを使います。 と言っても、Threadクラス(System.Threading名前空間)を直接使うことはあまりなくて、 TaskクラスやParallelクラス(System.Threading.Tasks名前空間)を使います。

積和演算くらいの小さな処理をParallelクラスを使って高速化するのは難しいようです。 そこで、ここではTaskクラスを使った例を挙げます。 以下のような書き方になります。

private static float MultiThreadScalar(float[] x, float[] y)
{
    var windowSize = N / NumWorkerThread;

    var prod = 0f;

    var partialProds = Task.WhenAll(
        Enumerable.Range(0, NumWorkerThread)
        .Select(n => Task.Run(() =>
        {
            var local = 0f;
            for (int i = n * windowSize; i < (n + 1) * windowSize; i++)
                local += x[i] * y[i];
            return local;
        }))
        ).GetAwaiter().GetResult();
    prod = 0f;
    for (int i = 0; i < partialProds.Length; i++)
        prod += partialProds[i];
    return prod;
}

NumWorkerThreadは、CPUのコア数にそろえておくのが一番効率がいいと言われています。 例えば、NumWorkerThread = Environment.ProcessorCountで取得できます。

この例では、コア数分にループを分割して、それぞれのコアで個別に積和を求めて、 最後にそれぞれの結果を足しています。 こういう、それぞれ個別に計算して最後に集計できるものでないと、マルチコアを活用した高速化はなかなかできません。

CPU内並列

1つのCPUコアの内部でも並列処理があります。 SIMD(Single Instruction Multiple Data)命令と呼ばれていて、 複数のデータをまとめて格納できるレジスターや、 まとめて計算できる命令を持っています。

SIMD演算

SIMD命令を活用できるかどうかは、コンパイラーがどのくらい頑張っているか次第ではあります。 一応、プログラマーが意図的に、特定の場所でSIMD命令が使われるように指示できるライブラリも用意されている場合があります。

C#では、.NET Framework 4.6でSIMD命令対応がありました。 System.Numerics.Vectorsというパッケージを参照して、 その中にあるVector構造体を使うと.NET Framework 4.6以降のJIT (RyuJIT)は、SIMD命令に置き換えて最適化してくれます。

例えば、以下のようなコードを書いたとします。 2つの浮動小数点数データ列の積和を行っています。 Vector4.Dotは、4つの浮動小数点数をまとめたもの(Vector4)の内積(積和演算)です。

private static float SingleThreadVector(Vector4[] vx, Vector4[] vy)
{
    var prod = 0f;
    prod = 0f;
    for (int i = 0; i < N / 4; i++)
        prod += Vector4.Dot(vx[i], vy[i]);
    return prod;
}

これを、x64向けにビルドして実行すると、JIT結果は以下のようなネイティブ コードになります(上記コードのループの中身に相当する部分を抜粋)。

vmovupd xmm1,xmmword ptr [rdi+r8+10h] vdpps xmm0,xmm0,xmm1,0F1h vaddss xmm0,xmm0,xmm6 vmovaps xmm6,xmm0

xmm0xmm1が、複数のデータをまとめて格納できるレジスターの名前です。 vdppsが4つのfloat(32ビット浮動小数点数)の積和演算、 vaddssが加算、vmovupdvmovapsがレジスター間のデータ移動です。

ちなみに、それ以前の.NET Frameworkなど、対応していないプラットフォームで実行すると、SIMDではない普通の命令に展開されます(早くはならないけども、Vectorを使わない場合と比べてそんなに遅くなるわけでもない)。

同じコードを、Any CPU向けにビルドして実行すると、以下のような命令に変わります。

fmul dword ptr [edi+eax*4+8] faddp st(1),st

こちらは単なる浮動小数点数向けの命令で、1度に1データずつ掛けて、足してを行っています。

並列化の結果

最後に、これらの並列化でどのくらいの高速化を図れるかを示しておきます。

TaskVectorも使って、CPU間並列もCPU内並列も使った例も挙げておきます。

private static float MultiThreadVector(Vector4[] vx, Vector4[] vy)
{
    var windowSize = N / NumWorkerThread;

    var prod = 0f;
    var partialProds = Task.WhenAll(
        Enumerable.Range(0, NumWorkerThread)
        .Select(n => Task.Factory.StartNew(() =>
        {
            var local = 0f;
            for (int i = n * windowSize / 4; i < (n + 1) * windowSize / 4; i++)
                local += Vector4.Dot(vx[i], vy[i]);
            return local;
        }))
        ).GetAwaiter().GetResult();
    prod = 0f;
    for (int i = 0; i < partialProds.Length; i++)
        prod += partialProds[i];
    return prod;
}

計測コードの全体はGistに置いてあります。

僕の環境(Core i7-4790(4コア8スレッドなCPU)のデスクトップPC)で、それぞれを500回ずつループするのにかかった時間は以下の通りになります(単位は秒)。

シングルスレッド マルチスレッド
SIMD なし 2.0295843 0.9619755
SIMD あり 1.1649900 0.8847980

ループやスレッド起動のオーバーヘッドなどもあるのできっちりコア数やSIMD並列度分の倍率にはなりませんが、2倍以上には高速化できます。 ループの中身の処理がもう少し大きければ、もうちょっと良い倍率で高速化されるはずです。

小ネタ do-while

$
0
0

do-whileステートメントとか使っていますか?

あんまり実際に使われているコードを実務で見たことはなく。 使われていないキーワードランキング的にもdoは使われてない方から数えて27位。 もしかしたら使われないどころか存在を忘れてる人すらいるんじゃないかというこの文法。

「使ってる?」とか人に聞いてみたところ、 「初心者の頃にちょっと」「もしかしたら初心者ほど使ってるかも」とかいう回答も得られたり。 確かに、入門書とか(うちのサイト含めて)には書かれてますもんね。書かれてば使うか。

たぶん、徐々に、以下のように while (true) になっていくのかなぁとか。 まあ、そもそも、ループの大半が foreach ですけど。do-while どころか while もそこそこレア。

while (true)
{
    // 前にも書きたいことあるし、
    if (条件) break;
    // 後ろにも書きたいことある
}
while (true)
{
    // というか、メソッド抽出して return する方が多いかも
    if (条件) return ...;
}

さて、そんなdo-whileがなぜあるか、ですが。 確かにdo-whileの「最低1回は実行したい」という要件はそもそも出番が少ない上に、やろうと思えばwhileだけで書けます。 要するに、レアケースのために専用構文がある意味はあったのかという問題が。

ご存知の通り、この構文はC言語からあります。 「その当時ならば使ったのか」と言われると、やっぱりそんなに使いはしなかったと思うんですけど…

実は、生成されるコードがwhileよりもdo-whileの方が短いんですよね。 ということで、おそらく、do-whileがあるのは、そういうパフォーマンス上の理由かなぁと思います。

どういうことかというと、例えば、do-whileは以下のように展開されます。

static void DoWhile(int x)
{
    do
    {
        --x;
    } while (x > 0);
}
// ↓
static void DoWhileCompiled(int x)
{
    BEGIN_DO_WHILE:;
    --x;
    if (x > 0) goto BEGIN_DO_WHILE;
}

これに対して、whileだと以下のように、goto (IL 的には br 命令。x64 系 CPU のネイティブコード的には jmp 命令)が1個多く展開されたりします。

static void While(int x)
{
    while (x > 0)
    {
        --x;
    }
}
// ↓
static void WhileCompiled(int x)
{
    goto END_WHILE;// この goto がいまいち好きになれない
    BEGIN_WHILE:;
    --x;
    END_WHILE:;
    if (x > 0) goto BEGIN_WHILE;
}

この、whiledo-whileを使ったものと、展開結果のgotoを使ったものが本当に一緒になるかも確認してみましょう。 上記コードをコンパイルして、ildasmを掛けた結果は以下の通りです。 上がwhile、下がdo-while。 左が展開前、右が展開後。

コンパイル結果

ついでに、do-whileの方が数バイト小さくなることもわかります。 ここではILしか出していませんけども、たいていのCPUで、ネイティブ コードでもやっぱりdo-whileの方が短くなると思います。

とはいえ、この微々たる要件のためにいまだにこの構文が必要かと言われると微妙なラインですかね。

小ネタ atan2

$
0
0

今日は、MathクラスのAtan2メソッドの話。あんまり数学がわかってない人だと、「tanの逆関数」なのにどうして2引数あるのかとか、AtanAtan2で戻り値の範囲が違う(前者が-90度~90度、後者が-180度から180度)のが不思議だったりするみたいですね。

大元をたどるとatan2はFORTRANとかC言語とかの頃からあって、ちょっと調べれる範囲でもFORTRAN 77の時点であったらしいので、少なくとも1977年より前まで遡ります。なのでC#の小ネタというよりはプログラミング全般の小ネタだったり、むしろ、単に数学の話だったり。

x軸とのなす角

単純化のために、まずは半径1の円周上の点(x, y)の1点だけを考えて、原点からこの点までの線分と、x軸がなす角を考えます。以下の絵のような感じ。

x軸と線分のなす角

この絵を見ての通り、以下の条件を満たすθを計算することになります。

x=cosθ

y=sinθ

「逆三角関数を使えば簡単」と思うかもしれませんが、それだと半分だけ正解。θ=cos-1xだと、x軸を中心に左右どちら周りなのかがわからなくなります。

acosで求める角度

同様に、θ=sin-1yだとy軸中心の折り返しがわからないです。さらにいうと、以下の式もダメ。y/xしている時点でわかると思いますが、符号が消えます。x, yともに正の場合と、共に負の場合が同じ値になってしまうので、やっぱり半円分しか計算できません。

yx=tanθ

θ=tan-1yx

ということで、角度θを360度ちゃんと求めるためには、x, y、すなわち、cos, sinの両方の値が必要です。実際、Atan2は大体以下のような感じの分岐をしています。

static double Atan2(double y, double x)
{
    var z = Math.Atan(Math.Abs(y / x));
    if (x > 0)
    {
        if (y > 0) return z;
        else return -z;
    }
    else
    {
        if (y > 0) return Math.PI - z;
        else return z - Math.PI;
    }
    // ほんとは0, infinity, NaN の場合分けあり
}

2点のなす角

ここからは完全におまけ。ちょっとした数学の話。2点だとどうでしょう。(x1, y1)と原点と(x2, y2)のなす角。

2点のなす角

これも、正弦定理・余弦定理からの変形で、以下のような式が成り立ちます。

cosθ=x1x2+y1y2

sinθ=x1y2-x2y1

内積がcosで、面積(交代積)がsin。これらをAtan2(sin, cos)の順で与えれば角度θが求まります。

オイラーの公式

もう1つおまけ。 「オイラーは数多の公式を残しすぎてどの公式だよ」という話もあるんですが、ここで話すのは複素解析におけるオイラーの公式です。有名なあれ。 eiθ=cosθ+isinθ

これを逆に、cosθ+isinθ=x+iyだと考えた場合、両辺の対数を取ることで、

iθ=logx+iy

θ=-ilogx+iy

となります。 ここで、Atan2の使い道を思い出してみます。θ=Atan2(y, x)なわけで、

Atan2y, x=-ilogx+iy

です。Atan2は、絶対値が1の複素数に対する対数関数と関連していたりします(指数関数が三角関数と関連しているんだから、対数関数(指数関数の逆関数)が逆三角関数と関連しているのも当然の話です)。

てことで、実のところ、Atan2って、「複素対数関数」だと言っても過言ではなかったり。 実装都合の変な関数ではなくて、割かし「数学的にあり得る関数」です。

小ネタ 正規分布の丸み

$
0
0

今日もたいがい、数学の話です。 一瞬、「動きにコクが出る」って表現で話題になったあれの話。

そっかー、アニメーション付ける人に一言で説明するには「コク」って言葉になるのかー… という衝撃は結構ありますが、まあ、乱数をいくつか足すと丸みが出るというの自体は事実。

正規分布

ちゃんとした数学的な説明をすると、

  • 中心極限定理によって、独立な乱数を数多く足せば足すほど正規分布に近づく
  • 自然界は多数の独立な乱雑さが重なってできてるので結構な頻度で正規分布が出てくる
  • 正規分布で作った図形は丸みがかってる(というか、完全に真円・真球を作れる)

みたいな話です。

C#関係ない… こころなし程度にC#に関係している点というと、「Math.NET っていう数学ライブラリがあるよ」という話。

例: 2次元上の点の分布を作る

「丸み」の例として、2次元上の点(x, y)を乱数を使って作ることを考えます。

以降のサンプル コードでは、Math.NET Numericsを使って、 以下のusingディレクティブがあるものとして説明します。

using MathNet.Numerics.Distributions;
using static System.Math;

例えば、x, yそれぞれに対して、一様乱数(一定の範囲内で、全ての値が均等な確率で出現する乱数)を使って点を作ると、完全に真四角になります。

var rand = new ContinuousUniform(-1, 1);
var p = (rand.Sample(), rand.Sample());

このコードで1万点ほどプロットすると、以下のようになります。

一様乱数でx, yを生成した結果

で、四角いのはあまりに不自然なので、これを丸くしたいです。 割と軽い計算量で実現する方法があって、 それが冒頭で言った、「乱数をいくつか足す」というやつです。

乱数を足すために、以下のようなメソッドを用意してみます。 n個足して平均を取るだけの関数です。

static double Mean(IContinuousDistribution d, int n)
    => Enumerable.Range(0, n).Select(_ => d.Sample()).Sum() / n;

これを使って、n = 2~5に対して、以下のコードで点を生成してみます。

var rand = new ContinuousUniform(-1, 1);
var p = (Mean(rand, n), Mean(rand, n));

先ほどと同様、1万点ずつプロットした結果を以下に示します。 上から順に、n = 2~5です。

一様乱数を2個足してx, yを生成した結果 一様乱数を3個足してx, yを生成した結果 一様乱数を4個足してx, yを生成した結果 一様乱数を5個足してx, yを生成した結果

n = 5 くらいまでくると、だいぶ丸くなります。

ちなみに、正規分布乱数を使うと、真円になります。

var rand = new Normal(-1, 1);
var p = (rand.Sample(), rand.Sample());

正規分布乱数でx, yを生成した結果

要するに、以下のような感じ。

  • 一様乱数を足していくと、数が多いほど正規分布乱数に近づく
  • 5個も足すと結構いい感じに丸くなる
  • 正規分布乱数は真円(理論上、本当に完全に円)

乱数の和

まあ、一様乱数を複数足すと正規分布乱数になるってのは、 結構難しい話になります。

確率って、連続な分布を持つものをまっとうに考えようと思うと、 測度とか、 ルベーグ積分とか、 フーリエ変換とか、 数学の中でもそこそこ高等な部類に入る理論が出まくる結構ガチな分野です。

とりあえず、大学学部くらいで習う言葉で要約すると、

という感じ。

微分しても元通りになる指数関数が微分・積分の分野で最も自然な関数になるのと同様に、 確率分布では正規分布が最も自然な分布になる、という感じです。

正規分布は丸い

まあ、「正規分布乱数を使うと自然に見える」というのは正しいんですが。 ちょっとまだ不思議なことがあります。

これまで例を示してきた話はまとめると以下のようになります。

  • 一様乱数は四角い、そして、不自然に見える
  • 正規分布乱数は丸い、そして、自然に見える

ここで疑問に思うべきは、丸いのは自然なのか。 自然は丸くなるのか。

まあ実際、丸いんですよね。自然物にはあんまり角がない。 いろいろ根拠はあるんですが。 いろんな物理法則が、計算してみると円や球を解に持ちます。 例えば、わかりやすい例だと以下のようなやつ。

  • 距離に反比例するような物理法則が多くて、等ポテンシャル面をプロットすると球
  • 体積に対して表面積が最小になる図形が球なので、真球状態が一番安定して存在できる

そして、正規分布乱数の話でも、やっぱり真円や真球が出てきたりします。 先ほど、「正規分布同士を畳み込みすると、結果もまた正規分布になる」と書きましたが、 これから言えることは、「正規分布乱数同士の和や差は、やっぱり正規分布乱数になる」ということです。

回転を表す座標変換は、以下の通り、和と差になるので、 x1, y1が正規分布乱数で作られているとき、 回転した結果の x2, y2 も正規分布乱数になります。

x2=cosθ x1-sinθ y1

y2=sinθ x1+cosθ y1

正規分布乱数は、回転対象な分布を作ります。 どの方向も均一です。

例えばの話、以下のような乱数で2次元の点を作ってみましょう。 角度を一様分布にしたものです。

var chi = new ChiSquared(2);
var uni = new ContinuousUniform(0, 2 * PI));
var r = Sqrt(chi.Sample());
var θ = uni.Sample();
var p = (r * Cos(θ), r * Sin(θ));

ChiSquaredカイ二乗分布っていうやつで、 正規分布なx, yに対して、x2+y2 の分布がカイ二乗分布になります。

これも1万点プロットすると、以下のようになります。

カイ二乗分布で半径を、一様分布で角度を生成した結果

比較のために、正規分布乱数でx, yを作ったものを再度並べてみましょう。

正規分布乱数でx, yを生成した結果

ほとんど同じ分布になっていると思います。 理論上は完全一致するはずで、点の数を増やせば増やすほど一致するはずです。

適当にx軸、y軸を決めて、それぞれ独立に乱雑に座標を振っても、 x, yともに正規分布乱数(= 自然な乱数)になっている限り、 軸のとり方に依らない(回転する座標変換を掛けても、元の法則と同じ式が現れる)ということです。

自然物は向きを持たなくて、自然法則は軸のとり方に依らない。 ということが、自然法則を表すいろいろな数式上に現れます。 その結果が、真円や真球に結びついたりします。

今年の振り返り

$
0
0

普段あんまりこういう「1年の〆」みたいなブログは書かないんですけど、今年は12月に1日1ブログを書いてるついでに、最後に1日を振り返りで埋めてしまおうかなぁとか、数日前に思いついたので。

今年というか、去年くらいからなんですけども、このサイトや僕が関わっているものの傾向を手短にまとめると、

  • 勉強会開催・登壇が減った
  • ブログが増えた

ですかねぇ。

まあ、主に「ピックアップRoslyn」をやってるせい。 で、そうなった理由まで考えると、C# のオープンソース化の影響かなぁ。

出る前が楽しい

C# 7、結局、2016年中には出ませんでしたが。

とはいえ、RC版まで行くと後は基本的にバグ修正しかしなくなるので、むしろPreviewの頃の方が楽しいですね。 これも、オープンソース化の影響。 リリースよりも、作業が進んでるところが見えている方が楽しいって言う。

build insiderで書いてる記事なんかも、「最新動向」ってテーマでお願いされていて、なので、リリースよりも、現在の進捗追いつつ将来の機能を書く感じになっていますし。

直接的な開発コミュニティ化

なんか、コミュニティの作られ方がより直接的になったのかなとか感じます。

どこの方面でも共通の話ですが、「勉強会の高齢化」みたいな話題結構あって、C#もまあ、コミュニティによっては結構おっさんばっかり。MS技術はImagine CupとかMSPとかのおかげで定期的に若い子入ってきたりはしますし、 Unityの流行のおかげでUnityにも触れるようなところでは若返った感はありますが。

まあでも、全体的に高齢化していくのは世の常で。「上が詰まってて活躍できないから他のところに行く」みたいな、技術の良し悪しとは関係ないレイヤーで高齢化したりしますし…

でも、じゃあ、若い子がいないかというと、GitHub上でissue報告とかpull-request送ってくれたりするのは圧倒的に若い子っぽいんですよね。 なんていうか、紹介記事書いたり登壇したりじゃなくて、直接開発に関われる。 今時のコミュニティって、ソースコード リポジトリ上に中心があるのかも、とか思います。

もちろん、オープンソース自体はここ数年の動きじゃなくて、だいぶ昔からあるものですけども。 pull-request出しやすいとか、GitHubの功績が大きいのかなぁ。 C# 的にも、(Codeplexで)オープンソース化した瞬間よりも、GitHub移行したときの方がインパクトが大きかった気がします。

勉強会

まあ、だからって、オフラインでの集まりがなくていいとも思ってはいないんですけども。 単純に、ちょっと記事書きに時間取られすぎてさぼっちゃったかなぁ…

幸いなのは、「C#ユーザー会」の名前で、僕以外にも活動してくれる人が現れたこと。 こばやんさん。

あんまり個人に依存していると、その個人の事情に左右されて、頻度や質にどうしても波ができちゃうんで、 協力してくれる人がいるって言うのは大変助かります。

あっ、ちなみに、VS 2017リリース記念勉強会はやります。 会場都合でたぶん2017年の3月11日(会場都合があるので、リリースがまだだったら「リリース間近」とかにタイトル変えてやります)。

英語で出す

で、ソースコード リポジトリが中心になると、割かし必然的にコミュニケーションは英語になっちゃうんですよね…

まあ、PPAPの流行に対しても「最大の教訓は、グローバルなメディアに英語で出すべきということ」なんて言う人いますしね。

というか、使ってくれる人、コメントくれる人はほんと英語が多い。 自分専用と思って適当に作ってまったく宣伝していないものでも、英語で出しとくと気が付いたら誰かが使ってて、 気が付いたら「いいね」的なコメントが付いてたりします。

ということで、まあ、宣伝して人に使ってもらう気が全くなくても、 ちょっとでも実用の芽があるあるかもって思ったものはとりあえず、ソースコード リポジトリを全部英語にしてます。

最近だと、こないだ作って公開したContextFreeTaskってやつに興味を持って、 pull-requestを送ってくれた人がいます。 元々はコンセプトの紹介用に適当に、実用性考えずに作ったものですけども、 どうも自分が思っていた以上に実用に使いたい/使えそうと思ってくれる人がいまして、 実用に耐えうるように修正するpull-requestという感じです。

日本人同士のやり取りでも全部とりあえず英語にしてもらいました。 まあ、面倒だったらtwitter上とかで日本語で話つつ修正するんですけども、 少なくとも、記録に残るところは英語にしておこうかなと。

英語が返ってくる

さらに言うと、タイトルとか説明文が日本語でも、例えば言語を問わないような動画(操作デモとか、演奏の類とか)だと、 真っ先にコメントが付くのは英語だったりしますし。

なんか、「特に意識しなくても返ってくるのは英語」って感じですかね。 もちろん能動的に自分で英語を書く方が得られる機会は多いんですけど、 受動的ですら英語。

個人の嗜好としては、やっぱりわざわざ英語の勉強を頑張るってのは嫌で、 「自然と触れるからなんとなく覚える」くらいの状態でありたいので、 最近のこういう傾向はありがたかったりします。

C#小ネタ集の振り返り

ついでなので、短期的な振り返りも。今月延々と書いてたC#小ネタ集。

当初予定では25日までの書き溜めがあったのが、追加や、別ネタの割り込みがあって気が付いたら30日分になっていて。 昨日でちょうど、在庫ピッタリ一掃。 今日、これを投稿したらほんとに1日1投稿です。

小ネタ集、アクセスが多かったのは上から順に以下の通りです。

  1. 小ネタ 隠し演算子(?)
    • かなり丁寧に、本気でだますつもりで書きましたけど、それが良かったのかな
  2. C#小ネタ集: C#をWeb上で試す
    • まあ、初日ですし(だいたい、アドベントカレンダー的なものは初日だけ伸びる)
    • あと、やっぱり「ブラウザー上とかでさらっと試したい」需要はありますよね
  3. 小ネタ privateメンバーはAPIの一部か
    • これはぐらばくさん効果かなぁ
    • あと、踏むと結構つらそうな不具合の話だからかな

オタマジャクシ演算子の話(他のやつの1.5倍~2倍)と比べると残りは微々たる差ですけども。

ちなみに、年間で言うと、以下の3つが上位。

  1. .NET Coreへの移植
  2. Unity 5.5でasync/await使えた話
  3. プログラミング言語における文字コードの話

案外 .NET Core が気になっている人多い。やっぱりUnityは強い。

意外だったのは、文字コードのやつですね。 他の方から、「システム開発やってて絶対はまるから文字コードの説明はしたいけど、文字コード単体だとものすごく受けが悪いから、プログラミング言語の講座とかに混ぜてやる」とかいう話も聞くんですけども。 なんか、3番目のアクセス数でした。

という結果を受けて書いたのが、以下のBuild Insiderでの記事なんですけども。

慣れない記事書いたもんだから、これ、ほんと苦労した…


2進数リテラルと数字区切り文字

$
0
0

C# 7思い出話

C# によるプログラミング入門に、ちらほらとC# 7の話題を書き始めたわけですが。

まあ、入門なんで仕様として固まったものだけを書いていくつもりです。ある程度固まりそうな段階まで書かないし、結局予定から漏れたものは修正したり。

一方で、その仕様が固まるまでにあった流れなんかも、ブログに残しといてもいいかなぁとか思ったり。

ってことで、「C# 7思い出話」なんていうカテゴリー付けて、ブログでも書いてみようというのが今回の話。 さしあたって、今、入門に書いたのが、

の2つなので、今日はこの2つ。

2進数リテラルと数字区切り文字

こういう機能。

var million = 1_000_000;
var abcd = 0b1010_1011_1100_1101; // 特に2進数リテラルで有用
var abcd2 = 0xab_cd;              // 16進数リテラルにも使える
var x = 1.123_456_789;            // 浮動小数点数リテラルにも使える

2進数リテラルと数字区切り文字の2つはセットですね。 2進数って普通に書いたらむちゃくちゃ大きな桁数になりますし。 そりゃ、区切らないと読めた代物じゃない。

この2つの機能、「C# 7」としては「気が付いたらいつの間にか実装があった」って感じです。 特に「実装したよ」アナウンスもなく、pull-requestも見かけず。

そもそも、「C# 7の最初の設計ミーティング」でちょこっと「ページ内検索してみたら確かに書かれてる」程度の地味な取り上げられ方してただけ。機能的にも小さなものなので、提案ページもすごく簡素。

それも当然でして、この機能はC# 6の頃からあったから。 要するに、「C# 6の頃から試験的な実装あったけど、結局C# 6には入れなった」というもの。 仕様的に何か問題があったわけでもなくて、単純に「優先度低、スケジュール的に後回し」。 という話が、今稼働してるGitHubのリポジトリじゃなくて、昔懐かしCodePlex時代にありました。

まあ、こういう、低コスト・低リターン機能は後回しになりがち。

実装するのは低コストと言っても、仕様的に問題ないかをよく考えたり、実際試してみる期間を設けるのはそれなりに大変です。 CodePlex上で、以下のようなディスカッションがあった記憶があります。

  • 区切り文字は _ でいいの?
  • 1010 1100みたいにスペースで区切らせてよ
    • それは字句解析的に面倒で、コストかかりすぎる
  • 8進数リテラルも入れてよ
    • あっても使わないだろ、実際
      • chmodで使うよ
    • C言語の0始まりは紛らわしいし、octalだからって0o (ゼロ、オー)も0とoが区別つきにくいし
  • そもそも、16進数リテラルの0xもなんなの、Xって。hexの3文字目って

簡単な機能であっても、なかなかめんどくさい感じの話に。

【開催報告】 //build/ 振り返り勉強会

$
0
0

5/21(土)に勉強会を開いてました。 今回はまどすた(旧めとべや)との共同開催で、//build/の振り返りでした。

以下、当日資料の一覧です。

ルームA (サーバー部屋)

//build/ まとめ(サーバー編)

祝GA、 Service Fabric 概要

Bot FrameworkでBot入門

Introduction to Azure Functions

Bash on Ubuntu on Windows、ちょっとだけWindows Subsystem for Linux

C# 7

ルームB (クライアント部屋)

//build/ 2016現地で感じたクライアント開発の潮流

Holo World ~ はじめの一歩 ~

Build/Evolve 振り返り

デスクトップ アプリがこの先生きのこるには

デスクトップ アプリの生存戦略 (Desktop App Converter)

VR元年のゲーム開発

Cutting Edge!

ピックアップRoslyn 1/24: null参照

$
0
0

今日はピックアップRoslynなのかC#小ネタ集なのか微妙なライン。

なんか、C# 7で導入される参照戻り値に関して、参照なのにnullを返せるというネタを思いついてしまったり。 ただのネタのつもりだったんですが、案外考えなきゃ行けない事案かもなぁという話。

経緯

参照戻り値で、「null参照」を返したいっていう要望が出ていたりします。

ref? T みたいな専用の記法が欲しいという要望です。 他の人の反応としては、「参照はnullを返すものじゃない。あきらめろ」的な雰囲気。 僕個人の感想としても、「メリットの割に複雑。実装するのは割に合わない」と思います。

なのでそのままスルーしようとしていたところで、ふと、邪悪なアイディアを思いついてしまいます。 「Unsafeクラス使ってnull返せるよ」とかいう。 以下のようなコードでできます。

    unsafe static ref T NullRef<T>() where T : struct => ref Unsafe.AsRef<T>((void*)0);
    unsafe static bool IsNull<T>(ref T r) where T : struct => Unsafe.AsPointer(ref r) == (void*)0;

どういうことかというと、先月18日の小ネタの最後でちょっと話しましたけど、 .NET ランタイムの内部的には参照とポインターの扱いは全く同じです。で、Unsafeクラスは、それを利用して(半分、悪用レベル)ポインターと参照の相互変換する機能を提供しています。名前通り結構安全性を損なう機能で、色々悪用もできます。その1つが、今回の「null参照」。0をポインターに渡して、それを参照に変換してやれば、nullな参照の完成という。

参照にnullは期待しない

参照とポインターの違いの1つに、無効な参照先(要するにnull)を認めるかどうかがあります。ポインターにはnullポインターがありますが、普通、参照先がない参照なんて想定しません。無効な場所を指さないように、コンパイラーが色々制限を掛けているのが参照です。 C#でも「そのつもり」です。 参照引数も、参照戻り値も、通常は必ず有効な参照先を持ちます。

それを台無しにできる程度に悪用可能なのがunsafeコンテキストなわけです。まあ、そういうことが可能だから、コンパイルオプションで「unsafeコードを認める」(/unsafeオプション)をオンにしないと使えなくしているわけですが… 参照戻り値を使うと、/unsafeオプションなしで危ない事ができる可能性が出てきます。

Unsafeクラスの利用自体では、危ない事をするにはポインターを介する必要があって、使う側にも/unsafeオプションが必要です。危ないことしている自覚が出るという意味では、Unsafeクラスはまだ安全な部類と言えます。

ところが、先ほどのコード、再掲になりますが以下のようなものが入ったライブラリを作るとします。

    unsafe static ref T NullRef<T>() where T : struct => ref Unsafe.AsRef<T>((void*)0);
    unsafe static bool IsNull<T>(ref T r) where T : struct => Unsafe.AsPointer(ref r) == (void*)0;

このコードを実装する際にはポインターが含まれているので/unsafeオプションが必要です。一方、引数や戻り値にはポインターが出てこず、これを使う側には/unsafeオプション不要です。通常の、safeなコンテキストで、危ないものが使えている状態になりました。

null参照を認めるべきか

できてしまうものは仕方がない。であれば、改めて、null参照を認めるべきなのかという問題が出てきます。

まあ、前述の通り、普通、「無効な参照」なんて期待しません。他の言語でも見たことがない。「nullがほしければポインターを使え」という感じ。あまり期待されないていないものが存在するのはそれだけでデメリットです。完全に消せるならそれに越したことはない。

ところが、先ほどの邪悪なアイディアにより、現状でも存在しうることがはっきりしました。まあ、敢えてあんなコードを書かない限りは起きない事なので、別に問題なしとして放置するのもありな範囲でしょう。(参照戻り値自体、利用頻度はそれほど多くならないだろうものです。低頻度なものにコストを掛けても割に合いません。)

逆に、積極的にnull参照を認めるならどうすべきでしょう。今現在、null許容参照型っていう言語機能も提案されているくらいですし、少なくとも、null参照があり得るかあり得ないかの区別は、メソッドシグネチャだけ見て区別が付くべきでしょう。そうなると、冒頭のref? Tという書き方が現実味を帯びてきます。

まとめ

Unsafeクラスがなかなかやりたい放題で、通常あり得なさそうな「null参照」を得られることに気づきました。

まあ、Unsafeとかいう名前からして危なそうなものを使って初めてできることですし、参照戻り値自体利用頻度はそう高くない機能になると思われるので、放っておいても問題なさそうなものです。

とはいえ、もしかすると、「null許容参照」みたいな概念も必要になるかもしれません。

ピックアップRoslyn 1/25: Design Notes 数か月分

$
0
0

Mads (C# コンパイラーのPM)が、去年8月辺りからの C# Language Design Notes がまとめて投稿されました。 たぶん、C# 7の作業が一段落したのかな(最近のC#チームは、実装作業が落ち着くまでドキュメントの類が放置されがち)。

ちなみに内容的には、個別のトピック用のissueページが別にあって、そっちで一通り公開済み。 当然、特に目新しい情報はなくて、まとめと履歴的な状態になっています。

かなりの分量一気に来たのであんまりしっかり読む気にもなれないけども、軽く紹介:

2016/8/24

-C# Language Design Meeting, Aug 24, 2016

  • Task-likeに対して、どうやってmethod builderを取るか決めた
  • フィールド初期化子やコンストラクター初期化子内でのout varを認めたらこんな問題が出るみたいな検討したみたい
    • ※結局、初期化子でのout varの利用自体、今のところは禁止してる

2016/9/6

引数の数が同じDeconstructメソッドがあるときに分解できないの不便だなぁとは思ってたけども。 タプル間の変換を想定してのことらしい。

2016/10/18

C# 7に入れる機能の最終確認をしていたのはこの次期みたい。

  • discardsがC# 7に入るかどうかまだ怪しい時期だったものの、分解とかout varとか、今後追加する文法では_を予約しておこうとはしていたみたい
    • ※ 結局discardsの実装、C# 7に間に合ってる
  • case (int, int) x:みたいなタプルへの型スイッチは認めていない。将来「再帰パターン」って構文が入る予定で、その構文に近くなっちゃうんでその時まで保留
  • ローカル関数では、理想としてはその外でできることは全部ローカル関数内でもできるようにしたかったけど、2点ほど無理なものがあった:
    • コンストラクター内のローカル関数内での readonly field への書き込み
    • 非同期メソッドとイテレーターになっているローカル変数内で、その外の変数の「確実な初期化」保証ができない
      • https://gist.github.com/ufcpp/ad84a38cd8b5e324245bda1472de4679 ←たぶんこういう話
  • _を使った数字区切り、数字の間でだけ認める
    • 0x1a_2bはOKだけど、0x_1a2bはダメ。先頭と末尾を認めない
    • 「区切り」なんだから、数字の間だけであるべき
  • throw式は、???:でだけ使えるように制限した。&&とか||の後ろとか()の中はダメ。
  • タプルの名前の順序間違い、例えばvar (first, last) = (last, first);みたいなの、事故りそうな予感があって警告は出したい
    • ※結局は「工数の問題でC# 7の時点ではできない」「後からWarning Wave追加する」ってなってる(11/15のNotes参照)
  • タプルに対してnew (int x, int y)()は認めないことにしたけども、配列とかnullableはどうかnew (int x, int y)[10]new (int x, int y)?()は認めたい
    • ※結局認めたみたい

2016/10/25

  • C# Language Design Notes for Oct 25 and 26, 2016

  • C# 6の時に一度は検討して、最終的には入れれなかった「変数宣言式」について

    • 宣言式自体は問題あって、提案当時のままというわけにはいかない
    • 宣言式から派生した、分解とout varはC# 7に入った
    • 分解とout varは、再び統一的に扱った方がよさそう(C# 7では間に合わないけど、将来の予定として)
      • 分解代入と分解宣言を混ぜたり: (x, int y) = e
      • out引数のところに分解を書いたり: M(out (x, y))
  • パターン マッチングでは、いくらか、絶対に成功するパターン(irrefutable(反論の余地がない)パターンって呼んでる)がある
    • 今は、絶対に成功していることを前提としたフロー解析(確実な初期化とか、null解析とか)をまだあまりやっていないけど、将来的には役立ちそう
  • 分解のために使うDeconstructメソッド、out引数を使ったもの(Deconstruct(out int x, out int y))じゃなくてタプルを返す("(int x, int y)Deconstruct()")のはどうかというのを検討
    • 計測してみたけども、どっちが効率的かみたいな差は出なかった
    • あんまりやる価値はなさそう
  • if (int.TryParse(s1, out var i)) { ... i ... } みたいな書き方で、Tryの成功時も失敗時も必ずiが初期化されちゃうがために、ifの外でもiが使えてしまうのは問題じゃないか
    • ifの外に変数が漏れる設計にした以上これを防ぐ手立てはないんだけど
    • 「Tryパターンなメソッドのout varの場合はifの外で使うと警告」みたいなことができるAnalyzerは用意した方がいいかも
  • discards に_を使うことにしたけども…
    • 分解とかout varとか、これから足す構文については常にdiscards扱いしたい
    • 既存構文の場合、_を2個以上宣言していたらdiscards扱い、1つも読み出ししてないならdiscards扱い、みたいなルールでやる予定

2016/11/15

  • C# Design Notes for Nov 15, 2016 #16674

  • 要素の名前違いのタプルに関して警告を出すべき状況がある

    • 今は実装していないけど、将来的にはWarning Wavesで警告を追加したい
  • Wildcardsって呼び名をDiscardsに改めたのはこの辺り
    • MVP Summitで、MVPからこの案が出てた
    • *よりも_がいいだろうという感触もここで得た
      • 現状、_が有効な識別子だと言っても、それを「値の無視」以外の目的で使っている人は少なそう

2016/11/30

  • C# Language Design Notes for Nov 30, 2016

  • whileの条件式内のout varで宣言された変数は、whileの外に漏れないようにした

  • forの更新式の中で定義した変数は、forの内側にも漏れないようにした
  • 10/25のNotesで検討していた「分解から宣言式への統一化」、自信をもってできそう
  • 式中で宣言した変数をどこでも使わなかった場合は「未使用変数警告」を出すべきか?
    • そういう変数はたいてい無視するためのダミー変数で、discards機能が入ったら要らなくなるはず
    • でも、警告にまではせずとも、サジェストとして「discardsへの変更」機能を提供することもできる
    • 結局、後者(警告にはしない)を選択
  • C#では、embedded ステートメント(ifとかの後ろ)の中で宣言ステートメントを書くとエラーになるけど、同様の文脈で、今後、分解とかout varとかを使って変数宣言できるようになる。これはエラーにすべきか?
    • しない。むしろ既存の制限の方を緩める可能性すらある(と言ってもC# 7ではやらない)
  • nullでないことを調べるだけのパターンが欲しいって案が出てた
    • やりたい。C# 7までには時間がないけど、アイディアとしてはいい

2016/12/7と12/14

  • C# Design Notes for Dec 7 and Dec 14, 2016 #16709

  • クエリ式中で(out varとかで)宣言された変数は、式中ずっと使えるべきかも

    • where int.TryParse(s, out int i) とかで宣言されたiは、その後ろのselect句とかでも使えるようにする
    • そうなると、単純にWhere(x => int.TryParse(x.s, out int i))みたいな展開はできない。透過識別子が追加される
    • let句での分解(let (dx, dy) = (x - x0, y - y0)みたいなの)でも同様に透過識別子が増える
    • とりあえずC# 7までに実装できる時間はないんで、C# 7時点ではコンパイル エラーにしてある。将来的にはできる余地だけ残してある
  • 10/25で出てた「絶対に成功するパターン(irrefutableパターン)」は、到達可能かどうか(returnthrow、永久ループの後ろに何か書くと警告を出して呉れるやつ)の判定にも使うべきか
    • やってもそこまで価値はなさそうで、既存のセマンティクスを変えるほどのコストは見合わなさそう
  • 11/30で、whileの条件式と、forの更新式中で宣言した変数のスコープを縮めるって話は書いたけど、言い忘れてただけで、do-whileも同様

2017/1/17

  • C# Design Notes for Jan 17, 2017 #16710

  • 定数パターン(e is 42みたいなの)の展開結果をちょっと変更

    • これまでobject.Equals(e, 42)
    • これからobject.Equals(42, e)
    • object.Equalsの中で、第1引数のインスタンス メソッドのEqualsに処理を丸投げしているらしくて、第1引数が定数な(かつ、nullじゃない)保証がある方がちょっとだけパフォーマンスがいいらしい
  • タプルに対する拡張メソッドで、型変換が効くように
    • 配列に対してIEnumerable<T>の拡張メソッドが呼べるんだから、(T[], U[])に対して(IEnumerable<T>, IEnumerable<U>)の拡張メソッドを呼べるべきだろうという提案があった
    • 通常、明示的な型変換が必要な場合に拡張メソッドは呼ばれない。で、ValueTuple間には暗黙的型変換が働かないので、上記拡張メソッドを呼べない
    • でも、全要素がそれぞれ暗黙的に変換できる場合にコンパイラーがタプル間の変換をするんだから、拡張メソッド呼び出しの時にもこの変換を認めるべき
    • これ、「今やる」か「今後もうできないか」の2択なので、もう時間も限られているけど頑張って今やってみる

参照戻り値と参照ローカル変数

$
0
0

C# 7の説明、1つ足しました。参照戻り値がらみ。

参照戻り値と参照ローカル変数

追加される構文自体は割とシンプルなんですが、活用できそうな場面まで含めて説明しようとするとなかなか骨が折れる感じの機能。

まず、メモリ管理の方法について(スタックとかヒープとか)知ってないとピンとこないですしね。

「別の何かを参照する」って考え方も、そこそこ素養を求める概念ですし。 C言語とかC++で「ポインターは難しい」とか言われるのも、同種の問題だと思います。

ということで、GitHub上のディスカッションでも、大体は、

  • 活用場面がよくわからない
  • (自分は)使わなさそうなのに、複雑性を増すのには反対
  • 構造体は immutable に作れってのが常識じゃないのか

なんて話がまずあって、それに対して、

  • メモリ管理の手法とか知らない人はそう言って、パフォーマンスをいとも簡単に落とすんだ
  • パフォーマンスを求めると、mutable な構造体を配列で持っとくしかない場面があるんだ

とかの返事が毎度ついてる感じ。

文法自体についての反対意見はあんまりなさそう。しいて言うなら、

  • 非同期メソッドとかイテレーターとかでも使いたいけど、Task<ref T>とかIEnumerable<ref T>は作れないよね?
  • ローカル変数の宣言で、var (変数の型推論)を使う場合ですらref varって書かないといけないのはちょっと面倒

みたいな話が出てるくらい。

まあ、なかなか活用しどころが想像しにくい機能ではあるんですが、C# チーム的には優先度結構高いでしょうね。 Build Insiderの方で書きましたけど、 今、パフォーマンスに対する要求がかなり上がってるので。 .NET Core関連のチームとか、C#チーム自身とかにとってはかなり有用な機能のはず。 多くのプログラマーにとって直接使うことがない機能であっても、 低レイヤーなライブラリとかツールとかの性能向上が見込めるので、間接的な恩恵はそこそこありそうな気がします。

6/6 追記:

この手のメモリ管理がらみのパフォーマンス改善の話に付いて回るのは、ガベージコレクションもかなり高性能になってて、もうガベコレに任せてても大体大丈夫なんじゃないかという議題。

確かに、MS製ガベコレって結構高性能なんですよね。それでもガベコレ除けをきっちりするとかなりのパフォーマンス改善したりしますが。 仮にまあ、ガベコレがもっともっと優秀になったとしても…

その優秀なガベコレがどこででも使えると思うなよ!

と思うわけです。 ガベコレのコードなんて、.NETのものなんてファイルサイズが1.2MB(GitHubに"we can't show files"って言われるやつ)、行数3700行を超える長大なC++コードです。どこまで手書きでどこまで機械生成かわからないなんて話もありますが、どちらにしたって、なかなか保守が大変そうな分量。 特定の1環境で保守するのはまだ成り立つんですが、全然スペックの違う異なる環境向けにそれぞれ提供とかにあると、なかなか大変そう。 要するに何が言いたいかというと、あんまりこういうレイヤーの改善に期待してると、新しいプラットフォームに移りづらくなるとかのリスクもあったり。

具体的に、最近自分はUnityでiOS/Android両対応のゲームを作ってるわけですけども、 どうも、ヒープ確保の性能がiOSとAndroidでだいぶ違うみたいで、同じコードを書いててもAndroidでだけパフォーマンスが出なかったりします。 (他にもパフォーマンス ネックの原因になってそうな場所は多いんですが、ヒープ確保もネックの1つなことは間違いなさそう。)

一般論としても、「ムーアの法則での発展を期待していたら、モバイルな時代が来ちゃってむしろ時代が巻き戻った」なんて言葉よく聞くわけでして。 今更ながらC#に参照戻り値みたいな「使う人を選ぶ」機能が入るのも、そういう時代背景があるんじゃないかなと思います。

Viewing all 483 articles
Browse latest View live