前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >.NET7是如何优化Guid.Equals性能的?

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

作者头像
InCerry
发布2022-11-14 16:34:42
2800
发布2022-11-14 16:34:42
举报
文章被收录于专栏:InCerryInCerry

简介

在之前的文章中,我们多次提到 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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 InCerry 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 为什么 Guid 能使用 SIMD 优化?
  • SIMD 优化代码
  • 总结
    • 参考资料
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档