この記事には、特定の安全でないパターン、関連するリスク、およびそれらのリスクを軽減する方法に関する詳細な推奨事項が含まれています。 これらのガイドラインは、C# で安全でないコードを記述またはレビューしているすべての開発者を対象としています。 F# や Visual Basic などの他の .NET 言語は、この記事の範囲外ですが、これらの言語にもいくつかの推奨事項が適用される場合があります。
用語集
- AVE - アクセス違反の例外。
- Byref - アンマネージ ポインターに似ていますが、GC によって追跡されるマネージド ポインター (
ref T t)。 通常、オブジェクトまたはスタックの任意の部分を指します。 参照は実質的には、+0 オフセットを持つマネージド ポインターです。 - CVE - 一般に公開されたサイバーセキュリティの脆弱性。
- JIT - Just-In-Time コンパイラ (CoreCLR と NativeAOT の RyuJIT)。
- PGO - プロファイルガイド付き最適化。
- アンマネージ ポインター (または生ポインター) - 任意のメモリ位置を指し、GC によって管理または追跡されないポインター (
T* p)。
その他の用語については、「 .NET ランタイム用語集」を参照してください。
一般的な信頼性の低いパターン
C# は、開発者がランタイムと GC の内部動作について心配する必要がない安全な環境を提供します。 安全でないコードを使用すると、これらの安全性チェックをバイパスでき、メモリの破損につながる可能性のある信頼性の低いパターンが発生する可能性があります。 このようなパターンは特定のシナリオで役に立つ場合があります。このパターンは、絶対に必要な場合にのみ注意して使用する必要があります。 C# と .NET には、安全でないコードの健全性を検証するツールが用意されていないだけでなく (さまざまな C/C++ サニタイザーが提供する可能性があるため)、GC 固有の動作では、従来の C/C++ 開発者が慣れているとは異なる安全でない C# に追加のリスクが生じる可能性があります。
マネージド参照に関する安全でないコードは、次の控えめな前提を念頭に置いて記述する必要があります。
- GC は、任意の命令で任意の時点で任意のメソッドの実行を中断できます。
- GC はメモリ内のオブジェクトを移動し、 追跡 されたすべての参照を更新できます。
- GC は、参照が不要になったときを正確に認識します。
ヒープ破損の従来の例は、GC がオブジェクト参照の追跡を失うか、無効なポインターをヒープ参照として扱うときに発生します。 これにより、多くの場合、非決定的なクラッシュやメモリの破損が発生します。 ヒープ破損のバグは、次の理由によって診断および再現が特に困難となります。
- これらの問題は長い間非表示のままであり、関連のないコードの変更またはランタイムの更新後にのみマニフェストが表示される可能性があります。
- 多くの場合、特定の場所での実行を中断する GC やヒープ圧縮の開始など、再現には正確なタイミングが必要です。これはまれで非決定的なイベントです。
次のセクションでは、DO と ❌ DON'T の推奨事項を使用✔️した一般的な安全でないパターンについて説明します。
1. 追跡されていないマネージドポインター (Unsafe.AsPointer および関連要素)
マネージド (追跡済み) ポインターを、安全な C# のアンマネージド (追跡されていない) ポインターに変換することはできません。 このようなニーズが発生した場合は、Unsafe.AsPointer<T>(T) ステートメントのオーバーヘッドを回避するためにfixedを使用したくなる可能性があります。 そのための有効なユース ケースがありますが、移動可能なオブジェクトへの追跡されないポインターを作成するリスクが生じます。
例:
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
nativePointer[0] = 42;
}
ポインターが読み取られた直後に GC によって UnreliableCode メソッドの実行が中断され ( xによって参照されるアドレス)、参照先オブジェクトが再配置された場合、GC は x に格納されている場所を正しく更新しますが、 nativePointer について何も認識されず、格納されている値は更新されません。 その時点で、 nativePointer への書き込みは任意のメモリに書き込まれます。
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
// <-- GC happens here between the two lines of code and updates `x` to point to a new location.
// However, `nativePointer` still points to the old location as it's not reported to the GC
nativePointer[0] = 42; // Potentially corrupting write, access violation, or other issue.
}
GC は、メソッドの実行を再開すると、 xの古い場所に 42 を書き込みます。これは、予期しない例外、一般的なグローバル状態の破損、またはアクセス違反によるプロセス終了につながる可能性があります。
代わりに、 fixed キーワードと & address-of 演算子を使用して、操作中に GC がターゲット参照を再配置できないようにすることをお勧めします。
unsafe void ReliableCode(ref int x)
{
fixed (int* nativePointer = &x) // `x` cannot be relocated for the duration of this block.
{
nativePointer[0] = 42;
}
}
推奨事項
-
❌
ref X引数について、スタック割り当て、固有化、またはGCによって再配置できないという暗黙のコントラクトがある場合は、Xを使用しないでください。 プレーン オブジェクトとスパンにも同じことが当てはまります。メソッドシグネチャでの有効期間に関する明白でない呼び出し元ベースのコントラクトは導入しないでください。 代わりに ref 構造体 引数を取るか、引数を生ポインター型 (X*) に変更することを検討してください。 - ❌ 元のオブジェクトの存続期間を超える可能性がある場合、Unsafe.AsPointer<T>(T) からのポインタを使用しないでください。 API のドキュメントでは、GC が参照を再配置できないことを保証するには、 Unsafe.AsPointer<T>(T) の呼び出し元が必要です。 呼び出し元がこの前提条件を満たしていることをコード レビュー担当者に明確に確認します。
- ✔️ の代わりに
fixedまたは Unsafe.AsPointer<T>(T) スコープを使用して、アンマネージ ポインターの明示的なスコープを定義し、オブジェクトが常に固定されるようにします。 - ✔️ 配列を特定の境界に配置する必要がある場合は、byref ではなくアンマネージ ポインター (
fixed) を使用してください。 これにより、GC によってオブジェクトが再配置されなくなり、ロジックが依存する可能性のあるアラインメントの前提条件が無効になります。
2. fixed スコープ外のポインターの公開
固定キーワードはピン留めされたオブジェクトから取得されたポインターのスコープを定義しますが、そのポインターがfixedスコープをエスケープしてバグを発生させる可能性があります。C# では所有権やライフサイクルの保護が提供されないためです。
一般的な例を次に示します。
unsafe int* GetPointerToArray(int[] array)
{
fixed (int* pArray = array)
{
_ptrField = pArray; // Bug!
Method(pArray); // Bug if `Method` allows `pArray` to escape,
// perhaps by assigning it to a field.
return pArray; // Bug!
// And other ways to escape the scope.
}
}
この例では、配列は fixed キーワードを使用して適切にピン留めされますが (GC が fixed ブロック内で再配置できないようにします)、ポインターは fixed ブロックの外部に公開されます。 これにより、逆参照によって未定義の動作が発生するダングリングポインタが作成されます。
推奨事項
- ✔️
fixedブロック内のポインターが定義されたスコープから離れないことを確認します。 - ✔️ C#の ref 構造体など、組み込みのエスケープ分析を使用して安全な低レベルのプリミティブを優先します。 詳細については、「 低レベルの構造体の機能強化」を参照してください。
3. ランタイムとライブラリの内部実装の詳細
内部実装の詳細にアクセスしたり、内部実装の詳細に依存したりすることは一般的には不適切ですが (.NET ではサポートされていません)、一般的に観察される特定のケースを呼び出す価値があります。 これは、コードが内部実装の詳細に不適切に依存している場合に間違って発生する可能性のあるすべてのことを網羅したリストではありません。
推奨事項
❌ オブジェクトのヘッダーの一部を変更したり読み取ったりしないでください。
- オブジェクト ヘッダーは、ランタイムによって異なる場合があります。
- CoreCLR では、最初にオブジェクトをピン留めしないと、オブジェクト ヘッダーに安全にアクセスできません。
- MethodTable ポインターを変更してオブジェクトの型を変更しないでください。
❌ オブジェクトのパディングにデータを格納しないでください。 埋め込みコンテンツが保持されるか、パディングが既定で常にゼロになると想定しないでください。
❌ シーケンシャルまたは明示的なレイアウトを持つプリミティブおよび構造体以外のサイズとオフセットについては、想定しないでください。 その場合でも、GC ハンドルが関係する場合など、例外が存在します。
❌ 非パブリック メソッドを呼び出したり、非パブリック フィールドにアクセスしたり、リフレクションまたは安全でないコードを使用して BCL 型の読み取り専用フィールドを変更したりしないでください。
❌ BCL 内の特定の非パブリック メンバーが常に存在するか、特定の図形を持つものと想定しないでください。 .NET チームは、サービス リリースで非パブリック API を変更または削除することがあります。
❌ リフレクションまたは安全でないコードを使用して
static readonlyフィールドを変更しないでください。これは定数であると見なされるためです。 たとえば、RyuJIT は通常、明示的な定数としてインライン化します。❌ 参照が再割り当て不可能であると仮定しないでください。 このガイダンスは、文字列および UTF-8 (
"..."u8) リテラル、静的フィールド、RVA フィールド、LOH オブジェクトなどにも適用されます。- これらはランタイム実装の詳細であり、一部のランタイムには保持される可能性がありますが、他のランタイムには保持されません。
- このようなオブジェクトへのアンマネージ ポインターは、アセンブリのアンロードを妨げることはなく、その結果、ポインターがダングリングになる可能性があります。
fixedスコープを使用して、正確性を確保します。
ReadOnlySpan<int> rva = [1, 2, 4, 4]; int* p = (int*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(rva)); // Bug! The assembly containing the RVA field might be unloaded at this point // and `p` becomes a dangling pointer. int value = p[0]; // Access violation or other issue.❌ 特定のランタイムの実装の詳細に依存するコードを記述しないでください。
4. 無効なマネージド ポインター (逆参照されていなくても)
コードの特定のカテゴリでは、ポインター操作と算術演算が行われ、多くの場合、アンマネージ ポインター (T* p) とマネージド ポインター (ref T p) の使用を選択できます。
これらのポインターは、たとえば、アンマネージ ポインター (p++) の演算子を介して、およびマネージド ポインター (Unsafe) のp = ref Unsafe.Add(ref p, 1)メソッドを使用して、任意に操作できます。 どちらも "安全でないコード" と見なされ、両方で信頼性の低いパターンを作成できます。 ただし、特定のアルゴリズムでは、マネージド ポインターを操作するときに誤って GC アンセーフ パターンを作成する方が簡単な場合があります。 アンマネージ ポインターは GC によって追跡されないため、含まれる値は開発者のコードによって逆参照された場合にのみ関連します。 これに対し、マネージド ポインターの値は、開発者のコードによって逆参照される場合だけでなく、GC によって検査される場合にも関連します。 したがって、開発者は逆参照されていない限り、無効なアンマネージ ポインターを結果なしで作成できますが、無効なマネージド ポインターを作成することはバグです。 例:
unsafe void UnmanagedPointers(int[] array)
{
fixed (int* p = array)
{
int* invalidPtr = p - 1000;
// invalidPtr is pointing to an undefined location in memory
// it's ok as long as it's not dereferenced.
int* validPtr = invalidPtr + 1000; // Returning back to the original location
*validPtr = 42; // OK
}
}
ただし、byrefs (マネージド ポインター) を使用する同様のコードは無効です。
void ManagedPointers_Incorrect(int[] array)
{
ref int invalidPtr = ref Unsafe.Add(ref array[0], -1000); // Already a bug!
ref int validPtr = ref Unsafe.Add(ref invalidPtr, 1000);
validPtr = 42; // possibly corrupting write
}
ここでのマネージド実装では、わずかなピン留めオーバーヘッドを回避できますが、invalidPtrの実際のアドレスがGCによって更新されている間にが外部ポインターになる可能性があるため、これは不安定です。
このようなバグは微妙であり、 .NET でも開発中にそれらの問題が発生しています 。
推奨事項
-
❌ 無効なマネージド ポインターは、逆参照されていない場合や、実行されないコード パス内に配置されている場合でも、作成しないでください。
- 有効なマネージド ポインターを構成する内容の詳細については、 ECMA-335、Sec. II.14.4.2 マネージド ポインターを参照してください。および ECMA-335 CLI 仕様補遺、Sec. II.14.4.2。
- ✔️ アルゴリズムでこのような操作が必要な場合は、固定されたアンマネージ ポインターを使用してください。
5. 再解釈に似た型キャスト
すべての種類の構造体からクラスへのキャストまたはクラスから構造体へのキャストは定義上未定義の動作ですが、構造体から構造体への変換やクラスからクラスへの変換で信頼性の低いパターンが発生する可能性もあります。 信頼性の低いパターンの一般的な例は、次のコードです。
struct S1
{
string a;
nint b;
}
struct S2
{
string a;
string b;
}
S1 s1 = ...
S2 s2 = Unsafe.As<S1, S2>(ref s1); // Bug! A random nint value becomes a reference reported to the GC.
また、レイアウトが似ている場合でも、GC 参照 (フィールド) が関係する場合は注意する必要があります。
推奨事項
- ❌ 構造体をクラスに、またはクラスを構造体にキャストしないでください。
-
❌ キャストが有効であることが確実でない限り、構造体から構造体への変換またはクラスからクラスへの変換には
Unsafe.Asを使用しないでください。 詳細については、のUnsafe.As」セクションを参照してください。 - ✔️ このような変換では、フィールドごとのコピー、 AutoMapper などの外部ライブラリ、ソース ジェネレーターの方が安全です。
- ✔️
Unsafe.BitCastでは基本的な使用状況チェックが提供されるため、Unsafe.AsよりもBitCastを優先してください。 これらのチェックは完全な正確性の保証を提供しないことに注意してください。つまり、BitCastは安全でない API と見なされます。
6. GC 参照に対する書き込みバリアと非アトミック操作のバイパス
通常、GC 参照のすべての種類の書き込みまたは読み取りは常にアトミックです。 また、GC 参照 (または GC フィールドを持つ構造体への byref) を潜在的なヒープ位置に割り当てようとするすべての試行は、オブジェクト間の新しい接続を GC が認識することを保証する書き込みバリアを通過します。 ただし、安全でないコードを使用すると、これらの保証をバイパスし、信頼性の低いパターンを導入できます。 例:
unsafe void InvalidCode1(object[] arr1, object[] arr2)
{
fixed (object* p1 = arr1)
fixed (object* p2 = arr2)
{
nint* ptr1 = (nint*)p1;
nint* ptr2 = (nint*)p2;
// Bug! We're assigning a GC pointer to a heap location
// without going through the Write Barrier.
// Moreover, we also bypass array covariance checks.
*ptr1 = *ptr2;
}
}
同様に、マネージド ポインターを含む次のコードも信頼できません。
struct StructWithGcFields
{
object a;
int b;
}
void InvalidCode2(ref StructWithGcFields dst, ref StructWithGcFields src)
{
// It's already a bad idea to cast a struct with GC fields to `ref byte`, etc.
ref byte dstBytes = ref Unsafe.As<StructWithGcFields, byte>(ref dst);
ref byte srcBytes = ref Unsafe.As<StructWithGcFields, byte>(ref src);
// Bug! Bypasses the Write Barrier. Also, non-atomic writes/reads for GC references.
Unsafe.CopyBlockUnaligned(
ref dstBytes, ref srcBytes, (uint)Unsafe.SizeOf<StructWithGcFields>());
// Bug! Same as above.
Vector128.LoadUnsafe(ref srcBytes).StoreUnsafe(ref dstBytes);
}
推奨事項
- ❌ GC 参照に対して非アトミック操作を使用しないでください (たとえば、SIMD 操作では多くの場合、それらを提供しません)。
- ❌ アンマネージ ポインターを使用して GC 参照をヒープの場所に格納しないでください (書き込みバリアを省略)。
7. オブジェクトの有効期間に関する前提条件 (ファイナライザー、 GC.KeepAlive)
GC の観点からオブジェクトの有効期間に関する想定は避けてください。 具体的には、オブジェクトが存在しない可能性がある場合は、オブジェクトがまだ有効であると想定しないでください。 オブジェクトの有効期間は、異なるランタイム間、または同じメソッドの異なる階層 (RyuJIT の Tier0 と Tier1) によって異なる場合があります。 ファイナライザーは、このような前提条件が正しくない可能性がある一般的なシナリオです。
public class MyClassWithBadCode
{
public IntPtr _handle;
public void DoWork() => DoSomeWork(_handle); // A use-after-free bug!
~MyClassWithBadCode() => DestroyHandle(_handle);
}
// Example usage:
var obj = new MyClassWithBadCode()
obj.DoWork();
この例では、DestroyHandleが完了する前または開始する前に、DoWorkが呼び出されることがあります。
したがって、 thisなどのオブジェクトがメソッドの最後まで存続すると想定しないことが重要です。
void DoWork()
{
// A pseudo-code of what might happen under the hood:
IntPtr reg = this._handle;
// 'this' object is no longer alive at this point.
// <-- GC interrupts here, collects the 'this' object, and triggers its finalizer.
// DestroyHandle(_handle) is called.
// Bug! 'reg' is now a dangling pointer.
DoSomeWork(reg);
// You can resolve the issue and force 'this' to be kept alive (thus ensuring the
// finalizer will not run) by uncommenting the line below:
// GC.KeepAlive(this);
}
そのため、 GC.KeepAlive(Object) または SafeHandleを使用して、オブジェクトの有効期間を明示的に延長することをお勧めします。
この問題のもう 1 つのクラシック インスタンスは、 Marshal.GetFunctionPointerForDelegate<TDelegate>(TDelegate) API です。
var callback = new NativeCallback(OnCallback);
// Convert delegate to function pointer
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
// Bug! The delegate might be collected by the GC here.
// It should be kept alive until the native code is done with it.
RegisterCallback(fnPtr);
推奨事項
-
❌ オブジェクトの有効期間について想定しないでください。 たとえば、
thisがメソッドの最後まで常に有効であると想定しないでください。 - ✔️ ネイティブ リソースを管理するために SafeHandle を使用してください。
- ✔️ GC.KeepAlive(Object) を使用して、必要に応じてオブジェクトの有効期間を延長してください。
8. ローカル変数へのクロススレッド アクセス
通常、別のスレッドからローカル変数にアクセスすることは、不適切な方法と見なされます。 ただし、 .NET メモリ モデルで説明されているように、マネージド参照が関係する場合は、明示的に未定義の動作になります。
例: GC 参照を含む構造体は、別のスレッドが読み取っている間に、no-GC 領域内でスレッド セーフでない方法でゼロまたは上書きされ、未定義の動作が発生する可能性があります。
推奨事項
- ❌ スレッド間でローカルにアクセスしないでください (特に GC 参照が含まれている場合)。
- ✔️ 代わりにヒープまたはアンマネージ メモリ ( NativeMemory.Alloc など) を使用してください。
9. 安全でない境界チェックの削除
C# では、すべての慣用メモリ アクセスに既定で境界チェックが含まれます。 JIT コンパイラは、次の例のように、これらのチェックが不要であることを証明できる場合に削除できます。
int SumAllElements(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
// The JIT knows that within this loop body, i >= 0 and i < array.Length.
// The JIT can reason that its own bounds check would be duplicative and
// unnecessary, so it opts not to emit the bounds check into the final
// generated code.
sum += array[i];
}
}
JIT はこのようなパターンを認識する際に継続的に改善されていますが、チェックを実行したままにするシナリオが残り、ホット コードのパフォーマンスに影響を与える可能性があります。 このような場合、リスクを完全に理解したり、パフォーマンス上の利点を正確に評価したりすることなく、安全でないコードを使用してこれらのチェックを手動で削除したくなる場合があります。
たとえば、次のメソッドを考えてみましょう。
int FetchAnElement(int[] array, int index)
{
return array[index];
}
JIT が index が常に array の範囲内にあることを証明できない場合、メソッドは次のように書き換えられます。
int FetchAnElement_AsJitted(int[] array, int index)
{
if (index < 0 || index >= array.Length)
throw new IndexOutOfBoundsException();
return array.GetElementAt(index);
}
ホット コードでのチェックインによるオーバーヘッドを減らすために、安全でない同等の API (Unsafe と MemoryMarshal) を使用したくなる可能性があります。
int FetchAnElement_Unsafe1(int[] array, int index)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}
または、ピン留めポインターと生ポインターを使用します。
unsafe int FetchAnElement_Unsafe2(int[] array, int index)
{
fixed (int* pArray = array)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return pArray[index];
}
}
indexがarrayの範囲外にあると、ランダムなクラッシュや状態の破損につながる可能性があります。
このような安全でない変換は、非常にホットなパスでパフォーマンス上の利点を得ることができますが、多くの場合、これらの利点は一時的なものです。各 .NET リリースでは、安全な場合に不要な境界チェックを排除する JIT の機能が向上するためです。
推奨事項
- ✔️ .NET の最新バージョンが引き続き境界チェックを排除できないかどうかを確認してください。 可能であれば、安全なコードを使用して書き直します。 それ以外の場合は、RyuJIT に対して問題を提出してください。 この追跡の問題を出発点として使用してください。
- ✔️ 実際のパフォーマンスへの影響を測定します。 パフォーマンスの向上がごくわずかであるか、コードが単純なマイクロベンチマークの外部でホットであることが証明されていない場合は、安全なコードを使用して書き換えます。
- ✔️ .NET メモリ モデル では、一部のシナリオで JIT によって境界チェックが削除されるのを控えめに防ぐ可能性があるため、ループ前の手動の境界チェックやローカルへのフィールドの保存など、JIT に追加のヒントを提供してください。
- ✔️ 必ず
Debug.Assert境界チェックでコードを保護し、どうしても危険なコードが必要な場合は使用してください。 次の例を考えてみましょう。
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
// Unsafe code here
これらのチェックを再利用可能なヘルパー メソッドにリファクタリングすることもできます。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static T UnsafeGetElementAt<T>(this T[] array, int index)
{
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}
Debug.Assertを含めると、リリース ビルドのサウンド チェックは提供されませんが、デバッグ ビルドの潜在的なバグを検出するのに役立つ場合があります。
10. メモリ アクセスの結合
パフォーマンスを向上させるために、安全でないコードを使用してメモリ アクセスを結合したくなる可能性があります。
従来の例は、char 配列に "False" を書き込む次のコードです。
// Naive implementation
static void WriteToDestination_Safe(char[] dst)
{
if (dst.Length < 5) { throw new ArgumentException(); }
dst[0] = 'F';
dst[1] = 'a';
dst[2] = 'l';
dst[3] = 's';
dst[4] = 'e';
}
// Unsafe coalesced implementation
static void WriteToDestination_Unsafe(char[] destination)
{
Span<char> dstSpan = destination;
if (dstSpan.Length < 5) { throw new ArgumentException(); }
ulong fals_val = BitConverter.IsLittleEndian ? 0x0073006C00610046ul : 0x00460061006C0073ul;
MemoryMarshal.Write(MemoryMarshal.AsBytes(dstSpan.Slice(0, 4)), in fals_val); // Write "Fals" (4 chars)
dstSpan[4] = 'e'; // Write "e" (1 char)
}
以前のバージョンの .NET では、 MemoryMarshal を使用する安全でないバージョンは、単純な安全なバージョンよりも測定的に高速でした。 ただし、最新バージョンの .NET には、両方のケースで同等の codegen を生成する、大幅に改善された JIT が含まれています。 .NET 10 の時点では、x64 codegen は次のようになります。
; WriteToDestination_Safe
cmp eax, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rax, 0x73006C00610046
mov qword ptr [rdi+0x10], rax
mov word ptr [rdi+0x18], 101
; WriteToDestination_Unsafe
cmp edi, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rdi, 0x73006C00610046
mov qword ptr [rax], rdi
mov word ptr [rax+0x08], 101
コードのさらにシンプルで読みやすいバージョンがあります。
"False".CopyTo(dst);
.NET 10 の時点では、この呼び出しでは上記と同じ codegen が生成されます。 追加の利点もあります。要素ごとの厳密な書き込みをアトミックにする必要はないことを JIT に示唆します。 JIT では、このヒントを他のコンテキスト知識と組み合わせて、ここで説明した以上の最適化を提供できます。
推奨事項
- ✔️ メモリアクセスの結合には、安全で慣用的なコードを使用し、危険なコードは避けてください。
- データのコピーには
Span<T>.CopyToとSpan<T>.TryCopyToを使用します。 - (
String.Equalsを使用する場合でも) データを比較するために、Span<T>.SequenceEqualとStringComparer.OrdinalIgnoreCaseを優先します。 - データを入力する場合は
Span<T>.Fill、データをクリアするにはSpan<T>.Clearを使用します。 - 要素単位またはフィールドごとの書き込み/読み取りは、JIT によって自動的に結合される可能性があることに注意してください。
- データのコピーには
- ✔️ 慣用コードを記述し、想定どおりに最適化されていないことを確認した場合は、 dotnet/runtime に対して問題を提出してください。
- ❌ メモリ アクセスのリスク、アトミック性の保証、または関連するパフォーマンス上の利点が不明な場合は、メモリ アクセスを手動で結合しないでください。
11. アライメントされていないメモリ アクセス
メモリ アクセス合体で説明されている メモリ アクセス合体 では、多くの場合、明示的または暗黙的な読み取り/書き込みが不適切になります。 通常、これは重大な問題を引き起こしませんが (キャッシュとページの境界を越えてパフォーマンスが低下する可能性は除きます)、実際のリスクが引き続き発生します。
たとえば、配列の 2 つの要素を一度にクリアするシナリオを考えてみましょう。
uint[] arr = _arr;
arr[i + 0] = 0;
arr[i + 1] = 0;
これらの場所の前の値が両方とも uint.MaxValue (0xFFFFFFFF) であったとします。
.NET メモリ モデルでは、両方の書き込みがアトミックであることを保証するため、プロセス内の他のすべてのスレッドは、新しい値0または古い値0xFFFFFFFFのみを観察し、0xFFFF0000のような値を "破損" することはありません。
ただし、次の安全でないコードを使用して境界チェックをバイパスし、1 つの 64 ビット ストアで両方の要素を 0 個使用するとします。
ref uint p = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(arr), i);
Unsafe.WriteUnaligned<ulong>(ref Unsafe.As<uint, byte>(ref p), 0UL);
このコードには、原子性の保証を削除する副作用があります。 破損した値が他のスレッドによって観察され、未定義の動作が発生する可能性があります。 このような結合された書き込みをアトミックにするには、メモリを書き込みのサイズ (この場合は 8 バイト) に合わせる必要があります。 操作の前にメモリを手動で配置しようとする場合は、GC が固定されていない場合は、いつでも配列を再配置 (および効果的に、配置を変更) できることを考慮する必要があります。 詳細については、 .NET メモリ モデル のドキュメントを参照してください。
アライメントされていないメモリ アクセスのもう 1 つのリスクは、特定のシナリオでアプリケーションがクラッシュする可能性があることです。 一部の .NET ランタイムは、不適切なアクセスを修正するために OS に依存していますが、一部のプラットフォームでは、アクセスが不適切な場合に DataMisalignedException (または SEHException) につながる可能性があるシナリオがあります。 例の一部を次に示します。
-
Interlocked一部のプラットフォームでは、メモリの位置がずれている場合に操作が行われます (例)。 - ARM での浮動小数点演算の位置がずれている。
- 特定のアラインメント要件を持つ特殊なデバイス メモリへのアクセス (.NET では実際にはサポートされていません)。
推奨事項
- ❌ ロックフリーアルゴリズムやアトミック性が重要なその他のシナリオでは、アライメントされていないメモリ アクセスを使用しないでください。
- ✔️ 必要に応じてデータを手動で配置しますが、GC はいつでもオブジェクトを再配置でき、配置を動的に効果的に変更できることに注意してください。 これは、SIMD のさまざまな
StoreAligned/LoadAlignedAPI で特に重要です。 - ✔️ Unsafe.ReadUnaligned/Unsafe.WriteUnalignedなどのアラインメントされた API ではなく、Unsafe.Read<T>(Void*)/Unsafe.Write<T>(Void*, T)などの明示的にアラインされていない読み取り/書き込み API を使用するか、データがずれている可能性がある場合はUnsafe.As<TFrom,TTo>(TFrom)してください。
- ✔️ Span<T>.CopyTo(Span<T>) などのさまざまなメモリ操作 API では、アトミック性の保証も提供されない点に注意してください。
- ✔️ 原子性の保証の詳細については、 .NET メモリ モデル のドキュメント (リファレンスを参照) を参照してください。
- ✔️ 一部のプラットフォームでは、アライメントされていないメモリ アクセスに対してパフォーマンスの大幅な低下が課されるため、すべてのターゲット プラットフォームのパフォーマンスを測定します。 これらのプラットフォームでは、単純なコードは賢いコードよりも優れたパフォーマンスを発揮します。
- ✔️ 非アライン化メモリ アクセスによって例外を引き起こす可能性があるシナリオやプラットフォームが存在することに注意してください。
12. パディングまたは「非ブリッタブル」メンバーを含む構造体のバイナリ(デ)シリアル化
さまざまなシリアル化のような API を使用して、バイト配列との間で構造体をコピーまたは読み取る場合は注意してください。
構造体にパディングやブリッタブルでないメンバー (bool や GC フィールドなど) が含まれている場合、Fill、CopyTo、SequenceEqual などの以前からある安全性の低いメモリ操作によって、スタックからパディングに誤って機密データをコピーしたり、比較中に不要データを重要なものとして扱ったりする可能性があり、これが原因で再現が困難なバグが発生する可能性があります。 一般的なアンチパターンは次のようになります。
T UnreliableDeserialization<TObject>(ReadOnlySpan<byte> data) where TObject : unmanaged
{
return MemoryMarshal.Read<TObject>(data); // or Unsafe.ReadUnaligned
// BUG! TObject : unmanaged doesn't guarantee that TObject is blittable and contains no paddings.
}
唯一の正しい方法は、各 TObject 入力に特化したフィールドごとの読み込み/格納を使用することです (またはリフレクション、ソース ジェネレーター、または (de) シリアル化ライブラリを使用して一般化します)。
推奨事項
-
❌ パディングや非ブリッタブルなメンバーを持つ構造体のコピー/読み込み/比較に、安全でないコードを使用しないでください。 信頼されていない入力からの読み込みは、
boolやdecimalなどの基本型の場合でも問題になります。 同時に、ストアはスタックの機密情報を構造体のギャップ/パディングに誤ってシリアル化する可能性があります。 -
❌ ジェネリック型がビットごとの操作を実行しても安全であることを保証するために、
T : unmanaged制約、RuntimeHelpers.IsReferenceOrContainsReferences、または同様の API に依存しないでください。 これらのガイドラインを書いている時点では、特定の型に対して任意のビット演算を実行することが有効かどうかを判断するための信頼性の高いプログラムによる方法はありません。- このようなビットごとの操作を実行する必要がある場合は、このハードコーディングされた型のリストに対してのみ実行し、現在のマシンのエンディアンに注意してください。
- プリミティブ整数型
Byte、SByte、Int16、UInt16、Int32、UInt32、Int64、およびUInt64。 - 上記のプリミティブ整数型のいずれかを基にした
Enum。 -
Char、Int128、UInt128、Half、Single、Double、IntPtr、UIntPtr。
- プリミティブ整数型
- このようなビットごとの操作を実行する必要がある場合は、このハードコーディングされた型のリストに対してのみ実行し、現在のマシンのエンディアンに注意してください。
- ✔️ 代わりに、フィールドごとの読み書きによる(デ)シリアル化を使用してください。 シリアル化およびデシリアライズ化において、一般的で安全なライブラリを使用することを検討してください。
13. ヌルマネージドポインター
一般に、byref (マネージド ポインター) は null になることはほとんどありません。現在、null byref を作成する唯一の安全な方法は、ref structを使用してdefaultを初期化することです。 その後、そのすべての ref フィールドは null マネージド ポインターです。
RefStructWithRefField s = default;
ref byte nullRef = ref s.refFld;
ただし、null byrefs を作成する安全でない方法はいくつかあります。 いくつかの例を次に示します。
// Null byref by calling Unsafe.NullRef directly:
ref object obj = ref Unsafe.NullRef<object>();
// Null byref by turning a null unmanaged pointer into a null managed pointer:
ref object obj = ref Unsafe.AsRef<object>((void*)0);
メモリの安全性の問題が発生するリスクは低く、null byref を逆参照しようとすると、 NullReferenceException が明確に定義されます。 ただし、C# コンパイラでは、byref の逆参照は常に成功し、観察可能な副作用は生じないことを 前提としています 。 したがって、結果の値がすぐに破棄される逆参照を省略することは、合法的な最適化です。 .NET 内で修正されたバグの例については 、dotnet/runtime#98681 (および この関連コメント) を参照してください。ライブラリ コードが副作用をトリガーする逆参照に不適切に依存しており、C# コンパイラが目的のロジックを効果的に短絡していることを認識しません。
推奨事項
- ❌ 必要がない場合は、C# で null byrefs を作成しないでください。 代わりに、通常のマネージド参照、 Null オブジェクト パターン、または空のスパンを使用することを検討してください。
- ❌ byref 逆参照の結果は、最適化によって無視される可能性があり、それが潜在的なバグにつながることもあるため、破棄しないでください。
14. stackalloc
stackalloc は、これまではスタック上で小さなエスケープしない配列を作成し、GC 圧力を低下させるために使用されていました。 将来的には、JIT のエスケープ分析によって、オブジェクトをスタックする配列のエスケープされていない GC 割り当ての最適化が開始され、 stackalloc 冗長になる可能性があります。 それまでは、 stackalloc はスタックに小さなバッファーを割り当てるのに役立ちます。 バッファーのサイズが大きい場合やエスケープする場合は、ArrayPool<T>と組み合わされます。
推奨事項
✔️ 常に
stackallocを式の左側のReadOnlySpan<T>/Span<T>に消費して、境界チェックを行います。// Good: Span<int> s = stackalloc int[10]; s[2] = 0; // Bounds check is eliminated by JIT for this write. s[42] = 0; // IndexOutOfRangeException is thrown // Bad: int* s = stackalloc int[10]; s[2] = 0; s[42] = 0; // Out of bounds write, undefined behavior.❌ ループ内で
stackallocを使用しないでください。 スタック領域はメソッドが戻るまで再利用されないため、ループ内にstackallocを含めると、スタック オーバーフローが原因でプロセスが終了する可能性があります。❌
stackallocには大きな長さを使用しないでください。 たとえば、1024 バイトは妥当な上限と見なされます。✔️
stackallocの長さとして使用される変数の範囲を確認してください。void ProblematicCode(int length) { Span<int> s = stackalloc int[length]; // Bad practice: check the range of `length`! Consume(s); }修正バージョン:
void BetterCode(int length) { // The "throw if length < 0" check below is important, as attempting to stackalloc a negative // length will result in process termination. ArgumentOutOfRangeException.ThrowIfLessThan(length, 0, nameof(length)); Span<int> s = length <= 256 ? stackalloc int[length] : new int[length]; // Or: // Span<int> s = length <= 256 ? stackalloc int[256] : new int[length]; // Which performs a faster zeroing of the stackalloc, but potentially consumes more stack space. Consume(s); }✔️ 可能な限り手動でメモリを管理しないように、コレクション リテラル (
Span<int> s = [1, 2, 3];)、params Span<T>、インライン配列などの最新の C# 機能を使用してください。
15. 固定サイズバッファー
固定サイズのバッファーは、他の言語またはプラットフォームのデータ ソースとの相互運用シナリオに役立ちました。 その後、より安全で便利な インライン配列に置き換えられました。
固定サイズ バッファー ( unsafe コンテキストが必要) の例を次に示します。
public struct MyStruct
{
public unsafe fixed byte data[8];
// Some other fields
}
MyStruct m = new();
ms.data[10] = 0; // Out-of-bounds write, undefined behavior.
最新で安全な代替手段は、 インライン配列です。
[System.Runtime.CompilerServices.InlineArray(8)]
public struct Buffer
{
private int _element0; // can be generic
}
public struct MyStruct
{
public Buffer buffer;
// Some other fields
}
MyStruct ms = new();
ms.buffer[i] = 0; // Runtime performs a bounds check on index 'i'; could throw IndexOutOfRangeException.
ms.buffer[7] = 0; // Bounds check elided; index is known to be in range.
ms.buffer[10] = 0; // Compiler knows this is out of range and produces compiler error CS9166.
既定では常にゼロ初期化されるインライン配列を優先して固定サイズバッファーを回避するもう 1 つの理由は、特定のシナリオでは、固定サイズバッファーにゼロ以外の内容が含まれる可能性があることです。
推奨事項
- ✔️ 可能な場合は、固定サイズのバッファーをインライン配列または IL マーシャリング属性に置き換えることを推奨します。
16. 連続したデータをポインター + 長さとして渡す (またはゼロ終端に依存する)
連続するデータへのアンマネージ ポインターまたはマネージド ポインターを受け入れる API の定義は避けてください。 代わりに、 Span<T> または ReadOnlySpan<T>を使用します。
// Poor API designs:
void Consume(ref byte data, int length);
void Consume(byte* data, int length);
void Consume(byte* data); // zero-terminated
void Consume(ref byte data); // zero-terminated
// Better API designs:
void Consume(Span<byte> data);
void Consume(Memory<byte> data);
void Consume(byte[] data);
void Consume(byte[] data, int offset, int length);
ゼロ終端は特に危険です。すべてのバッファーが 0 で終了するわけではないため、ゼロターミネータを超えて読み取る場合は、情報漏えい、データ破損、またはアクセス違反によるプロセス終了につながる可能性があります。
推奨事項
❌ これらの引数がバッファーを表すことを意図している場合は、引数がポインター型 (アンマネージ ポインター
T*またはマネージド ポインターref T) であるメソッドを公開しないでください。 代わりに、Span<T>やReadOnlySpan<T>などの安全なバッファーの種類を使用してください。❌ すべての呼び出し元がスタックに入力を割り当てる必要があるなど、byref 引数に暗黙的なコントラクトを使用しないでください。 このようなコントラクトが必要な場合は、代わりに ref 構造体 を使用することを検討してください。
❌ このシナリオでこれが有効な前提であることを明示的に文書化しない限り、バッファーが 0 で終わるとは想定しないでください。 たとえば、.NET では、
stringインスタンスと"..."u8リテラルが null で終了することを保証しますが、ReadOnlySpan<char>やchar[]などの他のバッファー型も同じです。unsafe void NullTerminationExamples(string str, ReadOnlySpan<char> span, char[] array) { Debug.Assert(str is not null); Debug.Assert(array is not null); fixed (char* pStr = str) { // OK: Strings are always guaranteed to have a null terminator. // This will assign the value '\0' to the variable 'ch'. char ch = pStr[str.Length]; } fixed (char* pSpan = span) { // INCORRECT: Spans aren't guaranteed to be null-terminated. // This could throw, assign garbage data to 'ch', or cause an AV and crash. char ch = pSpan[span.Length]; } fixed (char* pArray = array) { // INCORRECT: Arrays aren't guaranteed to be null-terminated. // This could throw, assign garbage data to 'ch', or cause an AV and crash. char ch = pArray[array.Length]; } }❌ 明示的な長さ引数も渡していない限り、ピン留めされた
Span<char>またはReadOnlySpan<char>を p/invoke 境界を越えて渡さないでください。 それ以外の場合、p/invoke 境界の反対側のコードは、バッファーが null 終端であると誤って信じる可能性があります。
unsafe static extern void SomePInvokeMethod(char* pwszData);
unsafe void IncorrectPInvokeExample(ReadOnlySpan<char> data)
{
fixed (char* pData = data)
{
// INCORRECT: Since 'data' is a span and is not guaranteed to be null-terminated,
// the receiver might attempt to keep reading beyond the end of the buffer,
// resulting in undefined behavior.
SomePInvokeMethod(pData);
}
}
これを解決するには、可能であればデータ ポインターと長さの両方を受け入れる代替の p/invoke シグネチャを使用します。 それ以外の場合、受信側が別の長さの引数を受け入れる方法がない場合は、元のデータをピン留めして p/invoke 境界を越えて渡す前に、元のデータが string に変換されていることを確認します。
unsafe static extern void SomePInvokeMethod(char* pwszData);
unsafe static extern void SomePInvokeMethodWhichTakesLength(char* pwszData, uint cchData);
unsafe void CorrectPInvokeExample(ReadOnlySpan<char> data)
{
fixed (char* pData = data)
{
// OK: Since the receiver accepts an explicit length argument, they're signaling
// to us that they don't expect the pointer to point to a null-terminated buffer.
SomePInvokeMethodWhichTakesLength(pData, (uint)data.Length);
}
// Alternatively, if the receiver doesn't accept an explicit length argument, use
// ReadOnlySpan<T>.ToString to convert the data to a null-terminated string before
// pinning it and sending it across the p/invoke boundary.
fixed (char* pStr = data.ToString())
{
// OK: Strings are guaranteed to be null-terminated.
SomePInvokeMethod(pStr);
}
}
17. 文字列の変異
C# の文字列は設計上不変であり、安全でないコードを使用して変更しようとすると、未定義の動作が発生する可能性があります。 例:
string s = "Hello";
fixed (char* p = s)
{
p[0] = '_';
}
Console.WriteLine("Hello"); // prints "_ello" instead of "Hello"
インターンされた文字列 (ほとんどの 文字列リテラル) を変更すれば、他のすべての使用時の値が変更されます。 文字列のインターンがなくても、新しく作成された文字列への書き込みは、より安全な String.Create API に置き換える必要があります。
// Bad:
string s = new string('\n', 4); // non-interned string
fixed (char* p = s)
{
// Copy data into the newly created string
}
// Good:
string s = string.Create(4, state, (chr, state) =>
{
// Copy data into the newly created string
});
推奨事項
-
❌ 文字列を変更しないでください。 複雑なコピー ロジックが必要な場合は、
String.CreateAPI を使用して新しい文字列を作成します。 それ以外の場合は、.ToString()、StringBuilder、new string(...)、または文字列補間構文を使用します。
18. 生の IL コード (System.Reflection.Emit や Mono.Cecil など)
( System.Reflection.Emit、 Mono.Cecilなどのサードパーティ製ライブラリを介して、または IL コードを直接書き込む) 生の IL を定義によって出力すると、C# が提供するすべてのメモリ安全性保証がバイパスされます。
絶対に必要な場合を除き、このような手法の使用は避けてください。
推奨事項
- ❌ ガイドレールがないため、生のILコードを出力しないでください。タイプセーフティの問題やその他の問題が発生しやすくなります。 他の動的コード生成手法と同様に、生の IL を出力しても、ビルド時に行わないと AOT に対応できません。
- ✔️ 可能であれば、代わりにソース ジェネレーターを使用してください。
- ✔️ 必要に応じて、プライベート メンバーのオーバーヘッドの少ないシリアル化コードを記述するために生の IL を出力する代わりに 、[UnsafeAccessor] を 使用してください。
- ✔️ 一部の API が不足していて、代わりに生の IL コードを強制的に使用する場合は、 dotnet/runtime に対して API 提案を提出してください。
- ✔️ 生の IL を使用する必要がある場合は、
ilverifyまたは同様のツールを使用して、出力された IL コードを検証してください。
19. 初期化されていないローカル [SkipLocalsInit] と Unsafe.SkipInit
[SkipLocalsInit] は.NET 5.0 で導入されました。これにより、JIT はメソッドごとに、またはモジュール全体で、メソッド内のローカル変数のゼロ化をスキップできます。 この機能は、 stackallocなどの冗長なゼロ初期化を JIT で排除するためによく使用されました。 ただし、ローカルが使用前に明示的に初期化されていない場合は、未定義の動作につながる可能性があります。 JIT のゼロ初期化を排除し、ベクター化を実行する機能が最近改善されたため、 [SkipLocalsInit] と Unsafe.SkipInit の必要性が大幅に減少しました。
推奨事項
-
❌ ホット コードでパフォーマンス上の利点が見つからない場合や、発生するリスクがわからない場合は、
[SkipLocalsInit]とUnsafe.SkipInitを使用しないでください。 - ✔️
GC.AllocateUninitializedArrayやArrayPool<T>.Shared.Rentなどの API を使用する際には、防御的なコーディングを心がけ、同様に初期化されていないバッファーを返す可能性があることに注意してください。
20. ArrayPool<T>.Shared と類似のプールAPI
ArrayPool<T>.Shared は、ホット コードで GC の負荷を軽減するために使用される配列の共有プールです。 多くの場合、I/O 操作やその他の有効期間の短いシナリオに一時的なバッファーを割り当てる場合に使用されます。 API は単純であり、本質的に安全でない機能は含まれませんが、C# では無料で使用できるバグにつながる可能性があります。 例:
var buffer = ArrayPool<byte>.Shared.Rent(1024);
_buffer = buffer; // buffer object escapes the scope
Use(buffer);
ArrayPool<byte>.Shared.Return(buffer);
_buffer呼び出しの後でReturnを使用すると、use-after-freeバグが発生します。 この最小限の例は簡単に見つけることができますが、 Rent と Return が異なるスコープまたはメソッドにある場合、バグの検出が困難になります。
推奨事項
- ✔️ 可能であれば、
RentとReturnに一致する呼び出しを同じメソッド内に保持して、潜在的なバグの範囲を絞り込みます。 -
❌失敗したロジックがバッファーの使用を完了したことを確信している場合を除き、
try-finallyブロック内のReturnを呼び出すために、finallyパターンを使用しないでください。 予期しない早期のReturnが原因で、use-after-freeバグのリスクを負うより、バッファーを破棄する方が賢明です。 - ✔️ 他のプール API やパターン ( ObjectPool<T> など) でも同様の問題が発生する可能性があることに注意してください。
21. bool<->int 変換
ECMA-335 標準では、ブール値を 0 から 255 として定義しますが、 true はゼロ以外の値ですが、0 または 1 以外の値が "非正規化" 値を導入しないようにするために、整数とブール値の間の明示的な変換を避けた方が、信頼性の低い動作になる可能性があります。
// Bad:
bool b = Unsafe.As<int, bool>(ref someInteger);
int i = Unsafe.As<bool, int>(ref someBool);
// Good:
bool b = (byte)someInteger != 0;
int i = someBool ? 1 : 0;
以前の .NET ランタイムに存在する JIT では、このロジックの安全なバージョンを完全に最適化できなかったため、開発者は安全でないコンストラクトを使用して、パフォーマンスに依存するコード パスの bool と int の間で変換を行いました。 これはもはや当てはまるのではなく、最新の .NET JIT は安全なバージョンを効果的に最適化できます。
推奨事項
- ❌ 安全でないコードを使用して、整数とブール値の間に "ブランチレス" 変換を記述しないでください。
- ✔️ 代わりに三項演算子 (またはその他の分岐ロジック) を使用してください。 最新の .NET JIT は、それらを効果的に最適化します。
-
❌入力を信頼できない場合は、
boolやUnsafe.ReadUnalignedなどの安全でない API を使用してMemoryMarshal.Castを読み取らないでください。 代わりに三項演算子または等値比較を使用することを検討してください。
// Bad:
bool b = Unsafe.ReadUnaligned<bool>(ref byteData);
// Good:
bool b = byteData[0] != 0;
// Bad:
ReadOnlySpan<byte> byteSpan = ReadDataFromNetwork();
bool[] boolArray = MemoryMarshal.Cast<byte, bool>(byteSpan).ToArray();
// Good:
ReadOnlySpan<byte> byteSpan = ReadDataFromNetwork();
bool[] boolArray = new bool[byteSpan];
for (int i = 0; i < byteSpan.Length; i++) { boolArray[i] = byteSpan[i] != 0; }
詳細については、パディングまたは非ブリッタブルなメンバーを含む構造体のバイナリデシリアル化を参照してください。
22. 相互運用
このドキュメントの推奨事項のほとんどは相互運用シナリオにも適用されますが、 ネイティブ相互運用性のベスト プラクティス ガイドに従うことをお勧めします。 さらに、 CsWin32 や CsWinRT などの自動生成された相互運用ラッパー の使用を検討してください。 これにより、手動相互運用コードを記述する必要が最小限に抑えられます。また、メモリの安全性の問題が発生するリスクが軽減されます。
23. スレッドセーフ性
メモリ セーフとスレッド セーフは直交概念です。 コードはメモリ安全であっても、データ競合や不完全な読み取り、可視性のバグが含まれている可能性があります。逆に、安全でないメモリ操作によって未定義の動作を呼び出しながら、コードをスレッド安全にすることができます。 より広範なガイダンスについては、 マネージド スレッドのベスト プラクティス と .NET メモリ モデルに関するトピックを参照してください。
24. SIMD/Vectorization に関する安全でないコード
詳細については、 ベクター化のガイドライン を参照してください。 安全でないコードのコンテキストでは、次の点に注意することが重要です。
- SIMD 操作には、原子性の保証を提供するための複雑な要件があります (場合によっては、まったく提供されない場合があります)。
- ほとんどの SIMD 読み込み/ストア API では、境界チェックは提供されません。
25. ファジーテスト
ファジー テスト ("ファジー") は、コンピューター プログラムへの入力として無効な、予期しない、またはランダムなデータを提供する自動化されたソフトウェア テスト手法です。 これは、テスト カバレッジにギャップがある可能性があるコードでメモリの安全性の問題を検出する方法を提供します。 SharpFuzz などのツールを使用して、.NET コードのファジー テストを設定できます。
26. コンパイラの警告
一般に、C# コンパイラでは、不適切な安全でないコードの使用に関する警告やアナライザーなどの広範なサポートは提供されません。 ただし、潜在的な問題を検出するのに役立つ既存の警告がいくつかあり、慎重に考慮せずに無視したり抑制したりするべきではありません。 いくつかの例を次に示します。
nint ptr = 0;
unsafe
{
int local = 0;
ptr = (nint)(&local);
}
await Task.Delay(100);
// ptr is used here
このコードでは、警告 CS9123 ("非同期メソッドのパラメーターまたはローカル変数で '> 演算子を使用しないでください") が生成されます。これは、コードが正しくない可能性があることを意味します。
推奨事項
- ✔️ コンパイラの警告に注意を払い、基になる問題を抑制するのではなく修正してください。
- ❌ コンパイラの警告がない場合は、コードが正しいことを意味すると想定しないでください。 C# コンパイラは、不適切な安全でないコードの使用を検出するためのサポートに制限されていません。
References
- 安全でないコード、ポインター型、および関数ポインター。
- 安全でないコード、言語仕様。
- CoreCLR と GC 内部に関する高度なトピックのコードを記述する前に、すべての CLR 開発者が知っておくべきこと。
- ネイティブ相互運用性のベスト プラクティス。
- マネージド スレッドのベスト プラクティス。
- 例外のベスト プラクティス。
- ベクター化のガイドライン
- .NET メモリ モデル
- ECMA-335
- ECMA-335 の拡張
.NET