首先贴原始代码:

private Guid HashToGuid(object input)
{
    switch (input)
    {
        case string str:
            return new Guid(_md5.ComputeHash(Encoding.UTF8.GetBytes(str)));
        case int i32:
            byte[] buf1 = new byte[4];
            MemoryMarshal.Write(buf1, ref i32);
            return new Guid(_md5.ComputeHash(buf1));
        case uint ui32:
            byte[] buf2 = new byte[4];
            MemoryMarshal.Write(buf2, ref ui32);
            return new Guid(_md5.ComputeHash(buf2));
        case long i64:
            byte[] buf3 = new byte[8];
            MemoryMarshal.Write(buf3, ref i64);
            return new Guid(_md5.ComputeHash(buf3));
        case ulong ui64:
            byte[] buf4 = new byte[8];
            MemoryMarshal.Write(buf4, ref ui64);
            return new Guid(_md5.ComputeHash(buf4));
         case byte[] bytes:
            return new Guid(_md5.ComputeHash(bytes));
         default:
            throw new ArgumentException($"The input type '{input.GetType()} is not supported.'");
    }
}

首先是重复代码过多的问题,可以使用switchcase中允许使用的disjunction pattern把四个case集成为同一个:

private Guid HashToGuid(object input)
{
    switch (input)
    {
        case string str:
            return new Guid(_md5.ComputeHash(Encoding.UTF8.GetBytes(str)));
        case var number and (int or uint or long or ulong):
            // what should we write here?
            break;
        case byte[] bytes:
            return new Guid(_md5.ComputeHash(bytes));
        default:
            throw new ArgumentException($"The input type '{input.GetType()} is not supported.'");
    }
}

现在问题来了,在中间注释掉的部分要如何填充进原先的逻辑。
首先观察原先逻辑的代码,都是通过创建一个byte数组,同时数组的大小是对应的数据类型的大小,因此可以自然而然的想到使用ArrayPool<byte>或者IMemoryOwner<byte>来池化数组以在该函数可能被极为频繁的大量调用时减轻GC压力,但是观察到此处的byte数组大小非常小,要么是8要么是4,因此可以用stackalloc进行栈上分配来完全释放GC的压力,并且栈上分配得到的数组由于是直接访问,要比堆上分配采用间接寻址的方法效率要更高;唯一的问题是数组大小的问题,longulong都是64位,对应的数组大小是8,而intuint都是32位,对应的数组大小是4,最显而易见的方法当然是判断number的类型,并且根据对应的类型来获取数组大小,然而这么做要么就要写一大堆if条件,要么就又要四个case子句,而这两种情况都是我们想尽力避免的。该问题的解决办法是使用Marshal.SizeOf(object)来获取对象的大小:

private Guid HashToGuid(object input)
{
    switch (input)
    {
        case string str:
            return new Guid(_md5.ComputeHash(Encoding.UTF8.GetBytes(str)));
        case var number and (int or uint or long or ulong):
            Span<byte> span = stackalloc byte[Marshal.SizeOf(number)];
            // what should we write here?
            break;
        case byte[] bytes:
            return new Guid(_md5.ComputeHash(bytes));
        default:
            throw new ArgumentException($"The input type '{input.GetType()} is not supported.'");
    }
}

现在出现了新的问题:之前用到的MemoryMarshal.Write<T>(byte[], ref T)在这里不能使用了,因为该方法要求泛型参数T满足约束struct(详见Constraints on Type Parameters - MSDN),而我们的number由于intuintlongulong没有一个公共父类而被推导为object类型,因此不满足struct约束,此时需要查看一下MemoryMarshal.Write<T>(byte[], ref T)的实现:

/// <summary>
/// Writes a structure of type T into a span of bytes.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Write<T>(Span<byte> destination, ref T value)
    where T : struct
{
    if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
    {
        ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));
    }
    if ((uint) Unsafe.SizeOf<T>() > (uint) destination.Length)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length);
    }
    Unsafe.WriteUnaligned<T>(ref GetReference(destination), value);
}

其中两个if第一个检测T是否是引用类型或者包含引用类型的数组,第二个检测destination的大小是否足以容纳一个T类型的对象,重点在于最后一句,
Unsafe.WriteUnaligned<T>(ref GetReference(destination), value),其中MemoryMarshal.GetReference<T>(Span<T>)方法返回Span<T>中第一个元素的引用,而Unsafe.WriteUnaligned<T>(ref byte, T)则会将一个T类型的对象写入到ref byte所代表的内存位置(不考虑字段对齐),重点在于,Unsafe.WriteUnaligned<T>(ref byte, T)函数的泛型参数T上是没有任何约束的,也就是我们可以合法的在该函数上使用object,鉴于我们已经知道number的类型一定是上面提到过的四者之一,而这四者都一定满足MemoryMarshal.Write<T>(byte[], ref T)的preconditions,因此我们可以放心的提取出最后一句,再加上返回值,完成了对该函数的优化:

private Guid HashToGuid(object input)
{
    switch (input)
    {
        case string str:
            return new Guid(_md5.ComputeHash(Encoding.UTF8.GetBytes(str)));
        case var number and (int or uint or long or ulong):
            Span<byte> span = stackalloc byte[Marshal.SizeOf(number)];
            Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(span), number);
            return new Guid(_md5.ComputeHash(span));
        case byte[] bytes:
            return new Guid(_md5.ComputeHash(bytes));
        default:
            throw new ArgumentException($"The input type '{input.GetType()} is not supported.'");
    }
}

修改过的函数相比起最初的版本,除了在代码上更简洁美观以外,更重要的是在需要被大量调用的场景里通过stackalloc减少了小对象的频繁创建,减少了GC在Gen 0的压力,在运行时可以提供更好的性能。


行成于思,毁于随