TypedReferenceが舞台裏にあるのはなぜですか?それはとても速くて安全です…ほとんど魔法です!


128

警告:この質問は少し異端です...宗教的なプログラマーは常に良い習慣を守っています。読んではいけません。:)

TypedReferenceの使用が(暗黙的に、ドキュメントがないために)推奨されない理由を誰かが知っていますか?

ジェネリックであってはならない関数を介してジェネリックパラメーターを渡す場合(object値の型が必要な場合は、過剰または低速である可能性があります)、不透明なポインターが必要な場合など、その使用方法が優れていることがわかりました。実行時に(を使用してArray.InternalGetReference)仕様が見つかる配列の要素にすばやくアクセスする必要がある場合に使用します。CLRはこのタイプの誤った使用さえも許可しないので、なぜそれが推奨されないのですか?安全ではないようです。


私が見つけた他の用途TypedReference

C#でジェネリックを「特殊化」します(これはタイプセーフです):

static void foo<T>(ref T value)
{
    //This is the ONLY way to treat value as int, without boxing/unboxing objects
    if (value is int)
    { __refvalue(__makeref(value), int) = 1; }
    else { value = default(T); }
}

ジェネリックポインターで動作するコードを記述する(これは、誤用すると非常に危険ですが、正しく使用すれば高速で安全です):

//This bypasses the restriction that you can't have a pointer to T,
//letting you write very high-performance generic code.
//It's dangerous if you don't know what you're doing, but very worth if you do.
static T Read<T>(IntPtr address)
{
    var obj = default(T);
    var tr = __makeref(obj);

    //This is equivalent to shooting yourself in the foot
    //but it's the only high-perf solution in some cases
    //it sets the first field of the TypedReference (which is a pointer)
    //to the address you give it, then it dereferences the value.
    //Better be 10000% sure that your type T is unmanaged/blittable...
    unsafe { *(IntPtr*)(&tr) = address; }

    return __refvalue(tr, T);
}

書き込みメソッドのバージョンsizeofたまに役立つことができた命令を、:

static class ArrayOfTwoElements<T> { static readonly Value = new T[2]; }

static uint SizeOf<T>()
{
    unsafe 
    {
        TypedReference
            elem1 = __makeref(ArrayOfTwoElements<T>.Value[0] ),
            elem2 = __makeref(ArrayOfTwoElements<T>.Value[1] );
        unsafe
        { return (uint)((byte*)*(IntPtr*)(&elem2) - (byte*)*(IntPtr*)(&elem1)); }
    }
}

ボクシングを回避したい「状態」パラメータを渡すメソッドを書く:

static void call(Action<int, TypedReference> action, TypedReference state)
{
    //Note: I could've said "object" instead of "TypedReference",
    //but if I had, then the user would've had to box any value types
    try
    {
        action(0, state);
    }
    finally { /*Do any cleanup needed*/ }
}

それでは、なぜこのような「文書化されていない」ような使用法なのでしょうか(ドキュメントがないため)?安全上の理由はありますか?ポインタと混合されていなければ、完全に安全で検証可能であるように見えます(いずれにしても安全でも検証可能でもありません)...


更新:

実際に、TypedReference2倍以上の速度になることを示すサンプルコード:

using System;
using System.Collections.Generic;
static class Program
{
    static void Set1<T>(T[] a, int i, int v)
    { __refvalue(__makeref(a[i]), int) = v; }

    static void Set2<T>(T[] a, int i, int v)
    { a[i] = (T)(object)v; }

    static void Main(string[] args)
    {
        var root = new List<object>();
        var rand = new Random();
        for (int i = 0; i < 1024; i++)
        { root.Add(new byte[rand.Next(1024 * 64)]); }
        //The above code is to put just a bit of pressure on the GC

        var arr = new int[5];
        int start;
        const int COUNT = 40000000;

        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set1(arr, 0, i); }
        Console.WriteLine("Using TypedReference:  {0} ticks",
                          Environment.TickCount - start);
        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set2(arr, 0, i); }
        Console.WriteLine("Using boxing/unboxing: {0} ticks",
                          Environment.TickCount - start);

        //Output Using TypedReference:  156 ticks
        //Output Using boxing/unboxing: 484 ticks
    }
}

(編集:上記のベンチマークを編集しました。ポストの最後のバージョンはデバッグバージョンのコードを使用していたため[リリースに変更するのを忘れていました]、GCに圧力をかけていません。このバージョンは少し現実的です。私のシステムでは、TypedReference平均で3倍以上高速です。)


あなたの例を実行すると、まったく異なる結果が得られます。TypedReference: 203 ticksboxing/unboxing: 31 ticks。私が何をしようとしても(タイミングをとる別の方法を含めて)、ボクシング/アンボクシングは私のシステムでより高速です。
SEPH

1
@Seph:私はあなたのコメントを見ました。これは非常に興味深いことです。x64では高速に見えますが、x86では低速のようです。奇妙な...
user541686

1
そのベンチマークコードを.NET 4.5のx64マシンでテストしました。Environment.TickCountをDiagnostics.Stopwatchに置き換え、ティックではなくmsを使用しました。各ビルド(x86、64、Any)を3回実行しました。3つの結果のうち最高のものは次のとおりでした:x86:205 / 27ms(このビルドの2/3実行で同じ結果)x64:218 / 109ms任意:205 / 27ms(このビルドの2/3実行で同じ結果) -すべてのケースのボックス/ボックス化解除が高速でした。
kornman00 2013

2
奇妙な速度測定は、次の2つの事実に起因する可能性があります。*(T)(object)vは実際にはヒープ割り当てを行いません。.NET 4+では、最適化されています。このパスには割り当てがなく、非常に高速です。* makerefを使用するには、変数をスタックに実際に割り当てる必要があります(一方、ちょっとしたボックスの方法では、変数をレジスターに最適化する場合があります)。また、タイミングを見ると、強制インラインフラグでもインライン化が損なわれていると思います。そのため、ちょっとしたボックスがインライン化されて登録され、makerefが関数を呼び出してスタックを操作します
hypersw

1
タイプレフキャストの利点を確認するには、それほど重要ではありません。たとえば、基になる型を列挙型(int-> DockStyle)にキャストします。このボックスは現実のものであり、ほぼ10倍遅くなります。
hypersw

回答:


42

短い答え:移植性

一方で__arglist__makeref__refvalueされている言語拡張、ボンネットの下にそれらを実装するために使用される構築物はとC#言語仕様に文書化されていない(vararg呼び出し規約、TypedReferenceタイプは、arglistrefanytypemkanyref、とrefanyvalの命令が)完全に文書化されているCLI仕様(ECMA-335)可変引数ライブラリ

Varargライブラリで定義されているため、主に可変長の引数リストをサポートすることを目的としており、それ以外のことはほとんどサポートされていません。可変引数リストは、可変引数を使用する外部Cコードとのインターフェースを必要としないプラットフォームではほとんど役に立ちません。このため、VarargsライブラリはCLIプロファイルの一部ではありません。正当なCLI実装は、VarargsライブラリがCLIカーネルプロファイルに含まれていないため、サポートしないことを選択する場合があります。

4.1.6 Vararg

可変引数機能セットは、可変長引数リストおよびランタイム型指定されたポインタをサポートしています。

省略した場合:vararg呼び出し規約またはvarargメソッドに関連付けられた署名エンコーディング(パーティションII を参照)でメソッドを参照しようとすると、System.NotImplementedException例外がスローされます。CIL命令を使用する方法はarglistrefanytypemkrefany、とrefanyval投げなければならSystem.NotImplementedException例外を。例外の正確なタイミングは指定されていません。タイプをSystem.TypedReference定義する必要はありません。

更新(GetValueDirectコメントに返信):

FieldInfo.GetValueDirect基本クラスライブラリの一部ではFieldInfo.SetValueDirectありません。.NET Frameworkクラスライブラリと基本クラスライブラリには違いがあることに注意してください。BCLは、CLI / C#の準拠実装に必要な唯一のものであり、ECMA TR / 84に文書化されています。(実際、FieldInfoそれ自体はリフレクションライブラリの一部であり、CLIカーネルプロファイルにも含まれていません)。

BCLの外でメソッドを使用するとすぐに、移植性を少しあきらめます(これは、SilverlightやMonoTouchのような非.NET CLI実装の出現によりますます重要になっています)。実装は、Microsoft .NET Frameworkクラスライブラリとcompatiblilityを増やしたいと思ったとしても、それは単に提供できるGetValueDirectSetValueDirect服用TypedReferenceせずにTypedReference(彼らは彼らに相当すること、基本的には特別にランタイムによって処理されるobjectパフォーマンス上の利点なしで対応)。

彼らがそれをC#で文書化した場合、少なくともいくつかの影響があったでしょう。

  1. 他の機能と同様に、これ新しい機能への障害になる可能性があります。特に、これはC#の設計に実際には適合せず、奇妙な構文拡張とランタイムによる型の特別な処理が必要になるためです。
  2. C#のすべての実装は、何らかの形でこの機能を実装する必要があり、CLIの上でまったく実行されない、またはVarargsなしでCLIの上で実行されるC#実装では、必ずしも簡単ではありません。

4
移植性の良い議論、+ 1。しかし、何についてFieldInfo.GetValueDirectFieldInfo.SetValueDirect?それらはBCLの一部であり TypedReference、それらを使用するには必要なので、基本的TypedReferenceに、言語仕様に関係なく、常に定義することを強制しませんか?(また、別の注:キーワードが存在しなくても、命令が存在する限り、動的にメソッドを発行することでキーワードにアクセスできます...プラットフォームがCライブラリと相互運用している限り、これらを使用できます。 C#にキーワードがあるかどうか)
user541686

ああ、別の問題:移植性がないとしても、なぜキーワードを文書化しなかったのですか?少なくとも、Cの可変引数と相互運用する場合は必要なので、少なくともそれについて言及することはできましたか?
user541686 2011年

@Mehrdad:ええ、それは面白いですね。.NETソースのBCLフォルダー内のファイルはBCLの一部であり、ECMAの標準化の部分にはまったく注意を払っていないと常に想定していたと思います。これはかなり説得力があります... 1つの小さなことを除いて:CLI仕様に(オプションの)機能を含めることさえ少し無意味ではありませんか?(あればそれは理にかなってTypedReference-たとえば、管理C ++を-ただ一つの言語のために文書化されていましたが、あれば何の言語の文書が、それはので、誰も実際にそれを使用することはできません場合は、なぜさえ機能を定義する気?)
user541686

@Mehrdad私は疑う主な動機は、内部で相互運用のために、この機能のために必要であった(例えば [DllImport("...")] void Foo(__arglist);)と、彼らは自分自身の使用のためにC#でそれを実装しました。CLIの設計は多くの言語に影響されます(注釈「共通言語インフラストラクチャアノテーション標準」はこの事実を示しています。)予期しない言語を含む、できるだけ多くの言語に適切なランタイムであることは、設計目標です(したがって、名前)そしてこれは、例えば、仮説的なマネージドC実装がおそらく恩恵を受けることができる機能です。
Mehrdad Afshari、2011年

@ Mehrdad:ああ...そう、それはかなり説得力のある理由です。ありがとう!
user541686

15

ええと、私はエリックリッペルではないので、マイクロソフトの動機を直接話すことはできませんが、もし私が推測を行ったとしても、それ以外のことは言うでしょうTypedReference。率直に言って、それらは必要ないため、十分に文書化されていません。

これらの機能について述べたすべての使用は、場合によってはパフォーマンスが低下しますが、それらがなくても実行できます。ただし、C#(および一般に.NET)は、高性能言語として設計されていません。(私は「Javaより速い」がパフォーマンス目標だったと思います。)

とはいえ、パフォーマンスに関する特定の考慮事項が提供されていないわけではありません。実際、ポインタ、、stackallocおよび特定の最適化されたフレームワーク関数などの機能は、特定の状況でパフォーマンスを向上させるために主に存在します。

ジェネリックスは、タイプセーフティの主な利点があると思いますが、TypedReferenceボックス化とボックス化解除を回避することにより、同様にパフォーマンスを向上させます。実際、なぜあなたはこれを好むのか不思議に思っていました:

static void call(Action<int, TypedReference> action, TypedReference state){
    action(0, state);
}

これに:

static void call<T>(Action<int, T> action, T state){
    action(0, state);
}

私がそれらを見ると、前者はより少ないJITを必要とし(そして、それに続いて、より少ないメモリ)、後者はより馴染みがあり、私は(ポインタの逆参照を回避することによって)わずかに速くなるというトレードオフです。

TypedReference友人や友人に実装の詳細を問い合わせます。あなたはそれらのいくつかのきちんとした使い方を指摘しました、そして私はそれらを探究する価値があると思いますが、実装の詳細に依存するという通常の警告が当てはまります—次のバージョンはあなたのコードを壊すかもしれません。


4
ええと...「あなたはそれらを必要としません」-私はそれが来るのを見たはずです。:-)それは真実ですが、それもまた真実ではありません。「必要」とは何ですか?たとえば、拡張メソッドは本当に「必要」ですか?でジェネリックを使用することについてのあなたの質問に関してcall():それはコードが常にそれほどまとまりがあるわけではないためです-私はのようなより多くの例を参照していましたIAsyncResult.Stateが、突然ジェネリックを導入するとすべてのジェネリックが導入されるため、関連するすべてのクラス/メソッド。答えは+1ですが、特に「Javaよりも速い」部分を指摘するために。:]
user541686

1
ああ、もう1つのポイント:パブリックであり、おそらく一部の開発者によって使用されているFieldInfo.SetValueDirectがそれに依存していることを考えると、おそらくすぐに重大な変更が行われるTypedReferenceことはないでしょう。:)
user541686 2011年

ああ、でも、LINQをサポートするには、拡張メソッド必要です。とにかく、私は実際に良い/必要な違いについて話しているのではありません。TypedReferenceどちらとも呼ばない。(ひどい構文と全体的な扱いやすさは、私の頭では、「良いこと」のカテゴリーからは失格です。)あちこちで数マイクロ秒をトリミングする必要がある場合に、これは良いことだと思います。とは言っても、自分のコード内のいくつかの場所を考えて、今から見ていき、指摘された手法を使用してそれらを最適化できるかどうかを確認します。
Pダディ

1
@Merhdad:私は当時、プロセス間/ホスト間通信(TCPおよびパイプ)のためにバイナリオブジェクトシリアライザー/デシリアライザーに取り組んでいました。私の目標は、それを可能な限り小さく(ワイヤーを介して送信されるバイト数の観点から)かつ高速(シリアライズおよびデシリアライズに費やす時間の観点から)にすることでした。TypedReferencesを使用してボクシングとアンボクシングを回避したいと思ったが、IIRCは、どこかでボクシングを回避できた唯一の場所は、プリミティブの1次元配列の要素でした。ここでの速度のわずかな利点は、プロジェクト全体に追加された複雑さの価値がなかったので、取り除きました。
Pダディ

1
与えられたdelegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);型のコレクションは、T方法を提供することができActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param)ますが、ジッタは、すべての値型のための方法の異なるバージョンを作成する必要がありますTParam。型付き参照を使用すると、メソッドの1つのJIT済みバージョンがすべてのパラメーター型で機能するようになります。
スーパーキャット2014

4

私はこの質問のタイトルは皮肉であるために仮定されているかどうかを把握することはできません:されてきた老舗そのTypedReference「真」の管理ポインタの遅い、肥大化し、醜いいとこである、我々が得るもの、後者はC ++ / CLI interior_ptr<T>、またはC#の従来の参照(ref/ out)パラメーターでも。実際、整数を使用して元のCLR配列のインデックスを毎回再作成するだけで、ベースラインパフォーマンスに到達することさえもかなり困難です。TypedReference

悲しい詳細はここにありますが、ありがたいことに、これは今重要ではありません...

この質問は、C#7の新しいref localsおよびref return機能によって疑わしくなりました。

これらの新しい言語機能は、慎重に規定された状況で真のマネージ参照型を宣言、共有、および操作するために、C#で卓越したファーストクラスのサポートを提供します。CLR

使用制限は以前に必要であったものよりも厳格ではないTypedReference(そしてパフォーマンスは文字通り最悪から最高にジャンプしている)ので、C#で考えられる残りの使用例はありませんTypedReference。たとえば、以前TypedReferenceGCヒープにを永続化する方法がなかったため、上位のマネージポインターに同じことが当てはまることはありません。

そして明らかに、TypedReference廃止された-または少なくともほぼ完全に廃止された-こと__makerefはジャンクヒープにも影響を与えます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.