.NET7是如何优化Guid.Equals性能的?
简介
在之前的文章中,我们屡次提到 Vector - SIMD 手艺,也容许各人在后面分享更多.NET7 中优化的例子,今天就带来一个利用 SIMD 优化 Guid.Equals 办法性能的例子。
为什么 Guid 能利用 SIMD 优化?
起首就需要介绍一些布景常识,那就是 Guid 它是什么,在我们人类眼中, Guid 就是一串字符串,如下方所示的那样。
"D313CD46-2724-7359-84A0-9E73C861CCD2"
而在定义中,全局独一标识符(GUID,Globally Unique Identifier)是一种由算法生成的二进造长度为 128 位的数字标识符。GUID 次要用于在拥有多个节点、多台计算机的收集或系统中。在抱负情状下,任何计算机和计算机集群都不会生成两个不异的 GUID。GUID 的总数到达了 2^128(3.4×10^38)个,所以随机生成两个不异 GUID 的可能性十分小,但其实不为 0。GUID 一词有时也专指微软对 UUID 原则的实现。
各人能够看到我着重标识表记标帜了它的位数是 128 位,128 位意味着什么?就是若是比力两个 Guid 能否相等的话,不论是 64 位 CPU 仍是 32 位的 CPU 需要多条指令比力屡次。若是我们用上了 Vector?是不是会有更好的性能呢?
起首我们来看看 Guid 是若何定义的,看看能不克不及间接读取 128 位数据,从而用上 Vector。Guid 它是值类型的,是一个构造体。代码如下所示,我省略了部门信息。
publicreadonlypartialstructGuid
privatereadonlyint_a; // Do not rename (binary serialization)
privatereadonlyshort_b; // Do not rename (binary serialization)
privatereadonlyshort_c; // Do not rename (binary serialization)
privatereadonlybyte_d; // Do not rename (binary serialization)
privatereadonlybyte_e; // Do not rename (binary serialization)
privatereadonlybyte_f; // Do not rename (binary serialization)
展开全文
privatereadonlybyte_g; // Do not rename (binary serialization)
privatereadonlybyte_h; // Do not rename (binary serialization)
privatereadonlybyte_i; // Do not rename (binary serialization)
privatereadonlybyte_j; // Do not rename (binary serialization)
privatereadonlybyte_k; // Do not rename (binary serialization)
能够看到它由 1 个 32 位 int,2 个 16 位的 short 和 8 个 8 位的 byte 构成,至于为什么需要如许构成,其实是一个原则化的工具,为了在生成和序列化时更快。
我们利用 ObjectLayoutInspector 能够打印出 Guid 的数据构造,数据成果如下图所示,和我们源码里面看到的一致:
那么 Guid 能否能利用 SIMD 优化的结论显而易见:
Guid 有 128 位,如今 CPU 都是 64 位或者 32 位,还存在提拔空间
Guid 是构造体类型,构造体类型在内存中是持续存储,我们能够间接读取内存来拜候整个构造体
Guid 有 128 位,如今 CPU 都是 64 位或者 32 位,还存在提拔空间
Guid 是构造体类型,构造体类型在内存中是持续存储,我们能够间接读取内存来拜候整个构造体
根据我们前面文章中,Min 和 Max 办法在.NET7 被优化的经历,我们能够间接写下面如许的代码。
[ MethodImpl(MethodImplOptions.AggressiveInlining)]
privatestaticboolEqualsCore( inGuid left, inGuid right )
// 检测硬件能否撑持Vector128
if(Vector128.IsHardwareAccelerated)
// 撑持Vector128就好办了,间接加载比力
returnVector128.LoadUnsafe( refUnsafe.AsGuid, byte( refUnsafe.AsRef( inleft))) == Vector128.LoadUnsafe( refUnsafe.AsGuid, byte( refUnsafe.AsRef( inright)));
// 若是不撑持,那么从Guid头部读取内存
// 32位比力四次
refintrA = refUnsafe.AsRef( inleft._a);
refintrB = refUnsafe.AsRef( inright._a);
returnrA == rB
Unsafe.Add( refrA, 1) == Unsafe.Add( refrB, 1)
Unsafe.Add( refrA, 2) == Unsafe.Add( refrB, 2)
Unsafe.Add( refrA, 3) == Unsafe.Add( refrB, 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 了
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 下同样成效的优化代码:
staticclassGuidExtensions
publicstaticboolOptimizedGuidEquals( inGuid left, inGuid right )
if(Sse2.IsSupported)
Vector128 byte leftVector = Unsafe.ReadUnalignedVector128 byte(
refUnsafe.AsGuid, byte(
refUnsafe.AsRef( inleft)));
Vector128 byte rightVector = Unsafe.ReadUnalignedVector128 byte(
refUnsafe.AsGuid, byte(
refUnsafe.AsRef( inright)));
// 利用Sse2.CompareEqual比力能否相等,它的返回值是一个128位向量,若是相等,该位置返回0xffff,不然返回0x0
// CompareEqual的成果是128位的,我们能够通过Sse2.MoveMask来从头摆列成16位,最末看能否等于0xffff就好
varequals= Sse2.CompareEqual(leftVector, rightVector);
varresult = Sse2.MoveMask( equals);
return(result 0xFFFF) == 0xFFFF;
returnleft == right;
从下图的汇编代码中,能够看到是一样的效果:
总结
最末那一波操做下来,我们能够看到 Guid.Equals 的性能提拔了 30%。若是你的法式中利用 Guid 做为数据库、对象主键的,只需要晋级.NET7 或者用上面的 GuidExtensions 就能获得如许的性能提拔。
参考材料
[1]
Meziantou: