首先贴原始代码:
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.'");
}
}
首先是重复代码过多的问题,可以使用switch
的case
中允许使用的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的压力,并且栈上分配得到的数组由于是直接访问,要比堆上分配采用间接寻址的方法效率要更高;唯一的问题是数组大小的问题,long
和ulong
都是64位,对应的数组大小是8
,而int
和uint
都是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
由于int
,uint
,long
,ulong
没有一个公共父类而被推导为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的压力,在运行时可以提供更好的性能。
Comments NOTHING