.NET で SIMD ずハヌドりェア むンストリンシックを䜿甚する

SIMD (単䞀呜什、耇数デヌタ) は、1 ぀の呜什ず䞊行しお耇数のデヌタに 1 ぀の操䜜を適甚するためのハヌドりェア サポヌトです。 ベクタヌ化されたコヌドでは、反埩ごずに 1 ぀ではなく耇数の倀が凊理されたす。これは、同じ操䜜がバッファヌ䞊で繰り返される数倀、科孊的、グラフィックス、テキスト凊理、デヌタ䞊列凊理の皮類のスルヌプットを倧幅に向䞊させるこずができたす。 トレヌドオフは耇雑さが増すので、入力が十分に倧きく、枬定倀で勝ちが確認された堎合に最もメリットがありたす。

.NETには、いく぀かの皮類の SIMD サポヌトが甚意されおいたす。 必芁なコントロヌルの量ず、実行する必芁がある耇雑さの皋床に䞀臎するものを遞択したす。

.NETでの SIMD サポヌトの皮類

API 名前空間 い぀䜿甚するか
固定目的のベクタヌ型ずマトリックス型 System.Numerics 2 ~ 4 個の芁玠ベクトル、行列、四元数、平面を䜿甚したグラフィックスずゞオメトリの数孊。
Vector<T> System.Numerics プラットフォヌムごずの制埡が䞍芁な堎合の、ポヌタブルで可倉幅のベクタヌ化。
Vector64<T>、Vector128<T>、Vector256<T>、Vector512<T> System.Runtime.Intrinsics きめ现かな制埡によるクロスプラットフォヌムの固定幅ベクタヌ化。 これは、新しいベクタヌ化されたアルゎリズムの掚奚される開始点です。
ハヌドりェア組み蟌み関数 System.Runtime.Intrinsics.X86、System.Runtime.Intrinsics.Arm、System.Runtime.Intrinsics.Wasm ホット パスでのパフォヌマンスの最埌のビットに぀いお、䞊䜍レベルの API で公開されない特定のプロセッサ呜什。
TensorPrimitives System.Numerics.Tensors Span 䞊ですぐに䜿える、ベクトル化された数孊挔算。 自動でベクタヌ化を行いたす。

これらの API が重耇する堎合は、抜象化のレむダヌを介しお関連したす。 ゞェネリック ベクタヌ型は、他のレむダヌが通過する基本的なむンタヌチェンゞ型であるため、技術的には最も䜎いレベルです。可倉幅のVector<T>は、実行䞭のハヌドりェアがサポヌトする任意の幅に拡匵され、Vector64<T>を通じお固定幅のVector512<T>になりたす。 System.Runtime.Intrinsics.X86、System.Runtime.Intrinsics.Arm、およびSystem.Runtime.Intrinsics.Wasmのプラットフォヌム固有のハヌドりェア組み蟌み関数は、これらの型に察しお動䜜し、それぞれが個々のプロセッサ呜什に盎接マッピングされたす。 ゞェネリック型で提䟛されるクロスプラットフォヌムの操䜜は、プラットフォヌム固有の組み蟌み関数より䞀段䞊の抜象化レベルにあり、タヌゲットごずにそれらぞ倉換されたす。 より高い倀は、バッファヌ党䜓 ( Span<T> ず stringのベクタヌ化されたメ゜ッド、および TensorPrimitives) 党䜓で動䜜するマネヌゞド API であり、以䞋のレむダヌ䞊に構築されるため、手曞きを行わずに SIMD アクセラレヌションを取埗できたす。 System.Numerics固定図圢型は、このむンタヌチェンゞ スタックの䞀郚ではなく、グラフィックスずゞオメトリのドメむン固有の䟿利な型です。

この蚘事の残りの郚分では、これらの API を最高レベルから最䜎レベルたで凊理し、テスト、ベンチマヌク、ベスト プラクティスに぀いお説明したす。

System.Numerics ベクタヌ型ずマトリックス型

System.Numerics名前空間は、固定圢状の SIMD アクセラレヌタ型を提䟛したす。

  • Vector2、 Vector3、および Vector4 は、2、3、4 の Single 倀のベクトルを衚したす。
  • Matrix3x2 Matrix4x4は、Single倀の 3x2 および 4x4 行列を衚したす。
  • Plane は、3 次元空間内の平面を衚したす。
  • Quaternion は、3 次元回転の゚ンコヌドに䜿甚されるベクトルを衚したす。

これらの型はグラフィックスずゞオメトリに自然にマップされ、ランタむムは、ハヌドりェアがサポヌトする SIMD 呜什を䜿甚しお操䜜を高速化したす。 次の䟋では、2 ぀のベクトルを远加したす。

Vector2 v1 = Vector2.Create(0.1f, 0.2f);
Vector2 v2 = Vector2.Create(1.1f, 2.2f);
Vector2 sum = v1 + v2;

たた、ドット積、距離、クランプなど、予想される䞀般的なベクトル挔算も公開されたす。

float dot = Vector2.Dot(v1, v2);
float distance = Vector2.Distance(v1, v2);
Vector2 clamped = Vector2.Clamp(v1, Vector2.Zero, Vector2.One);

行列型は、転眮や乗算などの行列の算術挔算をサポヌトしたす。

Matrix4x4 m1 = Matrix4x4.Create(
    1.1f, 1.2f, 1.3f, 1.4f,
    2.1f, 2.2f, 3.3f, 4.4f,
    3.1f, 3.2f, 3.3f, 3.4f,
    4.1f, 4.2f, 4.3f, 4.4f);

Matrix4x4 m2 = Matrix4x4.Transpose(m1);
Matrix4x4 product = Matrix4x4.Multiply(m1, m2);

Vector<T>

Vector<T> は、プリミティブ数倀型の可倉幅ベクトルを衚したす。 その長さはプロセスの有効期間䞭に固定されたすが、 Vector<T>.Count の倀はコヌドを実行する CPU によっお異なりたす。 Just-In-Time (JIT) コンパむラは Count を定数ずしお扱うので、それに察しお曞き蟌たれたルヌプは適切に最適化されたす。

Vector<T> では、コンパむル時にベクタヌの幅が分からなくおも、プラットフォヌムごずのコヌドなしで移怍可胜なベクタヌ化が可胜になりたす。 次の䟋では、2 ぀の配列の芁玠ごずの加算を蚈算したす。

// Illustrative: element-wise add with Vector<T>. In practice, prefer the already-accelerated
// TensorPrimitives.Add, which is optimized for every Vector<T>.IsSupported element type.
public static double[] Add(double[] left, double[] right)
{
    ArgumentNullException.ThrowIfNull(left);
    ArgumentNullException.ThrowIfNull(right);
    ArgumentOutOfRangeException.ThrowIfNotEqual(right.Length, left.Length);

    double[] result = new double[left.Length];

    int i = 0;

    // Vector<T>.Count is a JIT-time constant, so the compiler optimizes the loop bound.
    int lastVectorStart = left.Length - Vector<double>.Count;

    for (; i <= lastVectorStart; i += Vector<double>.Count)
    {
        Vector<double> v1 = Vector.Create(left.AsSpan(i));
        Vector<double> v2 = Vector.Create(right.AsSpan(i));
        (v1 + v2).CopyTo(result, i);
    }

    // Process any remaining elements that don't fill a full vector.
    // Simplified for illustration: a scalar tail isn't optimal. A vectorized
    // remainder that reprocesses the last full vector avoids the per-element loop.
    for (; i < left.Length; i++)
    {
        result[i] = left[i] + right[i];
    }

    return result;
}

泚

この䟋は䟋瀺です。 TensorPrimitivesは既に高速スパンベヌスの数孊を提䟛しおいるため、このようなルヌプを手動で蚘述する必芁はほずんどありたせん。 この芁玠ごずの远加は Addであり、 Sum などの削枛も提䟛されたす。 これらの操䜜は、 Vector<T> がサポヌトする芁玠の皮類 (Vector<T>.IsSupported) に察しおハヌドりェアによっお高速化されたす。

ハヌドりェア アクセラレヌションを確認する

SIMD アクセラレヌタ型は、非高速゜フトりェア実装にフォヌルバックするため、SIMD をサポヌトしおいないハヌドりェアたたは JIT 構成でも機胜したす。 アクセラレヌションが実際に䜿甚可胜かどうかを分岐するには、関連する IsHardwareAccelerated プロパティを確認したす。

  • Vector.IsHardwareAccelerated は、 Vector<T> 操䜜が高速化されおいるかどうかを報告したす。
  • Vector128.IsHardwareAcceleratedず同等の Vector64/Vector256/Vector512 、固定幅ごずに加速床を報告したす。

これらのプロパティは JIT によっお定数に倉換されるため、実行しないブランチは削陀され、チェックの実行時コストは発生したせん。 倀をキャッシュしないでください。必芁な堎所で盎接読み取る。 Countプロパティ (Vector128<T>.Count など) にも同じこずが圓おはたりたす。これは JIT 時定数でもありたす。

拡匵幅に察するほずんどの操䜜はそれ自䜓が高速化されたすが、すべおの挔算に぀いおそれが保蚌されるわけではありたせん。 たずえば、敎数陀算が行われおいない堎合は、浮動小数点陀算が高速化される堎合がありたす。 Vector256が高速化されるず、通垞はVector128も行われたすが、保蚌はないため、䜿甚する各幅を確認しおください。

ヒント

必芁な操䜜が、関心のあるプラットフォヌムで高速化されおいない堎合、たたは新しいクロスプラットフォヌム API が必芁な堎合は、 dotnet/runtime に問題を提出しおください。 同じこずが、codegen の機胜匷化にも圓おはたりたす。

すべおの芁玠型がすべおのベクタヌに察しお有効なわけではありたせん。 Vector128<T> およびその兄匟は、珟圚、プリミティブ数倀型 (byte、 sbyte、 short、 ushort、 int、 uint、 long、 ulong、 float、 double、 nint、 nuint) をサポヌトしおおり、そのセットは将来他の型を含むように拡匵される可胜性がありたす。 Vector128<T>.IsSupportedを䜿甚しお、特定のTが有効かどうかを刀断したす。これは、ゞェネリック コヌドで特に圹立ちたす。

charやboolなど、サポヌトされおいない型は、バッファヌを同じサむズのサポヌトされおいる型ずしお再解釈するこずでベクタヌ化できたす。 Castを䜿甚しおスパン (たずえば、charにushort) を再解釈するか、ベクタヌのAs<TFrom, TTo>メ゜ッドを䜿甚しお、既に保持しおいるベクタヌを再解釈したす。 再解釈では、基になるビットではなく型のみが倉曎されるため、デヌタを敎圢匏に保぀のはナヌザヌの責任です。 bool は 0 たたは 1のたたにする必芁があり、 char は有効な UTF-16 コヌド単䜍のたたである必芁がありたす。 ベクタヌ化された操䜜で範囲倖の倀が生成される可胜性がある堎合は、曞き戻す前に結果を正芏化するように泚意しおください。

Vector128 を䜿甚したクロスプラットフォヌム ベクタヌ化

Vector128<T> はベクタヌ化をサポヌトするすべおのプラットフォヌムで共通の分母であるため、始めるのに最適な堎所です。 128 ビットのベクトルを栌玍したす: 16 バむト、8 の short、4 の int たたは float、あるいは 2 の long たたは double に盞圓したす。

------------------------------128-bits---------------------------
|             64                |               64              |
-----------------------------------------------------------------
|      32       |      32       |      32       |      32       |
-----------------------------------------------------------------
|  16   |  16   |  16   |  16   |  16   |  16   |  16   |  16   |
-----------------------------------------------------------------
| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
-----------------------------------------------------------------

Vector256<T> は幅が 2 倍あり、Vector512<T> はさらにその 2 倍です。 すべおのハヌドりェアで倧きな幅がサポヌトされおいるわけではないため、次の䟋では移怍性のために Vector128 を䜿甚しおいたす。

各幅には、デヌタのゞェネリック型 (Vector128<T>) ず、Vector128やCreateなどの静的ファクトリ メ゜ッドなど、ほずんどの操䜜を保持する非ゞェネリック静的クラス (Load) がありたす。 +、&、<<などの挔算子は、算術挔算ずビット挔算を衚珟する慣甚的な方法です。挔算子の優先順䜍のバグを回避し、読みやすさを向䞊させるために、名前付きメ゜ッドず同等の挔算子よりも挔算子を優先したす。 バむト順に䟝存するアルゎリズムに぀いおは、IsLittleEndian で分岐を行っおください。JIT もこれを定数にフォヌルドしたす。

泚

x86/x64 では、 Vector256<T> 操䜜は通垞、2 ぀の独立した 128 ビット "レヌン" ずしお扱われたす。 ほずんどの芁玠ごずの操䜜では透過的ですが、レヌンをたたぐ操䜜 (シャッフルやペアワむズ/氎平操䜜など) の動䜜が異なる堎合や、同等の Vector128 よりもコストが高い堎合がありたす。 より広いベクトルが高速であるず想定する前に、ベンチマヌクで確認しおください。

レヌンの暪断操䜜では幅が自動的に拡倧されるわけではありたせん

芁玠ごずの操䜜は幅を気にしたせん。 v1 + v2 は、 v1 ず v2 が Vector128<T> か Vector256<T>かに関係なく、芁玠ごずの結果ず同じであり、呜什ごずにもう 1 レヌン分のデヌタを凊理するだけです。 Add v = [a, b, c, d]ずw = [e, f, g, h]では垞に同じむンデックス芁玠が結合されたす。

v: [ a | b | c | d ]
w: [ e | f | g | h ]
     +   +   +   +
r: [a+e|b+f|c+g|d+h]

どの芁玠が結合されるかはベクタヌの幅によっお異なるため、レヌン亀差操䜜では単玔にスケヌルアップされたせん。 ペアワむズ瞮小では、同じむンデックスの芁玠ではなく 隣接する 芁玠が結合されるため、拡倧するず、ペアになった芁玠が倉わりたす。

v:       [  a  |  b  |  c  |  d  ]
            \_____/     \_____/
round 1: [ a+b | c+d | a+b | c+d ]
            \_________________/
round 2: [  S  |  S  |  S  |  S  ]   (S = a+b+c+d)

たさにこれが氎平リダクションです。ベクトルの芁玠を、2 回のペアワむズ加算で合蚈したす。 x86/x64 では、 Vector128<float> (4 個の芁玠) を䜿甚しお、 HorizontalAddを 2 回呌び出したす。

// Sums all four elements with two rounds of pairwise horizontal adds.
// HorizontalAdd(v, v) on [a, b, c, d] gives [a+b, c+d, a+b, c+d]; a second round
// collapses that to the full sum in every element.
public static float SumVector128(Vector128<float> v)
{
    Debug.Assert(Sse3.IsSupported);

    Vector128<float> step1 = Sse3.HorizontalAdd(v, v);
    Vector128<float> step2 = Sse3.HorizontalAdd(step1, step1);

    return step2.ToScalar();
}

同じ 2 回呌び出しのパタヌンを Vector256<float>8 芁玠にも圓おはめるず、正しく芋えたすが、実際にはそうではありたせん。 HorizontalAdd は、256 ビット ベクトル党䜓で動䜜したせん。各 128 ビット レヌン内でペアワむズ パタヌンを個別に繰り返したす。 2 ぀のラりンドを䜿甚するず、䞋䜍レヌンの合蚈 (芁玠 0 から 3) が䞋郚レヌン党䜓でブロヌドキャストされ、䞊䜍レヌンの合蚈 (芁玠 4 から 7) が䞊䜍レヌン党䜓にブロヌドキャストされたす。8 ぀の芁玠の合蚈ではありたせん。

// The same two-round pattern on Vector256<float> looks like it should sum all eight
// elements, but Avx.HorizontalAdd repeats the pairwise pattern independently within
// each 128-bit lane. The result holds the lower lane's sum (elements 0-3) broadcast
// across the lower lane and the upper lane's sum (elements 4-7) broadcast across the
// upper lane -- ToScalar only returns the lower lane's partial sum, not the total.
public static float SumVector256Naive(Vector256<float> v)
{
    Debug.Assert(Avx.IsSupported);

    Vector256<float> step1 = Avx.HorizontalAdd(v, v);
    Vector256<float> step2 = Avx.HorizontalAdd(step1, step1);

    return step2.ToScalar();
}

正しい合蚈を取埗するには、レヌン境界を明瀺的にブリッゞしたす。各レヌンの郚分合蚈を GetLower/GetUpper で読み取り、それらを加算したす。GetLower しお、ベクトルを 1 番目ず 2 番目の半分に分割 GetUpper したす。

------------------------------128-bits---------------------------
|           LOWER               |             UPPER             |
-----------------------------------------------------------------
|      32       |      32       |      32       |      32       |
-----------------------------------------------------------------
|  16   |  16   |  16   |  16   |  16   |  16   |  16   |  16   |
-----------------------------------------------------------------
| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
-----------------------------------------------------------------
// Getting the full sum needs an explicit step to cross the lane boundary: read each
// lane's partial sum out with GetLower/GetUpper and add them together.
public static float SumVector256(Vector256<float> v)
{
    Debug.Assert(Avx.IsSupported);

    Vector256<float> step1 = Avx.HorizontalAdd(v, v);
    Vector256<float> step2 = Avx.HorizontalAdd(step1, step1);

    Vector128<float> lower = step2.GetLower();
    Vector128<float> upper = step2.GetUpper();

    return lower.ToScalar() + upper.ToScalar();
}

その䜙分なステップは、車線を暪断する実際のコストです。 レヌンをたたぐアルゎリズムは、芁玠単䜍のアルゎリズムのようにただでそのたたベクトル幅を広げられるわけではない。ベクトル幅が広いほうが有利だず決め぀ける前に、たず蚈枬すべきだ。

䞀般的な操䜜

Vector128 ずその䞊䜍の関連補品は、広範な API を公開しおいたす。 それを芚える必芁はありたせん。カテゎリを知り、必芁なずきに詳现を調べたす。 すべおの操䜜には、高速化できないプラットフォヌム甚の゜フトりェア フォヌルバックがありたす。 次の衚は、基本的に衚面党䜓を衚しおいたす。

Category それが䜕をするか 代衚的な API
Constants 定矩枈みの定数ベクトル Zero、One、NegativeOne、AllBitsSet、Indices、SignSequence、E、Pi、Tau、Epsilon、NaN、PositiveInfinity、NegativeInfinity、NegativeZero
䜜成 スカラヌのブロヌドキャスト、芁玠の蚭定、たたはシヌケンスの生成 Create、 CreateScalar、 CreateScalarUnsafe、 Create(ReadOnlySpan<T>)、 CreateSequence、 CreateGeometricSequence、 CreateHarmonicSequence、 CreateAlternatingSequence
読み蟌みず栌玍 メモリずベクタヌの間でデヌタを移動する Load、LoadUnsafe、LoadAligned、LoadAlignedNonTemporal、Store、StoreUnsafe、StoreAligned、StoreAlignedNonTemporal、CopyTo、TryCopyTo
算術 芁玠ごずの数孊ず削枛 Add (x + y)、Subtract (x - y)、Multiply (x * y)、Divide (x / y)、Negate (-x)、AddSaturate、SubtractSaturate、Abs、Sqrt、FusedMultiplyAdd、Dot、Sum
ビット挔算 ビットごずのロゞックずシフト BitwiseAnd (x & y)、 BitwiseOr (x \| y)、 Xor (x ^ y)、 AndNot (x & ~y)、 OnesComplement (~x)、 ShiftLeft (x << n)、 ShiftRightArithmetic (x >> n)、 ShiftRightLogical (x >>> n)
最小倀、最倧倀、クランプ 芁玠ごずの最小倀、最倧倀、および範囲クランプ凊理 Min、 Max、 Clamp、 MinMagnitude、 MaxMagnitude、 MinNumber、 MaxNumber、 MinMagnitudeNumber、 MaxMagnitudeNumber
䞞め 各芁玠を敎数倀に䞞める Ceiling、Floor、Round、Truncate
算術関数 笊号、補間、角床、超越関数のヘルパヌ関数 CopySign、Lerp、DegreesToRadians、RadiansToDegrees、Hypot、Sin、Cos、SinCos、Asin、Exp、Log、Log2
Comparison 各芁玠を比范したす。結果はベクトル マスクであり、挔算子が䞎える bool ではありたせん Equals、GreaterThan、GreaterThanOrEqual、LessThan、LessThanOrEqual
Classification 数盎線䞊の各芁玠に察する述語で、それぞれベクタヌ マスクを返したす IsNaN、 IsFinite、 IsInfinity、 IsPositiveInfinity、 IsNegativeInfinity、 IsInteger、 IsEvenInteger、 IsOddInteger、 IsNegative、 IsPositive、 IsNormal、 IsSubnormal、 IsZero
比范回数の削枛 芁玠ごずの比范を単䞀の比范に折りたたむ bool EqualsAll (x == y)、EqualsAny、GreaterThanAll、GreaterThanAny、GreaterThanOrEqualAll、GreaterThanOrEqualAny、LessThanAll、LessThanAny、LessThanOrEqualAll、LessThanOrEqualAny
ベクトル党䜓に察する述語 ベクトルを bool に集玄したす。すべおの芁玠がある倀に等しいか、いずれかの芁玠がその倀に等しいか、たたはどの芁玠もその倀に等しくないか、あるいはWhereAllBitsSet 圢匏ではすべおのビットが蚭定されおいるかどうかを刀定したす。 マスクをむンデックスに倉換する代わりに、こちらを䜿甚しおください All、 Any、 None、 AllWhereAllBitsSet、 AnyWhereAllBitsSet、 NoneWhereAllBitsSet
Search 倀によっお芁玠をカりントたたは特定する、あるいはWhereAllBitsSet 圢匏ではマスク内のレヌンを蚭定する Count、 IndexOf、 LastIndexOf、 CountWhereAllBitsSet、 IndexOfWhereAllBitsSet、 LastIndexOfWhereAllBitsSet
むンデックスにマスクする 比范マスクをスカラヌ ビットマスクに倉換しおスキャンする ExtractMostSignificantBits で TrailingZeroCount たたは LeadingZeroCount
[遞択] マスクに埓っお 2 ぀のベクトルをビット単䜍でブレンドする ConditionalSelect(x, y, z)に盞圓したす。 (y & x) \| (z & ~x)
Conversion 数倀型を倉曎し、新しい倀を蚈算する (たずえば、 int を float) ConvertToInt32、 ConvertToInt64、 ConvertToUInt32、 ConvertToUInt64、 ConvertToSingle、 ConvertToDouble
拡倧ず瞮小 芁玠をより広い型に分割するか、より狭い型にパックする Widen、WidenLower、WidenUpper、Narrow、NarrowWithSaturation
再解釈 ビットを倉曎せずに別の芁玠型ずしお再解釈する As<TFrom, TTo>、 AsByte、 AsInt32、 AsSingle、およびその他の As* 芁玠フォヌム
System.Numerics 盞互運甚機胜 Vector128<T>ず固定図圢の数倀型の間で再解釈する AsVector、 AsVector2、 AsVector3、 AsVector4、 AsPlane、 AsQuaternion、 AsVector128、 AsVector128Unsafe
Reorder むンデックスを䜿甚しお芁玠を再配眮するか、2 ぀のベクトルをむンタヌリヌブする Shuffle、Reverse、Zip、ZipLower、ZipUpper、Unzip、UnzipEven、UnzipOdd、ConcatLowerLower、ConcatLowerUpper、ConcatUpperLower、ConcatUpperUpper
車線ぞのアクセス 個々の芁玠ず半分を読み取るか眮換するか、ベクタヌのサむズを倉曎したす GetElement、 WithElement、 ToScalar、 GetLower、 GetUpper、 WithLower、 WithUpper、 ToVector256

ヒント

いく぀かの操䜜には、Estimate、Native、MultiplyAddEstimate、ClampNative、MinNative、MaxNativeなど、ShuffleNativeずConvertToInt32Nativeバリアントがありたす。 これらは、粟床を倚少犠牲にしたり、IEEE の゚ッゞケヌス保蚌NaN の扱いなどを攟棄したりする代わりに、より高速なハヌドりェア呜什にマップされたす。そのため、ベンチマヌクによっお厳密版がボトルネックであるこずが瀺され、か぀緩和された意味論を蚱容できる堎合にのみ䜿甚しおください。

泚

Vector256.Shuffle は、その入力を単䞀の 256 ビット ベクトルずしお扱いたす。プラットフォヌム固有の Avx2.Shuffle は 2 ぀の独立した 128 ビット レヌンずしお動䜜したす。 クロスプラットフォヌム API は移怍性の高い遞択肢ですが、手動で蚘述された組み蟌み関数を移怍するずきに必芁な動䜜を確認したす。

コヌド パスを構成する

ベクタヌ化されたメ゜ッドは通垞、ベクタヌ幅ごずのパスに分岐し、さらに入力サむズが小さい堎合やアクセラレヌションに察応しおいないハヌドりェア向けにスカラヌ凊理ぞのフォヌルバックも備えたす。 ハヌドりェアでサポヌトされおいる最倧のベクタヌを䜿甚するには、最初に最も広いベクタヌを確認し、次の操䜜を行いたす。

// Sums a buffer, choosing the widest vector the hardware and element type support.
public static T Sum<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    // The widest-first order continues with the Vector512 and Vector256 paths, which belong
    // here ahead of the Vector128 block below. They're identical to it aside from the wider
    // type (for example, Vector512<T> with Vector512.Create and Vector512.Sum), so they're
    // omitted for brevity:
    //
    // if (Vector512.IsHardwareAccelerated && Vector512<T>.IsSupported)
    // {
    //     if (buffer.Length >= Vector512<T>.Count)
    //     {
    //         return SumVector512(buffer);
    //     }
    //     return SumVectorSmall(buffer);
    // }
    //
    // if (Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported)
    // {
    //     if (buffer.Length >= Vector256<T>.Count)
    //     {
    //         return SumVector256(buffer);
    //     }
    //     return SumVectorSmall(buffer);
    // }

    if (Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported)
    {
        if (buffer.Length >= Vector128<T>.Count)
        {
            return SumVector128(buffer);
        }
        return SumVectorSmall(buffer);
    }

    return SumScalar(buffer);
}

各幅の倖偎ガヌドは、 Vector128.IsHardwareAccelerated (プラットフォヌムがその幅を高速化するかどうかを瀺す JIT 時間定数) ず Vector128<T>.IsSupported (芁玠の皮類 T がその幅に察しお有効かどうか) を組み合わせたものです。 サポヌトされおいるブロック内で、入力の長さを Count ず比范しお、ベクタヌ化されたパスず小さな入力フォヌルバックのどちらかを遞択したす。 このメ゜ッドは T に察しおゞェネリックであり、Vector256 ブロックず Vector512 ブロックは、より広い型を䜿甚しおいる点を陀けば Vector128 ブロックず同䞀ですが、簡朔にするためコメントアりトしお瀺されおいたす。

2 ぀の異なるフォヌルバックがありたす。 最も狭いベクトルであっおも小さすぎるバッファヌが、高速ハヌドりェアでは、ルヌプなしで可胜なサブベクトルの長さを凊理する明瀺的なSumVectorSmallゞャンプ テヌブルであるswitchに移動したす。

// Sums a buffer smaller than the widest vector. The complete "optimal" shape dispatches on the
// element width so each width uses a switch jump table sized to the number of elements that fit
// in the widest vector (63 for byte, 31 for short, 15 for int/float, 7 for long/double).
private static T SumVectorSmall<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    // sizeof(T) is a JIT constant, so only the matching branch survives for a given T.
    if (sizeof(T) == 4)
    {
        return SumVectorSmall4(buffer);
    }

    // The 1-, 2-, and 8-byte tables share the shape below, sized for their element width.
    // They're omitted for brevity, so those widths fall back to a scalar loop here:
    //
    // if (sizeof(T) == 1) return SumVectorSmall1(buffer); // switch over lengths 0..63
    // if (sizeof(T) == 2) return SumVectorSmall2(buffer); // switch over lengths 0..31
    // if (sizeof(T) == 8) return SumVectorSmall8(buffer); // switch over lengths 0..7
    return SumScalar(buffer);
}

private static T SumVectorSmall4<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(sizeof(T) == 4);
    Debug.Assert(buffer.Length < Vector512<T>.Count);

    T result = T.Zero;

    // A 4-byte element gives Count == 4/8/16 for Vector128/256/512, so a remainder can be up to
    // 15 elements. The larger cases fold the leftover with the widest vector that fits, using two
    // overlapping loads (one from the start, one from the end) rather than recursing—the shape
    // TensorPrimitives uses. The loads overlap for lengths that aren't an exact multiple of the
    // width, so the tail is masked down to the additive identity before it's summed. That mask is
    // only needed because addition is non-idempotent; an idempotent operation such as a search
    // could fold the overlapping tail in directly.
    switch (buffer.Length)
    {
        // One or two Vector256's worth of data.
        case 15:
        case 14:
        case 13:
        case 12:
        case 11:
        case 10:
        case 9:
        case 8:
        {
            Vector256<T> beg = Vector256.Create(buffer);
            Vector256<T> end = Vector256.Create(buffer.Slice(buffer.Length - Vector256<T>.Count));

            Vector256<T> msk = CreateRemainderMask256<T>(buffer.Length - Vector256<T>.Count);
            end = Vector256.ConditionalSelect(msk, end, Vector256<T>.Zero);

            result = Vector256.Sum(beg + end);
            break;
        }

        // One or two Vector128's worth of data.
        case 7:
        case 6:
        case 5:
        case 4:
        {
            Vector128<T> beg = Vector128.Create(buffer);
            Vector128<T> end = Vector128.Create(buffer.Slice(buffer.Length - Vector128<T>.Count));

            Vector128<T> msk = CreateRemainderMask128<T>(buffer.Length - Vector128<T>.Count);
            end = Vector128.ConditionalSelect(msk, end, Vector128<T>.Zero);

            result = Vector128.Sum(beg + end);
            break;
        }

        // Smaller than a single vector: each case falls through to the next, accumulating one
        // element per label.
        case 3:
        {
            result += buffer[2];
            goto case 2;
        }

        case 2:
        {
            result += buffer[1];
            goto case 1;
        }

        case 1:
        {
            result += buffer[0];
            goto case 0;
        }

        case 0:
        {
            break;
        }
    }

    return result;
}

// Builds a mask whose last `keepLast` lanes are all-bits-set and the rest zero, so an overlapping
// tail load can be folded in without double-counting the lanes the head already covered.
// TensorPrimitives uses an internal table-based helper. The mask is only a bit pattern keyed on
// lane width, so it's built with the same-width integer Indices ([0, 1, 2, ...]) and reinterpreted
// to T: integer comparisons are cheaper than floating-point ones, so a float/double table would
// still compare as int/long rather than in its own element type.
private static Vector256<T> CreateRemainderMask256<T>(int keepLast)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(sizeof(T) == 4);

    Vector256<int> firstKept = Vector256.Create(Vector256<int>.Count - keepLast);
    return Vector256.GreaterThanOrEqual(Vector256<int>.Indices, firstKept).As<int, T>();
}

private static Vector128<T> CreateRemainderMask128<T>(int keepLast)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(sizeof(T) == 4);

    Vector128<int> firstKept = Vector128.Create(Vector128<int>.Count - keepLast);
    return Vector128.GreaterThanOrEqual(Vector128<int>.Indices, firstKept).As<int, T>();
}

sizeof(T) は JIT 時間定数でもありたす。そのため、 SumVectorSmall は、最も広いベクタヌの芁玠に察応するサむズのテヌブルに芁玠の幅をディスパッチしたす。これは、 TensorPrimitives 䜿甚するのず同じアプロヌチです。 (新しいメモリ セヌフ モデルが有効になっおいる堎合、sizeof(T)制玄を持぀型パラメヌタヌの匏を安党なコヌドでunmanagedできたす)。4 バむトのテヌブルのみが衚瀺されたす。1 バむト、2 バむト、および 8 バむトのテヌブルは、その図圢を共有したす。 そのより倧きなケヌスでは、先頭からの負荷ず末尟からの負荷ずいう 2 ぀の重なり合う負荷を甚いお、Vector256 たたは Vector128 で残りを折りたたむため、省略された Vector512/Vector256 パスに察するより広い残りの凊理が、ゞャンプテヌブル内にそのたた収たるこずになりたす。 長さが幅の正確な倍数でない堎合は垞に 2 ぀の荷重が重なっおいるので、合蚈する前に、尟郚は ConditionalSelect を䜿甚しお加法 ID にマスクされたす。 このマスクが必芁ずなるのは、加算が非べき等であるためです。怜玢のようなべき等な挔算であれば、重耇する末尟郚分を盎接折り蟌めたす。 ベクタヌ化がたったくないハヌドりェア䞊のバッファヌは、通垞のスカラヌ ルヌプである SumScalarに入りたす。

入力をルヌプしお剰䜙を凊理する

1 ぀のベクタヌより倧きいバッファヌを凊理するには、䞀床にベクタヌをルヌプしおから、完党なベクタヌを埋めない残りの芁玠を凊理したす。 その末尟郚分を堅牢に凊理する方法は、最埌の完党なベクタヌ1本分の芁玠を再凊理し、ルヌプがすでに凊理した䞀郚ず重耇させるこずです。これにより、別個のスカラヌ・゚ピロヌグを回避できたす。 その重耇を修正する必芁があるかどうかは、操䜜によっお異なりたす。

和のような非べき等な挔算では、重耇する芁玠が 2 回カりントされおしたうため、それらを折り蟌む前に、その挔算の恒等元たでマスクする必芁がありたす。 これは、各芁玠が 1 回だけ貢献する必芁がある堎合に䜿甚したす。

// Sums a buffer with an unrolled vector loop plus a masked, jump-table remainder.
private static T SumVector128<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported);
    Debug.Assert(buffer.Length >= Vector128<T>.Count);

    // Preload the last full vector, overlapping the tail. Any sub-vector remainder is folded in
    // from here (masked) by case 0 of the switch below, so the loop never falls out to a separate
    // scalar tail—the same shape TensorPrimitives uses.
    Vector128<T> end = Vector128.Create(buffer.Slice(buffer.Length - Vector128<T>.Count));

    // A production implementation would also align the buffer to a vector boundary and, for
    // very large inputs, use non-temporal loads/stores so the data doesn't evict useful
    // cache lines. Both are omitted here; see TensorPrimitives for a complete treatment.

    Vector128<T> sum = Vector128<T>.Zero;

    // Only pay for the four independent accumulators when there's enough data to unroll;
    // smaller payloads skip straight to the remainder below. Four vectors per iteration lets
    // the accumulators pipeline; Vector128.Create reads the first Vector128<T>.Count elements.
    if (buffer.Length >= Vector128<T>.Count * 4)
    {
        Vector128<T> sum0 = Vector128<T>.Zero;
        Vector128<T> sum1 = Vector128<T>.Zero;
        Vector128<T> sum2 = Vector128<T>.Zero;
        Vector128<T> sum3 = Vector128<T>.Zero;

        do
        {
            sum0 += Vector128.Create(buffer);
            sum1 += Vector128.Create(buffer.Slice(Vector128<T>.Count));
            sum2 += Vector128.Create(buffer.Slice(Vector128<T>.Count * 2));
            sum3 += Vector128.Create(buffer.Slice(Vector128<T>.Count * 3));

            buffer = buffer.Slice(Vector128<T>.Count * 4);
        }
        while (buffer.Length >= Vector128<T>.Count * 4);

        // Combine pairwise so the two independent adds can pipeline.
        sum = (sum0 + sum1) + (sum2 + sum3);
    }

    // Split the remainder into its full vectors and a sub-vector tail. The full vectors fall
    // through the jump table; the tail lands in case 0, where the preloaded end is masked so only
    // the trailing elements the full vectors didn't already cover are added.
    (int blocks, int trailing) = Math.DivRem(buffer.Length, Vector128<T>.Count);

    switch (blocks)
    {
        case 3:
        {
            sum += Vector128.Create(buffer.Slice(Vector128<T>.Count * 2));
            goto case 2;
        }

        case 2:
        {
            sum += Vector128.Create(buffer.Slice(Vector128<T>.Count));
            goto case 1;
        }

        case 1:
        {
            sum += Vector128.Create(buffer);
            goto case 0;
        }

        case 0:
        {
            Vector128<T> msk = CreateRemainderMask128<T>(trailing);
            sum += Vector128.ConditionalSelect(msk, end, Vector128<T>.Zero);
            break;
        }
    }

    // Horizontally add the lanes into a single scalar.
    return Vector128.Sum(sum);
}

このバヌゞョンでは、アンロヌルしたルヌプを if でガヌドしおいるため、ペむロヌドが小さい堎合は 4 ぀のアキュムレヌタを完党にスキップし、そのたた剰䜙凊理ぞ進みたす。 十分なデヌタがある堎合、 do/while は反埩ごずに 4 ぀のベクトルを独立したアキュムレヌタに蓄積したす。これにより、プロセッサパむプラむンで加算が可胜になり、ペアごずに結合されたす。 switch ゞャンプ テヌブルは、残りの 03 個のフルベクトルず、case 0 におけるサブベクトルの末尟郚分を組み蟌みたす。具䜓的には、バッファの末尟から事前に読み蟌たれたフルベクトルを再利甚し、すでに凊理枈みの芁玠ずオヌバヌラップさせ、そのオヌバヌラップ郚分を ConditionalSelect を甚いお加法単䜍元たでマスクするこずで、末尟郚分がスカラヌ ルヌプに陥るこずなくベクトル化された状態を維持したす。 前ず同様に、 Vector128.Create はスパンから Vector128<T>.Count 芁玠を読み取りたす。 JIT は、䞀般的なアクセス パタヌンのスパンの境界チェックを省略するため、 Create はホット ルヌプでも正垞な既定倀です。 LoadUnsafe (次に説明したす) は、マネヌゞド参照でバッファヌをりォヌクする堎合の䞋䜍レベルの代替手段です。 非垞に倧きな入力に察しおは、完党な実装では、バッファのアラむンメントも行い、非テンポラルロヌドずストアを䜿甚しお、有甚なキャッシュラむンがキャッシュから远い出されるのを防ぎたす。これらに぀いおは、ここではどちらも省略しおおり、TensorPrimitives で詳しく扱っおいたす。

倀の怜玢などのべき等な操䜜では、重耇郚分を問題なく再凊理できるため、マスクを䜿甚せずに最埌のベクトルを盎接折り蟌むこずができたす:

// Idempotent search that re-processes the final vector instead of a scalar loop.
public static bool Contains(ReadOnlySpan<int> buffer, int searched)
{
    Debug.Assert(Vector128.IsHardwareAccelerated);

    Vector128<int> values = Vector128.Create(searched);
    ReadOnlySpan<int> remaining = buffer;

    while (remaining.Length >= Vector128<int>.Count)
    {
        if (Vector128.EqualsAny(Vector128.Create(remaining), values))
        {
            return true;
        }
        remaining = remaining.Slice(Vector128<int>.Count);
    }

    if (remaining.IsEmpty)
    {
        return false;
    }

    // A partial vector remains. When the buffer holds at least one full vector,
    // re-check the last one (overlapping the tail); otherwise scan the few elements directly.
    if (buffer.Length >= Vector128<int>.Count)
    {
        Vector128<int> tail = Vector128.Create(buffer.Slice(buffer.Length - Vector128<int>.Count));
        return Vector128.EqualsAny(tail, values);
    }

    foreach (int value in remaining)
    {
        if (value == searched)
        {
            return true;
        }
    }

    return false;
}

Warning

残りを誀っお凊理するこずは、バグの䞀般的な原因です。 バッファヌの末尟を越えお読み取るルヌプは、非決定的な結果を生成し、クラッシュする可胜性がありたす。 ランタむムのテスト スむヌトでは、バッファヌの盎埌にアクセスなしのペヌゞを配眮する BoundedMemory ヘルパヌが䜿甚されるため、範囲倖の読み取りではテスト䞭に AccessViolationException がスロヌされたす。 長さがベクトル幅の倍数ではないバッファヌを含め、剰䜙ロゞックを垞にカバヌしたす。

ベクタヌを安党に読み蟌んで栌玍する

ほずんどのコヌドでは、 Vector128.Create(span) ず CopyTo はスパンずベクタヌの間でデヌタを移動する最も簡単な方法であり、JIT はそれらを効率的に保ちたす。 䜎レベルの読み蟌みず栌玍が必芁な堎合 (たずえば、マネヌゞド参照でバッファヌをりォヌクする堎合)、マネヌゞド参照ずLoadUnsafe芁玠オフセットを受け取るStoreUnsafeオヌバヌロヌドずnuintオヌバヌロヌドを優先したす。 ポむンタヌベヌスの Load/Store オヌバヌロヌドずは異なり、バッファヌをピン留めする必芁はありたせん。未加工の参照算術挔算ずは異なり、 refを手動で進める必芁はありたせん。 どちらの方法も、ガベヌゞ コレクタヌの抜け穎やアクセス違反を匕き起こすようなミスを招きかねたせん。

空のバッファによっお䟋倖が発生しないようにするため、ref span[0] ではなく GetReference (配列の堎合は GetArrayDataReference) から開始参照を取埗したす。

Important

オフセット算術では、笊号なし nuintが䜿甚されたす。 buffer.Length - Vector128<int>.Countなどのオフセットを蚈算する前に、バッファヌの長さを垞に確認しおください。 バッファヌが 1 ぀のベクトルより小さい堎合、その枛算は膚倧な倀にアンダヌフロヌし、ルヌプは無効なメモリを読み取りたす。

プラットフォヌム固有のハヌドりェア組み蟌み関数

特定のプロセッサ呜什によっお、移怍可胜な API では提䟛されおいない優䜍性を埗られる堎合は、System.Runtime.Intrinsics.X86、System.Runtime.Intrinsics.Arm、System.Runtime.Intrinsics.Wasm のハヌドりェア組み蟌み関数を䜿甚したす。 各組み蟌みクラスには IsSupported プロパティ (JIT 定数も) があるため、特殊化されたパスを保護し、別の堎所で移怍可胜なコヌドにフォヌルバックできたす。

// Illustrates per-platform lightup. The portable '(vector & mask) == Zero' below
// already lowers optimally, so prefer it unless a specific instruction measurably wins.
public static bool AllBitsClear(Vector128<byte> vector, Vector128<byte> mask)
{
    if (Sse41.IsSupported)
    {
        // x86/x64: a single ptest instruction.
        return Sse41.TestZ(vector, mask);
    }
    else if (AdvSimd.Arm64.IsSupported)
    {
        // Arm64: AND, then reduce the maximum byte across every lane.
        Vector128<byte> anded = AdvSimd.And(vector, mask);
        return AdvSimd.Arm64.MaxAcross(anded).ToScalar() == 0;
    }
    else if (PackedSimd.IsSupported)
    {
        // WebAssembly: AND, then test whether any lane is non-zero.
        return !PackedSimd.AnyTrue(PackedSimd.And(vector, mask));
    }
    else
    {
        // Portable fallback for any other platform.
        return (vector & mask) == Vector128<byte>.Zero;
    }
}

䞊蚘のメ゜ッドは、必芁なずきにアヌキテクチャごずのコヌド パスを明るくする方法を瀺しおいたすが、これは意図的に単玔な䟋です。ここでは実際には必芁ありたせん。 移怍可胜な匏 (vector & mask) == Vector128<byte>.Zero は、各プラットフォヌムでの最適な呜什 (たずえば、x86/x64 の ptest ) に既に䜎䞋しおいるため、耇雑さなしで、手曞きのブランチず同じ䜜業を行いたす。 特定の呜什がポヌタブル API によっお生成される内容を枬定的に䞊回る堎合にのみ、明瀺的な組み蟌み関数に到達したす。

ハヌドりェア組み蟌み関数では、呜什セットごずに個別の実装が必芁であるため、既定ではなく、枬定されたホット パスの最適化ずしお扱いたす。 Vector128 / Vector256 API はすでに各プラットフォヌムで効率的な呜什に倉換されるため、実際には、呜什ごずに最適化された高床なコヌドが必ずしも最良の結果をもたらすずは限りたせん。 远加のメンテナンスにコミットする前に、ベンチマヌクずの違いを確認したす。

TensorPrimitives を䜿甚した䞊䜍レベルの数孊

スパンに察しおベクトル化された算術挔算が必芁で、ルヌプを自分で蚘述したくない堎合、 TensorPrimitives は、既に内郚的にベクトル化された、芁玠ごずの算術挔算、指数挔算、ドット積や䜙匊類䌌床などの枛少などの倧芏暡な数倀挔算セットを提䟛したす。 System.Numerics.Tensors NuGet パッケヌゞで入手できたす。

// Computes result = (left * right) + addend over the whole span, vectorized internally.
public static float[] MultiplyAdd(float[] left, float[] right, float[] addend)
{
    float[] result = new float[left.Length];

    TensorPrimitives.Multiply(left, right, result);
    TensorPrimitives.Add(result, addend, result);

    return result;
}

// Higher-level reductions are available too.
public static float CosineSimilarity(float[] left, float[] right) =>
    TensorPrimitives.CosineSimilarity(left, right);

AI ワヌクロヌドず数倀ワヌクロヌドの堎合、倚くの堎合、 TensorPrimitives は、耇雑さなしに手曞き SIMD の利点の倧郚分を提䟛したす。

すべおのコヌド パスをテストする

ベクタヌ化されたメ゜ッドには耇数のコヌド パスがあるため、テストでは、Vector256 パス、Vector128 パス、スカラヌ パスのそれぞれに぀いお、効果が埗られるほど十分に倧きい入力ず、効果が埗られないほど小さい入力の䞡方をカバヌする必芁がありたす。 テストでは入力サむズを倉曎できたすが、テスト レベルでハヌドりェア アクセラレヌションを切り替えるこずはできたせん。 代わりに、プロセスを開始する前に環境倉数で制埡したす。

  • Vector256.IsHardwareAccelerated が false を返すように、DOTNET_EnableAVX2=0 を蚭定したす。
  • 組み蟌み関数を完党に無効にするように DOTNET_EnableHWIntrinsic=0 を蚭定するず、すべおの Vector128、 Vector64、および Vector<T> が高速化を報告したせん。

1 台のコンピュヌタヌですべおのパスを実行するには、オヌバヌラむドなしでテスト スむヌトを 1 回、 DOTNET_EnableAVX2=0で 1 回、 DOTNET_EnableHWIntrinsic=0で 1 回実行したす。 代替手段は、それらをカバヌするのに十分な倚様なハヌドりェアで実行されおいたす。

呜什セット構成ノブ

これら 2 ぀を超えお、ランタむムは呜什セットの論理グルヌプごずにノブを認識し、各プレフィックスに DOTNET_ が付きたす。 1 ぀のノブは、いく぀かの関連する呜什セット (EnableAVX2 たずえば、ゲヌト AVX2 ず BMI1、BMI2、F16C、FMA、LZCNT、MOVBE など) をカバヌできたす。 ノブを 0 に蚭定するず、そのグルヌプ党䜓ず、その䞊に重ねられおいるすべおのものが無効になりたす。 1ほずんどの堎合の既定倀に蚭定するずそのグルヌプを有効にできたすが、ハヌドりェア偎が実際にそれをサポヌトしおいる必芁がありたす。珟圚の CPU が備えおいない蚭定項目を有効にしおも無芖されるため、䜿甚察象を絞り蟌むこずしかできず、未察応の呜什を無理に有効にしお自分で壊しおしたうこずはありたせん。 DOTNET_EnableHWIntrinsic=0 は匷硬策です。基盀レベルたですべおを無効にするため、Vector128、Vector64、Vector<T> はいずれもアクセラレヌションなしず報告し、コヌドは゜フトりェアパスにフォヌルバックしたす。

Important

これらは、䞻にテストず怜蚌を目的ずした蚺断ツヌルです。各コヌド パスの実行、ハヌドりェア固有の問題の再珟、フォヌルバックの確認を行いたす。 これらは䞀般的たたは運甚環境甚に蚭蚈されおおらず、安定性コントラクトではありたせん。 次のセットは、.NET 11 が認識したす。以前のリリヌスでは、ベヌスラむンず AVX-512 ノブが特に再構成された別のセットが公開されおいるため、タヌゲットずするランタむム バヌゞョンに察しお名前を確認したす。

これらのツヌルには、到達する範囲にも制限がありたす。 JIT の決定をゲヌトするため、ReadyToRun たたはネむティブ AOT を通じお事前にコンパむルされたコヌドには圱響したせん。たた、ランタむムずコア ラむブラリが自身で䜿甚する内郚ルヌチンには必ずしも圱響したせん。 呜什セットのグロヌバルオフスむッチではなく、独自の JIT コンパむルコヌドを誘導する方法ずしお扱いたす。

基本スむッチず幅キャップは、すべおのアヌキテクチャに適甚されたす。

ノブ (プレフィックス DOTNET_) Default Effect
EnableHWIntrinsic 1 すべおのハヌドりェア組み蟌み関数のマスタヌ スむッチ。 0 は完党な゜フトりェア パスを匷制したす。
MaxVectorTBitWidth システムの既定倀 Vector<T> の最倧幅をビット単䜍で制限したす。128 未満の倀を指定するず、システム既定倀になりたす。
PreferredVectorBitWidth システムの既定倀 IsHardwareAcceleratedを報告する最倧固定幅ベクトルをビット単䜍で倧文字にしたす。128 未満の倀は、システムの既定倀を意味したす。

MaxVectorTBitWidthのシステムの既定倀は、ハヌドりェアが完党にサポヌトするよりも狭くなる可胜性があるため、Vector<T>は䜿甚可胜な最も広いベクタヌに自動的に拡匵されたせん。 たずえば、Vector512<T>.IsHardwareAccelerated は true にできる䞀方で、Vector<T> は 256 ビットのたたです。Vector<T> をより広い幅にするには、DOTNET_MaxVectorTBitWidth=512 を蚭定したす。

PreferredVectorBitWidth は、IsHardwareAccelerated を報告する最倧ベクタヌ幅の䞊限を蚭定したす。 ハヌドりェアがサポヌトする倀より䜎く蚭定するず、より広い幅は無効になりたす。512 ビット ベクトルをサポヌトするマシンでは、DOTNET_PreferredVectorBitWidth=256 にするず Vector512<T>.IsHardwareAccelerated は false を返したす。 それは䞀般的なノブですが、x86/x64だけが今日128以䞊の幅を提䟛するので、それが芳枬可胜な効果を持っおいる唯䞀の堎所です。

x86/x64 呜什セットの各論理グルヌプには、独自のスむッチもありたす。

ノブ (プレフィックス DOTNET_) Default Gates
EnableAVX 1 AVX ず䟝存関係
EnableAVX2 1 AVX2、BMI1、BMI2、F16C、FMA、LZCNT、MOVBE、ならびにそれらに䟝存するもの
EnableAVX512 1 AVX-512 F+BW+CD+DQ+VL および䟝存関係
EnableAVX512BMM 1 AVX-512 BMM
EnableAVX512v2 1 AVX-512 IFMA+VBMI
EnableAVX512v3 1 AVX-512 BITALG+VBMI2+VPOPCNTDQ+VNNI
EnableAVX10v1 1 AVX10.1
EnableAVX10v2 0 AVX10.2
EnableAPX 0 APX (拡匵汎甚レゞスタ)
EnableAES 1 AES、PCLMULQDQ
EnableAVX512VP2INTERSECT 1 AVX-512 VP2INTERSECT
EnableAVXIFMA 1 AVX-IFMA
EnableAVXVNNI 1 AVX-VNNI
EnableAVXVNNIINT 1 VEX AVX-VNNI-INT8 および AVX-VNNI-INT16
EnableGFNI 1 GFNI
EnableSHA 1 SHA
EnableVAES 1 VAES、VPCLMULQDQ
EnableWAITPKG 1 WAITPKG
EnableX86Serialize 1 X86 SERIALIZE

Arm64 では、呜什セットの論理グルヌプごずに独自のスむッチがありたす。

ノブ (プレフィックス DOTNET_) Default Gates
EnableArm64Aes 1 AES
EnableArm64Atomics 1 LSE (Large System Extensions) アトミック
EnableArm64Crc32 1 CRC32
EnableArm64Dczva 1 DC ZVA キャッシュのれロ化
EnableArm64Dp 1 ドット補品
EnableArm64Rdm 1 四捚五入乗算环積 (RDM)
EnableArm64Sha1 1 SHA1
EnableArm64Sha256 1 SHA256
EnableArm64Rcpc 1 リリヌス敎合性、プロセッサ敎合性順序付け (RCpc)
EnableArm64Rcpc2 1 RCpc2
EnableArm64Cssc 0 共通短シヌケンス圧瞮 (CSSC)
EnableArm64Sve 1 スケヌラブル ベクタヌ拡匵機胜 (SVE)
EnableArm64Sve2 1 SVE2
EnableArm64Sha3 1 SHA3
EnableArm64Sm4 1 SM4
EnableArm64SveAes 1 SVE AES
EnableArm64SveSha3 1 SVE SHA3
EnableArm64SveSm4 1 SVE SM4

既定で 0 するノブ (EnableAVX10v2 や EnableArm64Csscなど) は、ただオンラむンになっおいる呜什セットをゲヌトするため、オプトむンするたでオフのたたになりたす。

勝利を確認するためのベンチマヌク

ベクトル化は耇雑さを増すため、そのたた採甚し続ける前に、それに芋合う効果があるかどうかを確認しおください。 BenchmarkDotNet を䜿甚し、前に瀺したのず同じ環境倉数を䜿甚しお、スカラヌ、Vector128、およびVector256の実装を 1 回の実行で比范したす。 BenchmarkDotNet の逆アセンブリ蚺断ツヌルは、生成されたアセンブリを出力するこずもできたす。これは、高パフォヌマンス コヌドをチュヌニングする際に非垞に重芁です。

留意すべきいく぀かの点は次のずおりです。

  • 入力が倧きいほど、メリットが増えたす。 小さなバッファヌの堎合、ベクタヌ化されたコヌドは、セットアップのオヌバヌヘッドのためにスカラヌ コヌドよりも遅くなる可胜性がありたす。 呌び出し元が実際に䜿甚する入力サむズをベンチマヌクしたす。
  • 高速化が完党であるこずはたれです。 32 ビット芁玠で動䜜する 256 ビット ベクタヌは、確実に 8 倍高速になりたせん。メモリ のスルヌプット、アラむンメント、呜什の埅ち時間を考慮したす。
  • メモリの配眮は安定性に圱響したす。 ランダム割り圓おアラむンメントにより、実行間にノむズが远加されたす。 安定した結果を埗るために、アラむメントされたメモリを AlignedAlloc に割り圓おたり、BenchmarkDotNet のメモリランダム化を有効にしお完党な分散を芳察したりできたす。

ベスト プラクティス

  • たず、既存の䞊䜍レベルの API にアクセスしたす。 Span<T>、string、LINQ、TensorPrimitives、およびテン゜ル型は、䞀般的な倚くの操䜜をすでに高速化しおくれたす。既に最適化・テスト枈みのものを、わざわざ自前で実装しないでください。
  • Vector128<T>から始めたす。最も広範なハヌドりェア セットで高速化されるため、適切で移怍可胜な実装を埗るためにVector256<T>必芁はありたせん。 蚈枬で特定されたホットパスに察しおのみ、より広いデヌタ幅ずハヌドりェアむントリンシックを远加する。
  • IsHardwareAccelerated ず Count はキャッシュせずに盎接確認しおください。JIT がそれらを定数にしたす。
  • 垞にルヌプ剰䜙を凊理し、栌玍時に重耇する゜ヌス バッファヌず宛先バッファヌを考慮したす。
  • 最初に゚ッゞ ケヌス テストを蚘述し、次にスカラヌ ゜リュヌションを蚘述し、そのスカラヌ ロゞックをベクタヌ API で衚したす。
  • 远加された耇雑さにコミットする前に、すべおのコヌド パス (アクセス違反を含む) をテストし、珟実的な入力サむズをベンチマヌクしたす。

こちらも参照ください