少し前に、「 High-Performance .NET codeを書く 」という本の作業を開始しました。この本はまだロシア語に翻訳されていませんが、まもなく1年になるでしょう。
もちろん、そのような本がすでに引用されていることは驚きではありませんでしたが、尊敬されている著者ベン・ワトソンは、章の1つに基づいてCodeproject Webサイトに記事全体を投稿しさえしました 。 残念ながら、この資料の量はhabropublishingには大きすぎますが、本の資料を評価できるように記事の最初の部分を翻訳することにしました。 調査を読んで参加してください。 さらに、2番目の部分を翻訳することをお勧めします-コメントを記入して、私たちはあなたの希望を考慮に入れようとします。
コンテキスト
この記事は、私の著書 『High-Performance .NET Codeの作成』の第5章に基づいています。
はじめに
この記事では、コードとタイプデザインを記述する一般的な原則について説明します。 .NETプラットフォームでは、さまざまなシナリオを実装する機会があり、それらのいくつかがせいぜいパフォーマンスに影響を与えない場合、それを深刻に台無しにするものがあります。 特定のシナリオは良いことも悪いこともありませんが、それが何であるかだけです。 各状況に最適なソリューションを選択する必要があります。
この記事の根底にある一般原則を定式化しようとすると、次のようになります。
深いパフォーマンスの最適化は、多くの場合、ソフトウェアの抽象化を壊します。
これは、非常に高いパフォーマンスを達成しようとする場合、すべてのレベルで実装の詳細を理解する必要があり、場合によってはこれらの微妙さを試してみる必要があることを意味します。 これらの詳細の多くは、この記事で説明されています。
クラスと構造の比較
クラスインスタンスは常にヒープ上に割り当てられ、これらのインスタンスへのアクセスはポインターの逆参照によって行われます。 ポインターのコピー(4または8バイト)であるため、それらを転送するのは安価です。 ただし、オブジェクトにはいくつかの固定オーバーヘッドもあります。32ビットプロセスでは8バイト、64ビットプロセスでは16バイトです。 これらのコストには、メソッドテーブルへのポインタと同期ブロックフィールドが含まれます。 フィールドなしでオブジェクトを作成し、デバッガで表示すると、実際にはそのサイズは8バイトではなく12バイトであることがわかります。 64ビットプロセスの場合、オブジェクトのサイズは24バイトになります。 実際には、最小サイズはメモリブロックのアライメントに依存します。 幸いなことに、これらの「余分な」4バイトはフィールドによって使用されます。
構造体構造はオーバーヘッドを伴わず、使用するメモリのサイズはそのすべてのフィールドのサイズの合計です。 構造体がメソッド内でローカル変数として宣言されている場合、 構造体はスタックに割り当てられます。 構造体がクラスの一部として宣言されている場合、 構造 体メモリは、このクラスが占有するメモリフラグメントの一部になります(したがって、ヒープ上にあります)。 構造体をメソッドに渡すと、 構造体はバイトごとに順番にコピーされます。 ヒープ上にないため、構造体を割り当ててもガベージコレクションは開始されません。
したがって、ここで妥協する必要があります。 構造の最大サイズに関するさまざまな推奨事項に出くわすかもしれませんが、私は特定の数字に執着しません。 原則として、 構造体のサイズを非常に小さく保つことをお勧めします 。特にこの構造体を前後に渡す場合は、構造体を参照渡しすることもできるため、サイズに大きな問題は生じません。 この手法が有用かどうかを自信を持って判断する唯一の方法は、使用パターンを注意深く見て、プロファイリングを実行することです。
状況によっては、効果は劇的に異なる場合があります。 単一のオブジェクトのコストの価値はごくわずかであるように見えますが、オブジェクトの配列を検討し、構造の配列と比較してみてください。 データ構造に16バイトのデータが含まれ、配列の長さが1,000,000であり、32ビットシステムで作業するとします。
オブジェクトの配列の場合、合計スペース消費量は次のとおりです。
12バイトの配列オーバーヘッド+
(ポインターサイズ4バイト×1,000,000)+
((オーバーヘッド8バイト+ 16バイトのデータ)×1,000,000)
= 28 MB
構造体の配列では、根本的に異なる結果が得られます。
12バイトの配列オーバーヘッド+
(16バイトのデータ×1,000,000)
= 16 MB
64ビットプロセスの場合、オブジェクトの配列は40 MB以上を占有しますが、 構造体配列は16 MBしか必要としません。
ご覧のとおり、 構造体配列では、オブジェクトの配列よりも同じ量のデータがメモリを消費しません。 オブジェクトの使用に関連するコストに加えて、メモリ負荷が高くなることで説明される、より集中的なガベージコレクションも行われます。
スペースの使用に加えて、プロセッサーの効率の問題もあります。 プロセッサにはいくつかのレベルのキャッシュがあります。 プロセッサに最も近いものは非常に小さいですが、非常に高速に動作し、シーケンシャルアクセス用に最適化されています。
構造体配列には、メモリ内に順番に配置された多くの値が含まれます。 構造体配列内の要素へのアクセスは非常に簡単です。 正しいエントリが見つかると、すでに正しい値があります。 したがって、大きな配列を反復処理する場合、アクセス速度に大きな違いが生じる可能性があります。 値が既にプロセッサキャッシュにある場合、RAMにある場合よりも1桁速くアクセスできます。
オブジェクト配列の要素にアクセスするには、配列のメモリにアクセスし、この要素へのポインタを間接参照する必要があります。この要素はヒープ内のどこにでも配置できます。 オブジェクト配列の列挙には、追加のポインターの逆参照、ヒープに沿った「ジャンプ」、およびプロセッサキャッシュの比較的頻繁な空化が含まれ、必要なデータが無駄になる可能性があります。
多くの場合、プロセッサレベルとメモリの両方でこのようなコストが発生しないことが、構造を優先する主な理由です。 この手法を賢明に使用すると、メモリの局所性が向上するため、パフォーマンスが大幅に向上します。
構造は常に値によってコピーされるため、不注意により興味深い位置に移動する可能性があります。 たとえば、次のコードはエラー付きで記述されており、コンパイルされません。
struct Point { public int x; public int y; } public static void Main() { List<Point> points = new List<Point>(); points.Add(new Point() { x = 1, y = 2 }); points[0].x = 3; }
問題は、リスト内の既存のポイントを変更しようとしている最後の行で発生します。 ポイント[0 ]を呼び出すと、元の値のコピーが返され、他のどこにも保存されないため、これは不可能です。 Pointを変更する正しい方法:
Point p = points[0]; px = 3; points[0] = p;
ただし、構造を変更できないようにするために、さらに厳しいポリシーを実装することをお勧めします。 構造を作成すると、そのような構造は新しい値を取得できなくなります。 この場合、上記の状況は原則的に不可能になり、構造に関するすべての作業が簡素化されます。
前述のように、構造はコンパクトであるため、コピーに多くの時間を費やす必要はありませんが、大きな構造を使用する必要がある場合もあります。 商業プロセスに関する多くの詳細が追跡されるオブジェクトを考えてください-例えば、多くのタイムスタンプが設定されます。
class Order { public DateTime ReceivedTime {get;set;} public DateTime AcknowledgeTime {get;set;} public DateTime ProcessBeginTime {get;set;} public DateTime WarehouseReceiveTime {get;set;} public DateTime WarehouseRunnerReceiveTime {get;set;} public DateTime WarehouseRunnerCompletionTime {get;set;} public DateTime PackingBeginTime {get;set;} public DateTime PackingEndTime {get;set;} public DateTime LabelPrintTime {get;set;} public DateTime CarrierNotifyTime {get;set;} public DateTime ProcessEndTime {get;set;} public DateTime EmailSentToCustomerTime {get;set;} public DateTime CarrerPickupTime {get;set;} // ... }
コードを簡素化するには、これらのラベルをそれぞれ独自のサブ構造で選択すると便利です。これらのラベルは、次のようにOrderクラスのコードから引き続き利用できます。
Order order = new Order(); Order.Times.ReceivedTime = DateTime.UtcNow;
これらの下位構造はすべて、別のクラスに移動できます。
class OrderTimes { public DateTime ReceivedTime {get;set;} public DateTime AcknowledgeTime {get;set;} public DateTime ProcessBeginTime {get;set;} public DateTime WarehouseReceiveTime {get;set;} public DateTime WarehouseRunnerReceiveTime {get;set;} public DateTime WarehouseRunnerCompletionTime {get;set;} public DateTime PackingBeginTime {get;set;} public DateTime PackingEndTime {get;set;} public DateTime LabelPrintTime {get;set;} public DateTime CarrierNotifyTime {get;set;} public DateTime ProcessEndTime {get;set;} public DateTime EmailSentToCustomerTime {get;set;} public DateTime CarrerPickupTime {get;set;} } class Order { public OrderTimes Times; }
ただし、これにより、各Orderオブジェクトに12または24バイトのコストが追加で発生します。
OrderTimesオブジェクト全体をさまざまなメソッドに渡す必要がある場合、そのようなオーバーヘッドはおそらく正当化されますが、なぜOrderオブジェクト全体への参照を渡さないのですか? 同時に数千のOrderオブジェクトを処理すると、ガベージコレクションが大幅に増加します。 さらに、追加の逆参照操作がメモリに格納されます。
OrderTimes構造を作成してください。 Orderオブジェクトのプロパティ(たとえば、 order.Times.ReceivedTime )を使用してOrderTimes構造の個々のプロパティにアクセスしても、構造のコピーは行われません(.NETでは、この可能性の高いシナリオは特別に最適化されています)。 したがって、 OrderTimesの構造は、ここに下位構造がないかのように、基本的にOrderクラスに割り当てられたメモリの一部です。 コード自体もより正確になります。
この手法は不変構造の原則に違反しませんが、ここでの秘trickは次のとおりです。OrderTimes構造のフィールドは、Orderオブジェクトのフィールドであるかのように処理します。 OrderTimes構造をエンティティ全体として渡す必要がない場合、提案されるメカニズムは純粋に組織的なものです。
構造体のEqualsおよびGetHashCodeメソッドのオーバーライド
構造を使用する場合、 EqualsメソッドとGetHashCodeメソッドをオーバーライドすることが非常に重要です。 これが行われない場合、デフォルトバージョンが取得されますが、これは決して高性能に貢献しません。 これがどれほど悪いかを評価するには、中間言語ビューアーを開き、 ValueType.Equalsメソッドのコードを見てください。 構造のすべてのフィールドの反射に関連付けられています。 ただし、これはバイナリ互換タイプの最適化です。 バイナリ互換(blittable)は、マネージコードとアンマネージコードの両方でメモリ内で同じ表現を持つタイプです。 これらには、プリミティブ数値型(たとえば、 Int32 、 UInt64が含まれますが、プリミティブではないDecimalは含まれません)およびIntPtr / UIntPtrのみが含まれます。 構造がバイナリ互換型のみで構成されている場合、Equals実装は、構造全体でメモリのバイト比較を実際に実行できます。 そのようなあいまいさを避け、独自のEqualsメソッドを実装してください。
Equals(その他のオブジェクト)を単純にオーバーライドする場合、このメソッドは値型の変換とパッケージ化に関連付けられているため、依然として不当に低いパフォーマンスが得られます。 代わりに、 Equals(T other)を実装します( Tは構造体のタイプです)。 IEquatableインターフェイスはこのために設計されており、すべての構造体が実装する必要があります。 コンパイラは、可能であれば、より強く型付けされたバージョンを常に優先します。 例を考えてみましょう:
struct Vector : IEquatable<Vector> { public int X { get; set; } public int Y { get; set; } public int Z { get; set; } public int Magnitude { get; set; } public override bool Equals(object obj) { if (obj == null) { return false; } if (obj.GetType() != this.GetType()) { return false; } return this.Equals((Vector)obj); } public bool Equals(Vector other) { return this.X == other.X && this.Y == other.Y && this.Z == other.Z && this.Magnitude == other.Magnitude; } public override int GetHashCode() { return X ^ Y ^ Z ^ Magnitude; } }
型がIEquatableインターフェイスを実装している場合、汎用.NETコレクションはそれを検出し、より効率的な検索と並べ替えに使用します。
また、値のタイプに== and!=演算子を使用し、 既存のEquals(T)メソッドを呼び出すように強制することもできます。
構造を比較したりコレクションに入れたりしない場合でも、これらのメソッドを実装することをお勧めします。 それらが将来どのように使用されるかを推測することは決してありません。また、メソッドの作成には数分しかかからず、中間言語のバイトを読み取ります。
クラスのEqualsメソッドとGetHashCodeメソッドをオーバーライドすることはそれほど重要ではありません。この場合、オブジェクトへの参照に基づいて同等性を計算するだけだからです。 コードにこれらのメソッドの標準実装が十分にあると思われる場合は、変更しないでください。
仮想メソッドとシールドクラス
デフォルトでは、「万が一に備えて」メソッドを仮想化しないでください。 ただし、プログラムの一貫した設計のために仮想メソッドが必要な場合は、おそらく削除して無理をしないでください。
メソッドを仮想化すると、動的コンパイラー側からのいくつかの最適化、特にメソッドの埋め込みができなくなります。 メソッドは、どのメソッドが呼び出されるかをコンパイラが100%知っている場合にのみ埋め込むことができます。 メソッドを仮想としてマークすると、そのような確実性が失われますが、そのような最適化を中止せざるを得ない他の要因があります。
仮想メソッドは、概念的にシールクラスに近く、たとえば次のようになります。
public sealed class MyClass {}
封印済みとしてマークされたクラスは、他のクラスがそのクラスから継承できないことを宣言します。
理論的には、動的コンパイラはこの情報を使用して、埋め込みにもっと関与することができますが、現在これは行われていません。 可能であれば、デフォルトでクラスを封印済みとして宣言し、必要でない限りデフォルトのメソッドを仮想化しないでください。 この場合、コードは動的コンパイラーの現在の最適化だけでなく、将来可能な最適化にも適応されます。
さまざまな状況、特に組織外で使用する予定のクラスライブラリを作成する場合は、注意が必要です。 この場合、仮想APIの存在は、最低限のパフォーマンスよりも重要である可能性があります。そのため、ライブラリは再利用とチューニングに便利です。 ただし、社内のニーズに合わせてコードを作成し、頻繁に変更する場合は、パフォーマンスを改善してください。
インターフェースディスパッチ
インターフェイスを介してメソッドを最初に呼び出すとき、.NETは呼び出しを行うタイプとメソッドを決定する必要があります。 まず、スタブへの呼び出しが行われます。スタブは、このインターフェイスを実装するオブジェクトを操作するときに呼び出されるメソッドを見つけます。 これが数回発生すると、CLRは同じ特定の型が常に呼び出されることを「学習」し、スタブを介したこの間接的な呼び出しは、目的のメソッドを呼び出す少数のアセンブリ命令のみで構成されるスタブに削減されます。 このような命令のグループは、1つのタイプのメソッドを呼び出す方法を知っているため、「単相スタブ」と呼ばれます。 これは、呼び出しの場所が常に同じ型のインターフェイスメソッドを呼び出す場合に理想的です。
単相スタブを使用すると、何か問題が発生したかどうかを検出することもできます。 ある時点で呼び出し元が別のタイプのメソッドを使用し始めると、CLRは最終的にスタブを新しいタイプの別の単相スタブに置き換えます。
状況がさらに複雑で、いくつかのタイプが関係し、予測が困難な場合(たとえば、インターフェイスタイプの配列があるが、この配列にいくつかの特定のタイプがある場合)、スタブはポリモーフィックなものに変わり、どのメソッドを選択できるようにするハッシュテーブルを使用します原因。 テーブルの検索は高速ですが、単相スタブで作業するときほどではありません。 さらに、そのようなハッシュテーブルのサイズは厳密に制限されており、型が多すぎる場合は、最初から汎用型検索コードにロールバックする必要があります。 この操作は非常にコストがかかります。
このような問題が発生した場合、次の2つの方法のいずれかで解決できます。
- 1.共通のインターフェースを介してこれらのオブジェクトを呼び出さないでください
- 2.共通の基本インターフェイスを選択し、 抽象基本クラスに置き換えます
この問題は一般的ではありませんが、巨大な型の階層があり、それらがすべて共通のインターフェイスを実装し、この共通のインターフェイスを介してメソッドを呼び出す場合に発生する可能性があります。 プロセッサはこれらのメソッドを呼び出す場所で非常に積極的に動作するため、メソッド自体が実行する作業だけでは説明できないことに気付くかもしれません。
歴史大規模なシステムを設計するとき、数千の異なるタイプが存在する可能性があり、それらはすべて共通のタイプに由来することが事前にわかっていました。 いくつかの場所では、基本型からアクセスする必要があります。 私たちのチームには、この規模の問題を解決するときにインターフェイスをディスパッチすることに精通している人がいたので、ルートインターフェイスではなく、基本クラスの抽象を使用することにしました。
インターフェイスのディスパッチに関する優れた記事は、Vance Morrisonのブログにあります。
梱包を避ける
パッケージングとは、ヒープ上のオブジェクトにプリミティブや構造などの重要なタイプをラップするプロセスです。 このフォームでは、この型をオブジェクト参照を必要とするメソッドに渡すことができます。 解凍は、元の値の抽出です。
パッケージ化には、オブジェクトの分離、コピー、およびキャストに必要なプロセッサー時間がかかります。 ただし、さらに重要なのは、ガベージコレクションがヒープ上でアクティブ化されることです。 パッケージングを不注意に処理すると、プログラムに多くの割り当て操作が発生する可能性があり、それらすべてがガベージコレクターに追加の負担をかけます。
同様の操作が実行されるたびに、明示的なパッケージ化が行われます。
int x = 32; object o = x;
ここの中間言語は次のようになります。
IL_0001: ldc.i4.s 32 IL_0003: stloc.0 IL_0004: ldloc.0 IL_0005: box [mscorlib]System.Int32 IL_000a: stloc.1
つまり、コード内のほとんどのパッケージングソースを見つけるのは比較的簡単です。ILDASMを使用して、IL全体をテキストに変換して検索するだけです。
不規則なパッケージングが可能な非常に一般的な状況は、 オブジェクトまたはオブジェクト[]をパラメーターとして受け取るAPIを使用することです。 最も簡単なのは、オブジェクトへの参照のみを格納するString.Formatまたは従来のコレクションであり、何らかの理由で完全に回避したい作業です。
さらに、次のように、インターフェイスに構造を割り当てるときにパッケージ化が発生する場合があります。
interface INameable { string Name { get; set; } } struct Foo : INameable { public string Name { get; set; } } void TestBoxing() { Foo foo = new Foo() { Name = "Bar" }; // ! INameable nameable = foo; ... }
このコードをテストする場合、実際にパックされた変数を使用していない場合、コンパイラは使用されていないため、最適化の順序でパック命令を削除するだけであることに注意してください。 メソッドを呼び出すか、他の方法で値を使用するとすぐに、パッキング命令が配置されます。
パッケージング中に発生するもう1つのことは、次のコードの結果です。
int val = 13; object boxedVal = val; val = 14;
その後、 boxedValの値はどうなりますか?
パッケージ化すると、値がコピーされ、元のコピーとコピーの間には接続がありません。 たとえば、 valは14に変更される場合がありますが、 boxedValは元の値13 を保持します。
プロセッサプロファイルでパッケージ化を追跡できる場合もありますが、多くのパッケージ化呼び出しは単に組み込みであり、それらを見つける信頼できる方法はありません。 過度のパッケージングでは、プロセッサプロファイルにnewを使用したメモリの大量割り当てのみが表示されます。
構造を積極的にパッケージ化する必要があり、それなしではできないという結論に達した場合、おそらく構造をクラスに変換する必要があります。
最後に、参照によって重要な型を渡すことはパッケージ化ではないことに注意してください。 ILを見て、パッケージングが発生しないことを確認してください。 重要なタイプのアドレスがメソッドに送信されます。
foreachとforeachの比較
MeasureItプログラムを使用して、 forループとforeachループでのコレクションの列挙の違いを評価します。 ほとんどの場合、標準forループの使用ははるかに高速です。 ただし、独自の簡単なテストを実行しようとすると、シナリオによっては、両方のサイクルのパフォーマンスがほぼ等しいことがわかります。 実際、多くの状況で、.NETは単純なforeachステートメントを標準forループに変換します。
次のコードを含むForEachVsForプロジェクトを検討してください。
int[] arr = new int[100]; for (int i = 0; i < arr.Length; i++) { arr[i] = i; } int sum = 0; foreach (int val in arr) { sum += val; } sum = 0; IEnumerable<int> arrEnum = arr; foreach (int val in arrEnum) { sum += val; }
収集した後、ILリフレクションツールを使用してこのコードを逆コンパイルしてみてください。 最初のforeachが実際にforループとしてコンパイルされることがわかります。 中間言語は次のとおりです。
// (head: IL_0034) IL_0024: ldloc.s CS$6$0000 IL_0026: ldloc.s CS$7$0001 IL_0028: ldelem.i4 IL_0029: stloc.3 IL_002a: ldloc.2 IL_002b: ldloc.3 IL_002c: add IL_002d: stloc.2 IL_002e: ldloc.s CS$7$0001 IL_0030: ldc.i4.1 IL_0031: add IL_0032: stloc.s CS$7$0001 IL_0034: ldloc.s CS$7$0001 IL_0036: ldloc.s CS$6$0000 IL_0038: ldlen IL_0039: conv.i4 IL_003a: blt.s IL_0024 //
保存、ロード、追加、1つのブランチという多くの操作があります-すべてが非常に簡単です。
ただし、配列をIEnumerableに持ち込んで同じ操作を実行すると、作業ははるかに高価になります。
IL_0043: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() IL_0048: stloc.s CS$5$0002 .try { IL_004a: br.s IL_005a // (head: IL_005a) IL_004c: ldloc.s CS$5$0002 IL_004e: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() IL_0053: stloc.s val IL_0055: ldloc.2 IL_0056: ldloc.s val IL_0058: add IL_0059: stloc.2 IL_005a: ldloc.s CS$5$0002 IL_005c: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() IL_0061: brtrue.s IL_004c // IL_0063: leave.s IL_0071 } // .try finally { IL_0065: ldloc.s CS$5$0002 IL_0067: brfalse.s IL_0070 IL_0069: ldloc.s CS$5$0002 IL_006b: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0070: endfinally } //
仮想メソッドの4つの呼び出し、try-finallyブロック、および列挙ステータスが監視されるローカル列挙子変数のメモリ割り当て(ここでは表示されていません)があります。 このような操作は、通常のforループよりもはるかに高価です。より多くのプロセッサ時間とより多くのメモリを使用します。
ここでの基本的なデータ構造は依然として配列であり、 forループを使用できることを忘れないでください。ただし、型をIEnumerableインターフェイスにキャストすることで難読化を進めます。 記事の冒頭で既に述べた事実を考慮することは重要です:深いパフォーマンスの最適化は、しばしばコードの抽象化に反します。 したがって、 foreachはループの抽象化であり、 IEnumerableはコレクションの抽象化です。 一緒に、それらは配列を反復するforループを使用して単純な最適化を妨げる動作を提供します。
キャスト
原則として、可能な場合は常に強制を回避する必要があります。 強制はしばしば質の悪いクラス設計を示しますが、時にはそれが本当に必要です。 そのため、たとえば、さまざまなサードパーティAPIを使用する場合など、符号付きの数値を符号なしに変換するときは、キャストに頼らなければなりません。 オブジェクトのキャストはそれほど頻繁には行われません。
オブジェクトのキャストにコストがかかることはありませんが、そのような操作のコストは、オブジェクト間の関係によって劇的に変化する可能性があります。 祖先を目的の子孫に持ってくることは、逆の操作を実行するよりもはるかに高価であり、そのような操作のコストは、階層が大きいほど高くなります。 インターフェイスへのキャストは、特定のタイプへのキャストよりも費用がかかります。
不正なキャストは絶対に受け入れられません。 その場合、 InvalidCastException例外が発生します。この例外のコストは、キャスト操作の「価格」を桁違いに超えます。
この本のソースコードのCastingPerfプロジェクトを参照してください。さまざまなタイプのキャストの数が記載されています。
コンピューターでのテスト実行中に、次の結果が得られました。
JIT (ignore): 1.00x No cast: 1.00x Up cast (1 gen): 1.00x Up cast (2 gens): 1.00x Up cast (3 gens): 1.00x Down cast (1 gen): 1.25x Down cast (2 gens): 1.37x Down cast (3 gens): 1.37x Interface: 2.73x Invalid Cast: 14934.51x as (success): 1.01x as (failure): 2.60x is (success): 2.00x is (failure): 1.98x
「is」演算子は、結果をテストしてブール値を返すキャストです。
「as」演算子は標準のキャストに似ていますが、キャストが機能しない場合はnullを返します。 上記の結果からわかるように、この操作は例外をスローするよりもはるかに高速です。
2つのキャストが行われるパターンを適用しないでください。
if (a is Foo) { Foo f = (Foo)a; }
'as' , :
Foo f = a as Foo; if (f != null) { ... }
, .
: , MemoryStream.Length long . API, , ( MemoryStream.GetBuffer ), , int. , long . .