如何在 .NET 中使用 SIMD

news/2025/11/12 18:06:34/文章来源:https://www.cnblogs.com/eventhorizon/p/19214931

目录
  • 什么是 SIMD
  • SIMD 基础 API
    • System.Runtime.Intrinsics 命名空间
    • 如何理解向量的大小
    • 跨平台实现方式
    • SIMD 指令集的使用
  • System.Numerics 命名空间中的 SIMD 支持
    • Vector<T> 结构体
    • Vector2、Vector3 和 Vector4 结构体
    • Matrix2x2、Matrix3x2 和 Matrix4x4 结构体
  • 其他 SIMD 的使用场景举例举例
    • 字母大小写转换
    • 二进制/位操作
  • 总结

什么是 SIMD

SIMD(Single Instruction, Multiple Data) 译为 单指令多数据,是一种并行计算技术,允许单条指令同时对多个数据元素进行操作,从而提高计算效率。

与 SIMD 相对的是 SISD(Single Instruction, Single Data,单指令单数据),即每条指令只处理一个数据元素。

现在的大多数 CPU 都支持 SIMD 指令集,例如 Intel 的 SSE 和 AVX,ARM 的 NEON 等。

如果我们要对两组数组进行加法运算,传统方法(SISD)是逐个元素相加,而使用 SIMD 技术,可以一次性将多个元素加载到向量寄存器中,并执行单一的加法指令,从而显著提高计算效率。

SIMD 图示

下面我们通过一个简单的示例,对比传统的数组加法和使用 SIMD 优化后的数组加法在性能上的差异。例子中会对两个浮点数组进行加法运算,把结果存储在第三个数组中。

using System.Runtime.Intrinsics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;[MemoryDiagnoser]
public class SimdBenchmark
{private float[] _arrA;private float[] _arrB;private float[] _resultArray;private readonly int _dataSize = 1_000_000;[GlobalSetup]public void Setup(){var random = new Random();_arrA = new float[_dataSize];_arrB = new float[_dataSize];_resultArray = new float[_dataSize];for (int i = 0; i < _dataSize; i++){_arrA[i] = (float)random.NextDouble() * 10f;_arrB[i] = (float)random.NextDouble() * 10f;}}[Benchmark]public void NormalAdd(){for (int i = 0; i < _dataSize; i++){_resultArray[i] = _arrA[i] + _arrB[i];}}[Benchmark]public void SimdAdd(){// 每次处理 4 个元素int simdLength = Vector128<float>.Count; // 4int i = 0;// 处理可被 SIMD 整除的部分for (; i <= _dataSize - simdLength; i += simdLength){var va = Vector128.Create(_arrA[i], _arrA[i + 1], _arrA[i + 2], _arrA[i + 3]);var vb = Vector128.Create(_arrB[i], _arrB[i + 1], _arrB[i + 2], _arrB[i + 3]);(va + vb).CopyTo(_resultArray, i);}// 处理尾部不足 4 个的元素for (; i < _dataSize; i++){_resultArray[i] = _arrA[i] + _arrB[i];}}
}public class Program
{public static void Main(string[] args){BenchmarkRunner.Run<SimdBenchmark>();}
}
BenchmarkDotNet v0.15.6, macOS Sequoia 15.7.2 (24G325) [Darwin 24.6.0]
Apple M2 Max, 1 CPU, 12 logical and 12 physical cores
.NET SDK 9.0.100[Host]     : .NET 9.0.0 (9.0.0, 9.0.24.52809), Arm64 RyuJIT armv8.0-aDefaultJob : .NET 9.0.0 (9.0.0, 9.0.24.52809), Arm64 RyuJIT armv8.0-a| Method    | Mean     | Error   | StdDev  | Allocated |
|---------- |---------:|--------:|--------:|----------:|
| NormalAdd | 880.4 us | 9.28 us | 7.75 us |         - |
| SimdAdd   | 568.5 us | 4.18 us | 3.70 us |         - |

笔者在 MacBook Pro M2 Max 上测试,使用 SIMD 优化后的数组加法运算相比传统方法提升了约 35% 的性能。

此处所使用的例子可能会受到结果需要拷贝到结果数组的影响,实际应用中如果能直接在向量上进行更多计算,性能提升会更加显著。

此例子也可以在 Windows 和 Linux 上运行,有兴趣的读者可以自行测试不同平台的性能差异。

SIMD 基础 API

System.Runtime.Intrinsics 命名空间

.NET 为我们提供了下面三个命名空间来使用 SIMD 技术:

  • System.Runtime.Intrinsics :包含用于创建和传递各种大小和格式的寄存器状态的类型。
  • System.Runtime.Intrinsics.X86 :包含特定于 x86/x64 架构的 SIMD 指令集的类型。
  • System.Runtime.Intrinsics.Arm :包含特定于 ARM 架构的 SIMD 指令集的类型。

System.Runtime.Intrinsics 命名空间中定义了表示不同大小向量的结构体和提供创建及操作这些向量的静态类。

结构体

类型 描述
Vector64<T> 表示指定数值类型的 64 位向量,该向量适用于并行算法的低级别优化。
Vector128<T> 表示指定数值类型的 128 位向量,该向量适用于并行算法的低级别优化。
Vector256<T> 表示指定数值类型的 256 位向量,该向量适用于并行算法的低级别优化。
Vector512<T> 表示指定数值类型的 512 位向量,该向量适用于并行算法的低级别优化。

静态类

类型 描述
Vector64 提供静态方法的集合,用于在 64 位向量上创建、操作和以其他方式操作。
Vector128 提供静态方法集合,用于在 128 位向量上创建、操作和以其他方式操作。
Vector256 提供静态方法集合,用于在 256 位向量上创建、操作和以其他方式操作。
Vector512 提供静态方法的集合,用于在 512 位向量上创建、操作和以其他方式操作。

System.Runtime.Intrinsics.X86System.Runtime.Intrinsics.Arm 命名空间中定义了特定于各自架构的 SIMD 指令集的类,这些类提供了访问底层硬件 SIMD 指令的能力。

常见的指令集类例如:

类型 描述
Sse 提供对 x86/x64 SSE 指令集的访问。
Sse2 提供对 x86/x64 SSE2 指令集的访问。
Avx 提供对 x86/x64 AVX 指令集的访问。
Avx2 提供对 x86/x64 AVX2 指令集的访问。
AdvSimd 提供对 ARM Advanced SIMD(NEON)指令集的访问。

更详细的列表可以参考官方文档:

System.Runtime.Intrinsics.X86 命名空间

System.Runtime.Intrinsics.Arm 命名空间

如何理解向量的大小

向量的大小(如 64 位、128 位、256 位、512 位)指的是向量寄存器能够容纳的数据总位数。每个向量寄存器可以存储多个数据元素,这些数据元素的类型和数量取决于向量的大小和数据类型的位数。

例如开头用到的 Vector128<float>,它表示一个 128 位的向量寄存器,可以存储 4 个 32 位的浮点数(因为 128 / 32 = 4)。

如果是用来存储 64 位的双精度浮点数(double),则 Vector128<double> 可以存储 2 个双精度浮点数(因为 128 / 64 = 2)。

using System.Runtime.Intrinsics;// 创建一个 128 位的向量,存储 16 个 8 位的 字节
Vector128<byte> vectorByte = Vector128.Create((byte)1, (byte)2, (byte)3, (byte)4,(byte)5, (byte)6, (byte)7, (byte)8,(byte)9, (byte)10, (byte)11, (byte)12,(byte)13, (byte)14, (byte)15, (byte)16);// 创建一个 128 位的向量,存储 4 个 32 位的 浮点数
Vector128<float> vectorFloat = Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f);// 创建一个 256 位的向量,存储 8 个 32 位的 浮点数
Vector256<float> vector256Float = Vector256.Create(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f);// 创建一个 128 位的向量,存储 2 个 64 位的 双精度浮点数
Vector128<double> vectorDouble = Vector128.Create(1.0, 2.0);// 创建一个 256 位的向量,存储 4 个 64 位的 双精度浮点数
Vector256<double> vector256Double = Vector256.Create(1.0, 2.0, 3.0, 4.0);

跨平台实现方式

.NET 的 SIMD 提供了跨平台的实现方式。无论是在 x86/x64 还是 ARM 架构上,.NET 都会根据运行时环境自动选择合适的 SIMD 指令集来执行向量化操作。

我们可以使用 VectorXXX.IsHardwareAccelerated 属性来检查当前平台是否支持特定大小的向量操作。例如:

Console.WriteLine(Vector128.IsHardwareAccelerated ? "128 位向量操作受支持" : "128 位向量操作不受支持");

但即使硬件不支持 SIMD,.NET 仍然会回退到非 SIMD 的实现方式,确保代码的兼容性。

VectorXXX 为我们提供了一组静态方法,用于创建和操作向量。例如,Vector128.Add 方法用于对两个 128 位向量执行加法运算。

我们也可以直接使用运算符号来进行向量运算,例如 +-*/ 等。VectorXXX<T> 结构体重载了这些运算符,使得向量运算更加直观和简洁。

下面这个例子使用 Vector128<float> 来进行浮点数的 SIMD 运算:

using System.Runtime.Intrinsics;// 创建两个 128 位的浮点向量
Vector128<float> vectorA = Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f);
Vector128<float> vectorB = Vector128.Create(5.0f, 6.0f, 7.0f, 8.0f);// 执行加法运算
// 等效于 vectorA + vectorB
var result = Vector128.Add(vectorA, vectorB);// 输出结果
Console.WriteLine($"Result: {result}");
Result: <6, 8, 10, 12>

SIMD 指令集的使用

在使用 SIMD 指令集之前,通常需要检查当前平台是否支持特定的指令集。可以通过调用指令集类的 IsSupported 属性来进行检查。例如:

using System.Runtime.Intrinsics.X86;Console.WriteLine(Sse.IsSupported ? "SSE 指令集受支持" : "SSE 指令集不受支持");

一旦确认指令集受支持,就可以使用该指令集类提供的静态方法来执行 SIMD 操作。例如,使用 Sse 类的 Add 方法来对两个 128 位向量执行加法运算:

using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;if (Sse.IsSupported)
{// 创建两个 128 位的浮点向量Vector128<float> vectorA = Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f);Vector128<float> vectorB = Vector128.Create(5.0f, 6.0f, 7.0f, 8.0f);// 使用 SSE 指令集执行加法运算var result = Sse.Add(vectorA, vectorB);// 输出结果Console.WriteLine($"Result: {result}");
}
else
{Console.WriteLine("SSE 指令集不受支持");
}
Result: <6, 8, 10, 12>

如果是在 ARM 架构上,可以使用 AdvSimd 类来执行类似的操作:

using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;if (AdvSimd.IsSupported)
{// 创建两个 128 位的浮点向量Vector128<float> vectorA = Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f);Vector128<float> vectorB = Vector128.Create(5.0f, 6.0f, 7.0f, 8.0f);// 使用 AdvSimd 指令集执行加法运算var result = AdvSimd.Add(vectorA, vectorB);// 输出结果Console.WriteLine($"Result: {result}");
}
else
{Console.WriteLine("AdvSimd 指令集不受支持");
}
Result: <6, 8, 10, 12>

System.Numerics 命名空间中的 SIMD 支持

基于 System.Runtime.Intrinsics,.NET 还提供了别的更高级别的 SIMD 支持。

比如在 System.Numerics 这个命名空间提供了一些易于使用的类型,如 Vector\<T>Matrix4x4,简化了 SIMD 编程。

Vector<T> 结构体

Vector<T> 是一个通用的向量类型,支持多种数值类型(如 int、float、double 等)。它会根据硬件能力自动选择最佳的向量大小(如 128 位或 256 位),从而实现跨平台的 SIMD 优化。

下面是一个使用 Vector<T> 进行数组加法的示例:

var vectorSize = Vector<float>.Count; // 获取向量大小(元素个数)
float[] arrayA = new float[1000];
float[] arrayB = new float[1000];
float[] resultArray = new float[1000];for (int i = 0; i <= arrayA.Length - vectorSize; i += vectorSize)
{var va = new Vector<float>(arrayA, i);var vb = new Vector<float>(arrayB, i);(va + vb).CopyTo(resultArray, i);
}

Vector2、Vector3 和 Vector4 结构体

System.Numerics 命名空间还提供了 Vector2、Vector3 和 Vector4 结构体,分别表示二维、三维和四维向量,常用于图形和物理计算中。

using System.Numerics;// 创建 Vector3 实例
Vector3 vector1 = new Vector3(1.0f, 2.0f, 3.0f);
Vector3 vector2 = new Vector3(4.0f, 5.0f, 6.0f);// 向量加法
Vector3 resultAdd = Vector3.Add(vector1, vector2);
Console.WriteLine($"Addition: {resultAdd}"); // 输出: <5, 7, 9>// 向量点乘
float dotProduct = Vector3.Dot(vector1, vector2);
Console.WriteLine($"Dot Product: {dotProduct}"); // 输出: 32// 向量归一化
Vector3 normalized = Vector3.Normalize(vector1);
Console.WriteLine($"Normalized: {normalized}"); // 输出: <0.2672612, 0.5345225, 0.8017837>

如果我们去看 Vector3 结构体的源码实现,会发现它内部使用了 SIMD 技术来优化向量运算:

public struct Vector3 : IEquatable<Vector3>, IFormattable
{/// <summary>The X component of the vector.</summary>public float X;/// <summary>The Y component of the vector.</summary>public float Y;/// <summary>The Z component of the vector.</summary>public float Z;public Vector3(float x, float y, float z) => this = Vector3.Create(x, y, z);public static Vector3 Create(float x, float y, float z){return Vector128.Create(x, y, z, 0.0f).AsVector3();}// 省略其他成员...
}

Matrix2x2、Matrix3x2 和 Matrix4x4 结构体

System.Numerics 还提供了 Matrix2x2、Matrix3x2 和 Matrix4x4 结构体,用于表示二维和三维空间中的矩阵,常用于变换和投影计算。

using System.Numerics;// 创建一个 4x4 矩阵
Matrix4x4 matrix = Matrix4x4.CreateRotationX((float)(Math.PI / 4));
Vector3 point = new Vector3(1.0f, 0.0f, 0.0f);// 使用矩阵变换点
Vector3 transformedPoint = Vector3.Transform(point, matrix);
Console.WriteLine($"Transformed Point: {transformedPoint}");
Transformed Point: <1, 0, 0>

其他 SIMD 的使用场景举例举例

字母大小写转换

在 ASCII 码表的设计中,大写字母和小写字母之间的差异仅在于第 6 位(从右往左数)。大写字母的第 6 位为 0,而小写字母的第 6 位为 1。因此,我们可以通过对字符的二进制表示进行按位操作来实现大小写转换。

示例:字母 A B C D 的 ASCII 编码对照

字符 ASCII 十进制 ASCII 十六进制 二进制表示
A 65 0x41 01000001
B 66 0x42 01000010
C 67 0x43 01000011
D 68 0x44 01000100
a 97 0x61 01100001
b 98 0x62 01100010
c 99 0x63 01100011
d 100 0x64 01100100

可以看到,对应的大写和小写之间,二进制的第 6 位(从右数,值为 32,即 0x20)状态不同:

  • 大写:第 6 位为 0
  • 小写:第 6 位为 1

这样,只需用 按位或(OR) 大写字母加上 0x20,就能得到对应的小写字母;
按位与(AND) 去掉 0x20,可以从小写变成大写。

System.Text 命名空间中,.NET 提供了 Ascii 类,里面包含了一些用于 ASCII 字符处理的静态方法。我们可以利用 SIMD 技术来实现高效的大小写转换。

public static class Ascii
{public static OperationStatus ToUpper(ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten);public static OperationStatus ToLower(ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten);public static OperationStatus ToUpperInPlace(Span<byte> value, out int bytesWritten);public static OperationStatus ToLowerInPlace(Span<byte> value, out int bytesWritten);public static OperationStatus ToUpper(ReadOnlySpan<char> source, Span<char> destination, out int charsWritten);public static OperationStatus ToLower(ReadOnlySpan<char> source, Span<char> destination, out int charsWritten);public static OperationStatus ToUpperInPlace(Span<char> value, out int charsWritten);public static OperationStatus ToLowerInPlace(Span<char> value, out int charsWritten);
}public enum OperationStatus
{/// <summary>The entire input buffer has been processed and the operation is complete.</summary>Done,/// <summary>The input is partially processed, up to what could fit into the destination buffer. The caller can enlarge the destination buffer, slice the buffers appropriately, and retry.</summary>DestinationTooSmall,/// <summary>The input is partially processed, up to the last valid chunk of the input that could be consumed. The caller can stitch the remaining unprocessed input with more data, slice the buffers appropriately, and retry.</summary>NeedMoreData,/// <summary>The input contained invalid bytes which could not be processed. If the input is partially processed, the destination contains the partial result. This guarantees that no additional data appended to the input will make the invalid sequence valid.</summary>InvalidData,
}

参数的重载有 bytechar 两种,分别用于处理保存为 byte 和 char 类型的 ASCII 字符数据。

如果是从 IO 流中读取数据进行大小写转换,可以使用 Span<byte> 版本;如果是处理 char 数组或字符串,则使用 Span<char> 版本。

ToUpperToLower 方法会将源数据转换为目标数据,并返回一个 OperationStatus 枚举值,指示操作的状态。

ToUpperInPlaceToLowerInPlace 方法则会直接在原始数据上进行大小写转换。

需要注意的是,这些方法仅处理 ASCII 范围内的字符(0-127),对于非 ASCII 字符不会进行任何转换。

这些方法并不能替代 string.ToUpper 等方法来获取 string 的大写或小写形式。需考虑 stringSpan<byte>, Span<char> 之间的转换开销,最终性能并不一定优于直接使用 string.ToUpper 等方法。

可以参考微软的开源项目 Garnet 中的使用场景 AsciiUtils.cs

Ascii.ToUpperInPlace(Span<char> value, out int charsWritten) 的核心实现经整理后大致如下:

void ToUpperInPlace(Span<char> value)
{// 将 Span<char> 转换成 Span<ushort>(char 占 2 字节,方便 SIMD 处理)var buffer = MemoryMarshal.Cast<char, ushort>(value);// 获取元素数量(ushort 数量)var elementCount = (uint)buffer.Length;// 每个向量能处理多少个 ushortvar numElementsPerVector = (uint)(Unsafe.SizeOf<Vector128<byte>>() / sizeof(ushort));// 如果支持 SIMD 且数据足够,否则走普通循环if (Vector128.IsHardwareAccelerated && elementCount >= numElementsPerVector){// 有符号最小值 (0x8000) 用于偏移正确比较ushort sourceSignedMinValue = (ushort)(1 << (8 * sizeof(ushort) - 1));// 'a' 的基准向量(所有元素都是 0x8000 + 'a')var subtractionVector = Vector128.Create((ushort)(sourceSignedMinValue + 'a'));// 26 个字母范围偏移向量(所有元素都是 0x8000 + 26)var comparisonVector = Vector128.Create((ushort)(sourceSignedMinValue + 26));// 大小写差值向量(0x20,所有元素都是这个值)var caseConversionVector = Vector128.Create((ushort)0x20);// 向量化循环索引uint i = 0;// 可以整除的元素个数uint n = elementCount - (elementCount % numElementsPerVector);// 向量化批量处理for (; i < n; i += numElementsPerVector){// 加载当前批次的向量数据var srcVector = Vector128.LoadUnsafe(ref Unsafe.Add(ref MemoryMarshal.GetReference(buffer), (int)i));// 计算 src - 'a' 基准,并判断是否小于 26(即在 a..z 范围)var matches = SignedLessThan(srcVector - subtractionVector, comparisonVector);// 对匹配的小写字母执行大小写转换(异或 0x20 得到大写)srcVector ^= matches & caseConversionVector;// 存回修改后的向量数据srcVector.StoreUnsafe(ref Unsafe.Add(ref MemoryMarshal.GetReference(buffer), (int)i));}// 处理剩余不足一个向量大小的元素for (; i < elementCount; i++){// 读取当前字符ushort c = Unsafe.Add(ref MemoryMarshal.GetReference(buffer), (int)i);// 如果是小写字母则转为大写if (c is >= 'a' and <= 'z'){c = (ushort)(c - 0x20);}// 写回结果Unsafe.Add(ref MemoryMarshal.GetReference(buffer), (int)i) = c;}}else{// 非向量化处理,每个元素单独判断for (int i = 0; i < buffer.Length; i++){ushort c = buffer[i];if (c is >= 'a' and <= 'z'){c = (ushort)(c - 0x20);}buffer[i] = c;}}
}// 有符号比较(用于判断 a..z 范围)
Vector128<ushort> SignedLessThan(Vector128<ushort> left, Vector128<ushort> right)
{// 将 ushort 当成 short 做有符号比较,然后再转换回 ushort 掩码return Vector128.LessThan(left.AsInt16(), right.AsInt16()).AsInt16().AsUInt16();
}

调用上述方法可以高效地将 ASCII 字符串转换为大写形式:

string input = "Hello World! This is a Test String.";
Span<char> span = input.ToCharArray();
ToUpperInPlace(span);
string result = new string(span);
Console.WriteLine(result); // 输出: "HELLO WORLD! THIS IS A TEST STRING."

实际的 Ascii 类实现要复杂得多,包含了更多的边界检查和错误处理逻辑,上述代码仅仅是为了说明核心的 SIMD 思路的简化版本。

使用 Ascii.UpperInPlace 的示例:

using System.Text;string input = "Hello World! This is a Test String. 这部分不会被转换。";
Span<char> span = input.ToCharArray();Ascii.ToUpperInPlace(span, out int charsWritten);// charsWritten 表示实际转换的字符数,非 ASCII 字符不会被转换
string result = new string(span[..charsWritten]);
Console.WriteLine(result); // 输出: "HELLO WORLD! THIS IS A TEST STRING."

二进制/位操作

SIMD 技术非常适合处理大量的二进制数据或位操作。例如,BinaryPrimitives.ReverseEndianness 方法利用 SIMD 来高效地反转字节序:

Span<ushort> data = [0x1234, 0xABCD, 0x5678, 0xEF01];
Span<ushort> reversedData = stackalloc ushort[4];
BinaryPrimitives.ReverseEndianness(data, reversedData);foreach (var value in reversedData)
{Console.WriteLine(value.ToString("X4"));
}
3412
CDAB
7856
01EF

总结

SIMD 技术在 .NET 中提供了强大的并行计算能力,能够显著提升处理大量数据时的性能。通过 System.Runtime.IntrinsicsSystem.Numerics,我们可以方便地利用 SIMD 指令集进行高效的向量化运算。

很多基础库已经内置了 SIMD 优化,开发者在日常编程中可以通过这些库间接受益于 SIMD 技术,而无需深入了解底层实现细节。当然,对于性能敏感的应用场景,理解和直接使用 SIMD 指令集仍然是非常有价值的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/963667.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux shell映射表(变量的变量)

前言全局说明一、说明 1.1 环境: Ubuntu 22.04二、映射表 declare -A ARCH_MAP=(["arm"]="arm-linux-gnueabihf"["aarch64"]="aarch64-linux-gnu"["mips"]="m…

前端 GIT 使用技巧

Hello World本文来自博客园,作者:南宫影,转载请注明原文链接:https://www.cnblogs.com/nangongying/p/19214917

详细介绍:显卡算力过高导致PyTorch不兼容的救赎指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025/11/13

2025/11/13循环不变量原则: 循环不变量原则是算法设计与证明中用于确保循环逻辑正确性的核心思想,指在循环执行的每一轮前后,都保持一个固定的、为真的命题(不变量)。 这个命题明确了循环变量、数据结构或窗口的核…

Linux《网络基础》 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

《程序员修炼之道》阅读笔记4

按合约设计 按合约设计(Design by Contract,DBC)是一种基于合约的软件开发方法,它借鉴了现实世界中合约的概念,明确模块之间的权利与责任。在软件系统中,每个函数或方法都有其特定的职责,DBC通过定义前条件、后…

记一次 .NET 某医联体管理系统 崩溃分析

一:背景 1. 讲故事 这段时间都在跑外卖,感觉好久都没写文章了,今天继续给大家带来一篇崩溃类的生产事故,这是微信上有位老朋友找到我的,让我帮忙看下为啥崩溃了,dump也在手,接下来就可以一顿分析。 二:崩溃分析…

如何构建可信智能 Data Agent?推荐 Aloudata Agent 分析决策智能体

企业构建可信智能的 Data Agent 需以强大的数据底座为支撑,统一指标语义层和 NoETL 数据工程成为关键摘要: 在 AI 与大数据深度融合的当下,数据分析民主化日渐火热。Aloudata Agent 分析决策智能体依托于统一的指标…

Java 集合-Set

Java 集合 - Set 详解 集合(Set)是用于存储和处理无重复元素的高效数据结构,映射表(Map)则类似目录,支持通过键值快速查询和获取对应值。例如检验某人是否在论文答辩名单中,用 Set 实现比线性表更高效;若需存储…

#题解#牛客:牛牛的构造#DP#构造#

传送门 分析 1.容易发现的一件事,当n,n-1,n-2......2,1排列时是满足条件的(i,j)对最多的n排列 2.我们用递推的想法求每一个n的最大(i,j)对数ans[n] ans[0] = 0;int pre = 0;int x = 0;for (int i = 1; i <= …

Machine Learning - SVM Part 2: The Radial Kernel

Machine Learning - SVM Part 2: The Radial Kernel

2025-11-12 ZYZ28-NOIP-aoao round 2 hetao1733837的record

2025-11-12 ZYZ28-NOIP-aoao round 2 hetao1733837的record比赛链接:比赛详情 - ZYZ28-NOIP-aoao round 2 - ZYZOJ 比赛背景 昨天双十一,ZYZ著名NOI Cu选手@[TaoRan](用户详情 - TaoRan - ZYZOJ)爆出了惊天大瓜——…

2025/11/12

2025/11/12滑动窗口的核心是通过维护一个动态调整的 “窗口”(连续子区间),用 O (n) 时间复杂度替代暴力枚举的 O (n),避免重复计算。 其核心逻辑是用左右指针界定窗口范围,根据问题条件移动指针收缩或扩展窗口,…

redis stream介绍

介绍 redis stream是一种类似日志追加的数据结构。可用来记录和实时处理事件。适用场景:事件溯源 传感器监控 通知性能 新增 O(1) 访问单个节点是O(n),n是ID的长度 redis stream使用radix trees实现 基础 XADD 新增条…

Java 线性表、栈、队列和优先队列

Java 线性表、栈、队列和优先队列 选择合适的数据结构和算法是开发高性能软件的关键。数据结构是按特定形式组织数据的集合,不仅存储数据,还支持数据的访问与处理操作。 在面向对象思想中,数据结构被视为容器或容器…

2025/11/11

2025/11/11全流程开发逻辑 从前端表单设计、后端 Servlet 处理、DAO 层数据库操作,到 MySQL 表结构设计,完整覆盖了 “用户交互 -> 业务逻辑 -> 数据存储” 的 Web 应用开发流程。 问题排查方法 面对代码报错,…

植物大战僵尸修改器下载教程:图文详解与实用技巧

前言: 在塔防游戏还没有被各类快节奏手游占据之前,《植物大战僵尸》几乎是每位玩家电脑中必装的一款经典作品。即便十多年过去,它依然凭借简单的机制与极高的策略深度拥有极强的生命力。 对于不少老玩家来说,玩到中…

微服务——注册中心

常见的注册中心:eureka、nacos、zookeeper 服务注册和发现是什么意思?Spring Cloud是如何实现服务注册发现? 服务注册:服务提供者需要把自己的信息注册到eureka,由eureka来保存这些信息,比如服务名称、IP、端口等…

【深度学习计算机视觉】13:实战Kaggle比赛:图像分类 (CIFAR-10) - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

fabricjs 整合 vue3-sketch-ruler 实现标尺功能

版本信息 fabricjs版本为6.7.1 ------ vue3-sketch-ruler的版本为1.3.15 引入标尺的部分代码<!-- 画布区域 --><div id="workspace" style="width: 100%;height: 100%; position: relative; …