exp-simd-vectorization

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SIMD Vectorization

SIMD向量化

Decision Gate

决策流程

  1. Check
    Span<T>
    and
    MemoryExtensions
    first.
    If the operation can be expressed using built-in
    Span<T>
    methods (e.g.,
    Contains
    ,
    IndexOf
    ,
    CopyTo
    ,
    SequenceEqual
    ) or
    MemoryExtensions
    , use them — no additional dependency is needed and the runtime already vectorizes many of these internally.
  2. Check for TensorPrimitives next. If one or more TensorPrimitives methods cover the operation → use them. If the
    .csproj
    does NOT already reference
    System.Numerics.Tensors
    , add the package, for example:
    <PackageReference Include="System.Numerics.Tensors" />
    (or use the versioning approach already used by your solution). Then replace the scalar loop with TP calls and stop. See the full API table below. Compose multiple TP calls when needed (e.g., finding both min and max →
    TensorPrimitives.Min(span)
    +
    TensorPrimitives.Max(span)
    as two calls). Do NOT write manual Vector128 code for operations TP already handles.
  3. Scalar loop over contiguous array/span of
    byte
    ,
    sbyte
    ,
    short
    ,
    ushort
    ,
    int
    ,
    uint
    ,
    long
    ,
    ulong
    ,
    nint
    ,
    nuint
    ,
    float
    ,
    double
    (and
    char
    via reinterpretation as
    ushort
    )? → Implement with explicit
    Vector128<T>
    /
    Vector256<T>
    /
    Vector512<T>
    intrinsics using the patterns below.
  4. No contiguous numeric arrays to process (dictionary lookups, tree traversals, linked lists, state machines, string formatting, small collections, enum comparisons, recursive algorithms, decimal arithmetic)? → Report
    [NO SIMD OPPORTUNITY]
    and write a full paragraph explaining WHY, referencing the specific code characteristics that prevent vectorization (e.g., "State machines require sequential branching on enum values — there are no contiguous numeric arrays to process in parallel, and each transition depends on the previous state"). This explanation is graded.
  1. 优先检查
    Span<T>
    MemoryExtensions
    。如果操作可以通过内置
    Span<T>
    方法(如
    Contains
    IndexOf
    CopyTo
    SequenceEqual
    )或
    MemoryExtensions
    实现,请直接使用——无需额外依赖,运行时已对其中许多方法进行了向量化优化。
  2. 接下来检查TensorPrimitives。如果有一个或多个TensorPrimitives方法可覆盖当前操作→直接使用。如果
    .csproj
    尚未引用
    System.Numerics.Tensors
    添加该包,例如:
    <PackageReference Include="System.Numerics.Tensors" />
    (或使用解决方案已采用的版本控制方式)。然后用TP调用替换标量循环即可停止操作。请查看下方完整API表。必要时可组合多个TP调用(例如,同时查找最小值和最大值→
    TensorPrimitives.Min(span)
    +
    TensorPrimitives.Max(span)
    两个调用)。对于TP已支持的操作,请勿编写手动Vector128代码。
  3. 针对
    byte
    sbyte
    short
    ushort
    int
    uint
    long
    ulong
    nint
    nuint
    float
    double
    的连续数组/span进行标量循环
    char
    可通过重新解释为
    ushort
    处理)?→使用以下模式,通过显式
    Vector128<T>
    /
    Vector256<T>
    /
    Vector512<T>
    内在函数实现。
  4. 无连续数值数组可处理(字典查找、树遍历、链表、状态机、字符串格式化、小型集合、枚举比较、递归算法、十进制算术)?→标记
    [NO SIMD OPPORTUNITY]
    并撰写完整段落说明原因,引用阻止向量化的具体代码特征(例如:“状态机需要基于枚举值的顺序分支——没有可并行处理的连续数值数组,且每个转换都依赖于前一个状态”)。该说明将作为评估依据。

TensorPrimitives API Reference

TensorPrimitives API参考

TensorPrimitives APIs are generic and work for any primitive type that satisfies the method's generic constraints — not just
float
/
double
. For example,
Sum
requires
IAdditionOperators<T,T,T>
+
IAdditiveIdentity<T,T>
and works for all primitive numeric types, while
CosineSimilarity
requires
IRootFunctions<T>
and only works for
float
/
double
. If the project doesn't already reference
System.Numerics.Tensors
, add it to the
.csproj
. Replace the entire manual loop with one or more
TensorPrimitives
calls as needed (prefer a single call when possible):
TensorPrimitives API是泛型的,适用于满足方法泛型约束的任何基元类型——不仅限于
float
/
double
。例如,
Sum
要求
IAdditionOperators<T,T,T>
+
IAdditiveIdentity<T,T>
,适用于所有基元数值类型;而
CosineSimilarity
要求
IRootFunctions<T>
,仅适用于
float
/
double
。如果项目尚未引用
System.Numerics.Tensors
,请将其添加到
.csproj
中。根据需要用一个或多个
TensorPrimitives
调用替换整个手动循环(尽可能优先使用单个调用):

Reductions (span → scalar)

归约运算(span → 标量)

OperationAPI
Sum
TensorPrimitives.Sum(span)
Sum of squares
TensorPrimitives.SumOfSquares(span)
Sum of magnitudes (L1 norm)
TensorPrimitives.SumOfMagnitudes(span)
L2 norm
TensorPrimitives.Norm(span)
Product of all elements
TensorPrimitives.Product(span)
Min value
TensorPrimitives.Min(span)
Max value
TensorPrimitives.Max(span)
Index of max
TensorPrimitives.IndexOfMax(span)
Index of min
TensorPrimitives.IndexOfMin(span)
Dot product
TensorPrimitives.Dot(a, b)
Cosine similarity
TensorPrimitives.CosineSimilarity(a, b)
Euclidean distance
TensorPrimitives.Distance(a, b)
操作API
求和
TensorPrimitives.Sum(span)
平方和
TensorPrimitives.SumOfSquares(span)
绝对值和(L1范数)
TensorPrimitives.SumOfMagnitudes(span)
L2范数
TensorPrimitives.Norm(span)
所有元素乘积
TensorPrimitives.Product(span)
最小值
TensorPrimitives.Min(span)
最大值
TensorPrimitives.Max(span)
最大值索引
TensorPrimitives.IndexOfMax(span)
最小值索引
TensorPrimitives.IndexOfMin(span)
点积
TensorPrimitives.Dot(a, b)
余弦相似度
TensorPrimitives.CosineSimilarity(a, b)
欧氏距离
TensorPrimitives.Distance(a, b)

Element-wise transforms (span → span)

逐元素转换(span → span)

OperationAPI
Negate
TensorPrimitives.Negate(src, dst)
Abs
TensorPrimitives.Abs(src, dst)
Sqrt
TensorPrimitives.Sqrt(src, dst)
Exp
TensorPrimitives.Exp(src, dst)
Log
TensorPrimitives.Log(src, dst)
Log2
TensorPrimitives.Log2(src, dst)
Tanh
TensorPrimitives.Tanh(src, dst)
Sigmoid
TensorPrimitives.Sigmoid(src, dst)
SoftMax
TensorPrimitives.SoftMax(src, dst)
Sinh
TensorPrimitives.Sinh(src, dst)
Cosh
TensorPrimitives.Cosh(src, dst)
Round
TensorPrimitives.Round(src, dst)
Floor
TensorPrimitives.Floor(src, dst)
Ceiling
TensorPrimitives.Ceiling(src, dst)
CopySign
TensorPrimitives.CopySign(src, sign, dst)
Pow
TensorPrimitives.Pow(bases, exponents, dst)
操作API
取反
TensorPrimitives.Negate(src, dst)
绝对值
TensorPrimitives.Abs(src, dst)
平方根
TensorPrimitives.Sqrt(src, dst)
指数运算
TensorPrimitives.Exp(src, dst)
自然对数
TensorPrimitives.Log(src, dst)
以2为底的对数
TensorPrimitives.Log2(src, dst)
双曲正切
TensorPrimitives.Tanh(src, dst)
Sigmoid函数
TensorPrimitives.Sigmoid(src, dst)
SoftMax函数
TensorPrimitives.SoftMax(src, dst)
双曲正弦
TensorPrimitives.Sinh(src, dst)
双曲余弦
TensorPrimitives.Cosh(src, dst)
四舍五入
TensorPrimitives.Round(src, dst)
向下取整
TensorPrimitives.Floor(src, dst)
向上取整
TensorPrimitives.Ceiling(src, dst)
复制符号
TensorPrimitives.CopySign(src, sign, dst)
幂运算
TensorPrimitives.Pow(bases, exponents, dst)

Two-span operations (a, b → dst)

双span运算(a, b → dst)

OperationAPI
Add
TensorPrimitives.Add(a, b, dst)
Subtract
TensorPrimitives.Subtract(a, b, dst)
Multiply
TensorPrimitives.Multiply(a, b, dst)
Divide
TensorPrimitives.Divide(a, b, dst)
Element-wise Min
TensorPrimitives.Min(a, b, dst)
Element-wise Max
TensorPrimitives.Max(a, b, dst)
操作API
加法
TensorPrimitives.Add(a, b, dst)
减法
TensorPrimitives.Subtract(a, b, dst)
乘法
TensorPrimitives.Multiply(a, b, dst)
除法
TensorPrimitives.Divide(a, b, dst)
逐元素最小值
TensorPrimitives.Min(a, b, dst)
逐元素最大值
TensorPrimitives.Max(a, b, dst)

Three-span fused operations

三span融合运算

OperationAPI
(x+y)*z
TensorPrimitives.AddMultiply(x, y, z, dst)
x*y+z
TensorPrimitives.MultiplyAdd(x, y, z, dst)
fma(x,y,z)
TensorPrimitives.FusedMultiplyAdd(x, y, z, dst)
AddMultiply
and
MultiplyAdd
are distinct — they optimize differently depending on whether the dependency chain flows from the addend or the multiplier.
FusedMultiplyAdd
is the IEEE 754 fused form of (x*y)+z with a single rounding step.
操作API
(x+y)*z
TensorPrimitives.AddMultiply(x, y, z, dst)
x*y+z
TensorPrimitives.MultiplyAdd(x, y, z, dst)
fma(x,y,z)
TensorPrimitives.FusedMultiplyAdd(x, y, z, dst)
AddMultiply
MultiplyAdd
是不同的——根据依赖链来自加数还是乘数,它们的优化方式不同。
FusedMultiplyAdd
是(x*y)+z的IEEE 754融合形式,仅执行一次舍入步骤。

Manual SIMD with Vector128/Vector256/Vector512

基于Vector128/Vector256/Vector512的手动SIMD实现

Use this when TensorPrimitives doesn't have a single API for the operation. This is required for byte-level operations, character class counting, range validation, bitwise bulk ops, cross-type conversions, and custom patterns.
当TensorPrimitives没有对应单个API的操作时使用此方法。这适用于字节级操作、字符类别计数、范围验证、批量位运算、跨类型转换和自定义模式。

Required imports

必要导入

csharp
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
Prefer cross-platform APIs (
System.Runtime.Intrinsics
). Only use platform-specific intrinsics (
System.Runtime.Intrinsics.X86
,
.Arm
) when there is a significant performance advantage that justifies the increased code complexity of maintaining separate code paths.
csharp
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
优先使用跨平台API(
System.Runtime.Intrinsics
)。仅当存在显著性能优势且值得承担维护不同代码路径带来的复杂度提升时,才使用平台特定内在函数(
System.Runtime.Intrinsics.X86
.Arm
)。

Three-tier dispatch pattern

三层分派模式

Always include all three tiers. Use
if
/
else if
so that small inputs hit only one branch before reaching the scalar fallback — a fallthrough pattern (sequential
if
s) pessimizes the scalar case by requiring up to three not-taken branches that may mispredict. The
IsHardwareAccelerated
checks are JIT-time constants, so dead paths are eliminated at compile time:
csharp
ref var src = ref MemoryMarshal.GetReference(span);
uint i = 0;
uint length = (uint)span.Length;

if (Vector512.IsHardwareAccelerated && Vector512<T>.IsSupported)
{
    uint vec512Count = (uint)Vector512<T>.Count;
    while (i + vec512Count <= length)
    {
        var vec = Vector512.LoadUnsafe(ref src, i);
        // ... process vec ...
        i += vec512Count;
    }
}
else if (Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported)
{
    uint vec256Count = (uint)Vector256<T>.Count;
    while (i + vec256Count <= length)
    {
        var vec = Vector256.LoadUnsafe(ref src, i);
        // ... process vec ...
        i += vec256Count;
    }
}
else if (Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported)
{
    uint vec128Count = (uint)Vector128<T>.Count;
    while (i + vec128Count <= length)
    {
        var vec = Vector128.LoadUnsafe(ref src, i);
        // ... process vec ...
        i += vec128Count;
    }
}
// Scalar fallback for remaining elements (and the only loop hit for small inputs)
for (; i < length; i++)
{
    // ... scalar processing ...
}
始终包含所有三层。使用
if
/
else if
结构,以便小输入只需命中一个分支即可进入标量回退——穿透模式(连续
if
)会通过要求最多三个未命中分支(可能导致预测错误)来降低标量情况的性能。
IsHardwareAccelerated
检查是JIT时的常量,因此死代码路径会在编译时被消除:
csharp
ref var src = ref MemoryMarshal.GetReference(span);
uint i = 0;
uint length = (uint)span.Length;

if (Vector512.IsHardwareAccelerated && Vector512<T>.IsSupported)
{
    uint vec512Count = (uint)Vector512<T>.Count;
    while (i + vec512Count <= length)
    {
        var vec = Vector512.LoadUnsafe(ref src, i);
        // ... 处理vec ...
        i += vec512Count;
    }
}
else if (Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported)
{
    uint vec256Count = (uint)Vector256<T>.Count;
    while (i + vec256Count <= length)
    {
        var vec = Vector256.LoadUnsafe(ref src, i);
        // ... 处理vec ...
        i += vec256Count;
    }
}
else if (Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported)
{
    uint vec128Count = (uint)Vector128<T>.Count;
    while (i + vec128Count <= length)
    {
        var vec = Vector128.LoadUnsafe(ref src, i);
        // ... 处理vec ...
        i += vec128Count;
    }
}
// 剩余元素的标量回退(小输入仅命中此循环)
for (; i < length; i++)
{
    // ... 标量处理 ...
}

Core SIMD operations

核心SIMD操作

  • Load/Store:
    Vector128.LoadUnsafe(ref src, offset)
    /
    .StoreUnsafe(ref dst, offset)
  • Arithmetic:
    +
    ,
    -
    ,
    *
    ,
    /
    operators on vector types
  • Multiply-add (approximate):
    Vector128.MultiplyAddEstimate(a, b, c)
    — performs a multiply-add with implementation-defined approximation; not guaranteed to be a strict IEEE fused multiply-add. For precise fused semantics, use
    Vector128.FusedMultiplyAdd(a, b, c)
    .
  • Comparison:
    Vector128.Equals
    ,
    .LessThan
    ,
    .GreaterThan
    — returns mask vector
  • Mask ops:
    Vector128.All(mask)
    ,
    .Any(mask)
    ,
    .None(mask)
    ,
    .Count(mask)
    ,
    .CountWhereAllBitsSet(mask)
  • Horizontal:
    Vector128.Sum(vec)
    for reduction;
    .Min(a,b)
    ,
    .Max(a,b)
    element-wise
  • Broadcast:
    Vector128.Create(scalarValue)
    — fill all lanes with one value
  • Bitwise:
    &
    ,
    |
    ,
    ^
    ,
    ~
    operators;
    Vector128.ShiftLeft
    ,
    .ShiftRightLogical
  • Widening:
    Vector128.WidenLower(v)
    /
    .WidenUpper(v)
    for byte→short, short→int
  • Narrowing:
    Vector128.Narrow(lower, upper)
    for int→short, short→byte
  • Type convert:
    Vector128.ConvertToSingle(intVec)
    ,
    .ConvertToInt32(floatVec)
  • Shuffle:
    Vector128.Shuffle(vec, indices)
    — lookup table / permutation
  • Conditional:
    Vector128.ConditionalSelect(mask, trueVec, falseVec)
  • 加载/存储:
    Vector128.LoadUnsafe(ref src, offset)
    /
    .StoreUnsafe(ref dst, offset)
  • 算术运算: 向量类型的
    +
    -
    *
    /
    运算符
  • 乘加(近似):
    Vector128.MultiplyAddEstimate(a, b, c)
    ——执行乘加运算,采用实现定义的近似方法;不保证是严格的IEEE融合乘加。如需精确的融合语义,请使用
    Vector128.FusedMultiplyAdd(a, b, c)
  • 比较:
    Vector128.Equals
    .LessThan
    .GreaterThan
    ——返回掩码向量
  • 掩码操作:
    Vector128.All(mask)
    .Any(mask)
    .None(mask)
    .Count(mask)
    .CountWhereAllBitsSet(mask)
  • 水平运算:
    Vector128.Sum(vec)
    用于归约;
    .Min(a,b)
    .Max(a,b)
    用于逐元素比较
  • 广播:
    Vector128.Create(scalarValue)
    ——用单个值填充所有通道
  • 位运算:
    &
    |
    ^
    ~
    运算符;
    Vector128.ShiftLeft
    .ShiftRightLogical
  • 拓宽:
    Vector128.WidenLower(v)
    /
    .WidenUpper(v)
    用于byte→short、short→int转换
  • 收窄:
    Vector128.Narrow(lower, upper)
    用于int→short、short→byte转换
  • 类型转换:
    Vector128.ConvertToSingle(intVec)
    .ConvertToInt32(floatVec)
  • 重排:
    Vector128.Shuffle(vec, indices)
    ——查找表/置换
  • 条件选择:
    Vector128.ConditionalSelect(mask, trueVec, falseVec)

Pattern: Unsigned range check (byte-range validation)

模式:无符号范围检查(字节范围验证)

For checking if all bytes are in range [lo, hi]:
csharp
var vLo = Vector128.Create((byte)lo);
var vRange = Vector128.Create((byte)(hi - lo));
// (b - lo) > range means out-of-range (unsigned wraparound catches b < lo)
var shifted = Vector128.Subtract(vec, vLo);
var inRange = Vector128.LessThanOrEqual(shifted, vRange);
if (!Vector128.All(inRange.AsByte())) return false; // for validation
// or: count += Vector128.CountWhereAllBitsSet(inRange); // for counting
用于检查所有字节是否在范围[lo, hi]内:
csharp
var vLo = Vector128.Create((byte)lo);
var vRange = Vector128.Create((byte)(hi - lo));
// (b - lo) > range 表示超出范围(无符号环绕会捕获b < lo的情况)
var shifted = Vector128.Subtract(vec, vLo);
var inRange = Vector128.LessThanOrEqual(shifted, vRange);
if (!Vector128.All(inRange.AsByte())) return false; // 用于验证
// 或:count += Vector128.CountWhereAllBitsSet(inRange); // 用于计数

Pattern: Nibble-lookup counting (character classes, popcount, etc.)

模式:半字节查找计数(字符类别、popcount等)

For counting bytes matching a sparse set of values (vowels, digits, punctuation, bit counts) — build two 16-byte lookup tables indexed by low/high nibble:
csharp
var lo_lut = Vector128.Create(/* 16 bytes: bit pattern for low nibble match */);
var hi_lut = Vector128.Create(/* 16 bytes: bit pattern for high nibble match */);
var nibbleMask = Vector128.Create((byte)0x0F);

var lo_nibble = vec & nibbleMask;
var hi_nibble = Vector128.ShiftRightLogical(vec.AsUInt16(), 4).AsByte() & nibbleMask;
var lo_match = Vector128.Shuffle(lo_lut, lo_nibble);
var hi_match = Vector128.Shuffle(hi_lut, hi_nibble);
var match = lo_match & hi_match;
count += Vector128.CountWhereAllBitsSet(~Vector128.Equals(match, Vector128<byte>.Zero));
This same technique works for popcount (LUT = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4}). For simpler cases (single byte value, adjacent range), use
Equals
+
Count
or range check instead.
用于统计匹配稀疏值集合的字节数(元音、数字、标点、位计数)——构建两个16字节查找表,按低/高半字节索引:
csharp
var lo_lut = Vector128.Create(/* 16字节:低半字节匹配的位模式 */);
var hi_lut = Vector128.Create(/* 16字节:高半字节匹配的位模式 */);
var nibbleMask = Vector128.Create((byte)0x0F);

var lo_nibble = vec & nibbleMask;
var hi_nibble = Vector128.ShiftRightLogical(vec.AsUInt16(), 4).AsByte() & nibbleMask;
var lo_match = Vector128.Shuffle(lo_lut, lo_nibble);
var hi_match = Vector128.Shuffle(hi_lut, hi_nibble);
var match = lo_match & hi_match;
count += Vector128.CountWhereAllBitsSet(~Vector128.Equals(match, Vector128<byte>.Zero));
此技术同样适用于popcount(查找表={0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4})。对于更简单的情况(单个字节值、相邻范围),请使用
Equals
+
Count
或范围检查替代。

Pattern: Cross-type conversion (widening chains)

模式:跨类型转换(拓宽链)

When the source and destination types differ (e.g., byte→float for dequantization, short→byte for narrowing):
csharp
// Widen: byte → short → int → float
var bytes = Vector128.LoadUnsafe(ref src, offset);
var (lo16, hi16) = Vector128.Widen(bytes);
var (lo32a, lo32b) = Vector128.Widen(lo16);
var f0 = Vector128.ConvertToSingle(lo32a.AsInt32());

// Narrow: int → short → byte (with saturation via Min/Max clamping)
var clamped = Vector128.Min(Vector128.Max(vec, Vector128<short>.Zero), Vector128.Create((short)255));
var narrowed = Vector128.Narrow(clamped.AsUInt16(), nextVec.AsUInt16());
当源类型和目标类型不同时(例如,byte→float用于反量化,short→byte用于收窄):
csharp
// 拓宽:byte → short → int → float
var bytes = Vector128.LoadUnsafe(ref src, offset);
var (lo16, hi16) = Vector128.Widen(bytes);
var (lo32a, lo32b) = Vector128.Widen(lo16);
var f0 = Vector128.ConvertToSingle(lo32a.AsInt32());

// 收窄:int → short → byte(通过Min/Max钳位实现饱和)
var clamped = Vector128.Min(Vector128.Max(vec, Vector128<short>.Zero), Vector128.Create((short)255));
var narrowed = Vector128.Narrow(clamped.AsUInt16(), nextVec.AsUInt16());

Trailing elements

尾部元素处理

  • Idempotent ops (validation, search): overlap last vector — re-processing is safe
  • Aggregations (sum, count, min/max): scalar loop for remainder to avoid double-counting
  • Store ops (transform in-place): use
    ConditionalSelect
    to merge with last stored vector
  • 幂等操作(验证、搜索):重叠最后一个向量——重新处理是安全的
  • 聚合操作(求和、计数、最小/最大值):对剩余元素使用标量循环,避免重复计数
  • 存储操作(原地转换):使用
    ConditionalSelect
    与最后存储的向量合并

Key Rules

关键规则

  • Preserve original method signature — drop-in replacement
  • Keep scalar code as fallback — never delete it
  • Use
    Vector128<T>
    /
    Vector256<T>
    /
    Vector512<T>
    explicitly — never
    Vector<T>
  • Prefer portable
    Vector128<T>
    /
    Vector256<T>
    /
    Vector512<T>
    APIs over platform-specific intrinsics (
    Avx2
    ,
    Sse42
    ,
    AdvSimd
    ,
    Fma
    ) unless there is a significant performance advantage
  • Testing: use
    dotnet run
    (NOT
    dotnet test
    ) — xunit.v3 is an in-process runner
  • 保留原方法签名——实现无缝替换
  • 保留标量代码作为回退——切勿删除
  • 显式使用
    Vector128<T>
    /
    Vector256<T>
    /
    Vector512<T>
    ——绝不使用
    Vector<T>
  • 优先使用可移植的
    Vector128<T>
    /
    Vector256<T>
    /
    Vector512<T>
    API,而非平台特定内在函数(
    Avx2
    Sse42
    AdvSimd
    Fma
    ),除非存在显著性能优势
  • 测试:使用
    dotnet run
    (而非
    dotnet test
    )——xunit.v3是进程内运行器