微信做公司网站怎么做,品牌营销策划书模板,个人免费建站系统,wordpress 阅读更多简介在之前的文章中#xff0c;我们多次提到 Vector - SIMD 技术#xff0c;也答应大家在后面分享更多.NET7 中优化的例子#xff0c;今天就带来一个使用 SIMD 优化Guid.Equals()方法性能的例子。为什么 Guid 能使用 SIMD 优化#xff1f;首先就需要介绍一些背景知识#…简介在之前的文章中我们多次提到 Vector - SIMD 技术也答应大家在后面分享更多.NET7 中优化的例子今天就带来一个使用 SIMD 优化Guid.Equals()方法性能的例子。为什么 Guid 能使用 SIMD 优化首先就需要介绍一些背景知识那就是Guid它是什么在我们人类眼中Guid就是一串字符串如下方所示的那样。D313CD46-2724-7359-84A0-9E73C861CCD2而在定义中全局唯一标识符GUIDGlobally Unique Identifier是一种由算法生成的二进制长度为128 位的数字标识符。GUID 主要用于在拥有多个节点、多台计算机的网络或系统中。在理想情况下任何计算机和计算机集群都不会生成两个相同的 GUID。GUID 的总数达到了 2^1283.4×10^38个所以随机生成两个相同 GUID 的可能性非常小但并不为 0。GUID 一词有时也专指微软对 UUID 标准的实现。大家可以看到我着重标记了它的位数是128 位128 位意味着什么就是如果比较两个 Guid 是否相等的话不管是 64 位 CPU 还是 32 位的 CPU 需要多条指令比较多次。如果我们用上了 Vector是不是会有更好的性能呢首先我们来看看 Guid 是如何定义的看看能不能直接读取 128 位数据从而用上 Vector。Guid 它是值类型的是一个结构体。代码如下所示我省略了部分信息。public readonly partial struct Guid{...private readonly int _a; // Do not rename (binary serialization)private readonly short _b; // Do not rename (binary serialization)private readonly short _c; // Do not rename (binary serialization)private readonly byte _d; // Do not rename (binary serialization)private readonly byte _e; // Do not rename (binary serialization)private readonly byte _f; // Do not rename (binary serialization)private readonly byte _g; // Do not rename (binary serialization)private readonly byte _h; // Do not rename (binary serialization)private readonly byte _i; // Do not rename (binary serialization)private readonly byte _j; // Do not rename (binary serialization)private readonly byte _k; // Do not rename (binary serialization)...}可以看到它由 1 个 32 位 int2 个 16 位的 short 和 8 个 8 位的 byte 组成至于为什么需要这样组成其实是一个标准化的东西为了在生成和序列化时更快。我们使用ObjectLayoutInspector可以打印出 Guid 的数据结构数据结果如下图所示和我们源码里面看到的一致那么 Guid 是否能使用 SIMD 优化的结论显而易见Guid 有 128 位现在 CPU 都是 64 位或者 32 位还存在提升空间Guid 是结构体类型结构体类型在内存中是连续存储我们可以直接读取内存来访问整个结构体SIMD 优化代码根据我们前面文章中Min 和 Max 方法在.NET7 被优化的经验我们可以直接写下面这样的代码。[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool EqualsCore(in Guid left, in Guid right)
{// 检测硬件是否支持Vector128if (Vector128.IsHardwareAccelerated){// 支持Vector128就好办了直接加载比较return Vector128.LoadUnsafe(ref Unsafe.AsGuid, byte(ref Unsafe.AsRef(in left))) Vector128.LoadUnsafe(ref Unsafe.AsGuid, byte(ref Unsafe.AsRef(in right)));}// 如果不支持那么从Guid头部读取内存// 32位比较四次ref int rA ref Unsafe.AsRef(in left._a);ref int rB ref Unsafe.AsRef(in right._a);return rA rB Unsafe.Add(ref rA, 1) Unsafe.Add(ref rB, 1) Unsafe.Add(ref rA, 2) Unsafe.Add(ref rB, 2) Unsafe.Add(ref rA, 3) Unsafe.Add(ref rB, 3);
}在上面的代码中我们可以看到不仅提供了 Vector 加速的方案还有不支持回退的场景。不过那段 Vector 代码是不是不太好理解我们逐个部分来解析一下。我们首先看左右的部分右边也是同样的意思Vector128.LoadUnsafe(ref Unsafe.AsGuid, byte(ref Unsafe.AsRef(in left)))。ref Unsafe.AsRef(in left) 是获取 left Guid 它的首地址指针此时返回的其实是Guid*ref Unsafe.AsGuid, byte(...) 将Guid*指针转换为byte*指针Vector128.LoadUnsafe(...) 由于 Guid 已经变为 Byte 指针所以就能直接 LoadUnsafe 了最后 right Guid 也使用相同的方式加载最后使用比较两个Vector是否相等就好了。其实还使用了CompareEqual和MoveMask两个指令只是在.NET7 中 JIT 会把两个向量的比较给优化。看下方图片中红色框标记的部分就是这两个指令。那么.NET6 下没有优化那该怎么办呢根据这里的汇编指令Meziantou[1]大佬给出了.NET6 下同样功效的优化代码static class GuidExtensions
{public static bool OptimizedGuidEquals(in Guid left, in Guid right){if (Sse2.IsSupported){Vector128byte leftVector Unsafe.ReadUnalignedVector128byte(ref Unsafe.AsGuid, byte(ref Unsafe.AsRef(in left)));Vector128byte rightVector Unsafe.ReadUnalignedVector128byte(ref Unsafe.AsGuid, byte(ref Unsafe.AsRef(in right)));// 使用Sse2.CompareEqual()比较是否相等它的返回值是一个128位向量如果相等该位置返回0xffff否则返回0x0// CompareEqual的结果是128位的我们可以通过Sse2.MoveMask()来重新排列成16位最终看是否等于0xffff就好var equals Sse2.CompareEqual(leftVector, rightVector);var result Sse2.MoveMask(equals);return (result 0xFFFF) 0xFFFF;}return left right;}
}从下图的汇编代码中可以看到是一样的效果总结最终这一波操作下来我们可以看到Guid.Equals的性能提升了 30%。如果你的程序中使用 Guid 作为数据库、对象主键的只需要升级.NET7 或者用上面的GuidExtensions就能获得这样的性能提升。参考资料[1] Meziantou: https://www.meziantou.net/faster-guid-comparisons-using-vectors-simd-in-dotnet.htm