.NET7是如何优化Guid.Equals性能的?

2022-11-14 16:34:42 浏览数 (2)

简介

在之前的文章中,我们多次提到 Vector - SIMD 技术,也答应大家在后面分享更多.NET7 中优化的例子,今天就带来一个使用 SIMD 优化Guid.Equals()方法性能的例子。

为什么 Guid 能使用 SIMD 优化?

首先就需要介绍一些背景知识,那就是Guid它是什么,在我们人类眼中,Guid就是一串字符串,如下方所示的那样。

代码语言:javascript复制
"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 它是值类型的,是一个结构体。代码如下所示,我省略了部分信息。

代码语言:javascript复制
    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 位 int,2 个 16 位的 short 和 8 个 8 位的 byte 组成,至于为什么需要这样组成,其实是一个标准化的东西,为了在生成和序列化时更快。

我们使用ObjectLayoutInspector可以打印出 Guid 的数据结构,数据结果如下图所示,和我们源码里面看到的一致:

那么 Guid 是否能使用 SIMD 优化的结论显而易见:

  • Guid 有 128 位,现在 CPU 都是 64 位或者 32 位,还存在提升空间
  • Guid 是结构体类型,结构体类型在内存中是连续存储,我们可以直接读取内存来访问整个结构体

SIMD 优化代码

根据我们前面文章中,Min 和 Max 方法在.NET7 被优化的经验,我们可以直接写下面这样的代码。

代码语言:javascript复制
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool EqualsCore(in Guid left, in Guid right)
{
    // 检测硬件是否支持Vector128
    if (Vector128.IsHardwareAccelerated)
    {
        // 支持Vector128就好办了,直接加载比较
        return Vector128.LoadUnsafe(ref Unsafe.As<Guid, byte>(ref Unsafe.AsRef(in left))) == Vector128.LoadUnsafe(ref Unsafe.As<Guid, 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.As<Guid, byte>(ref Unsafe.AsRef(in left)))

  • ref Unsafe.AsRef(in left) 是获取 left Guid 它的首地址指针,此时返回的其实是Guid*
  • ref Unsafe.As<Guid, byte>(...)Guid*指针转换为byte*指针
  • Vector128.LoadUnsafe(...) 由于 Guid 已经变为 Byte 指针,所以就能直接 LoadUnsafe 了

最后 right Guid 也使用相同的方式加载,最后使用==比较两个Vector是否相等就好了。其实==还使用了CompareEqualMoveMask两个指令,只是在.NET7 中 JIT 会把两个向量的比较给优化。看下方图片中红色框标记的部分,就是这两个指令。

那么.NET6 下==没有优化,那该怎么办呢?根据这里的汇编指令,Meziantou[1]大佬给出了.NET6 下同样功效的优化代码:

代码语言:javascript复制
static class GuidExtensions
{
    public static bool OptimizedGuidEquals(in Guid left, in Guid right)
    {
        if (Sse2.IsSupported)
        {
            Vector128<byte> leftVector = Unsafe.ReadUnaligned<Vector128<byte>>(
                ref Unsafe.As<Guid, byte>(
                    ref Unsafe.AsRef(in left)));

            Vector128<byte> rightVector = Unsafe.ReadUnaligned<Vector128<byte>>(
                ref Unsafe.As<Guid, 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

0 人点赞