查看原文
其他

.NET Core 最优 MD5 打开方式

DotNet 2021-09-23

(给DotNet加星标,提升.Net技能


转自:陈鑫伟
cnblogs.com/Dogwei/p/11342023.html
public static string GetMd5Hash(string input)
{
using (MD5 md5Hash = MD5.Create())
{
// Convert the input string to a byte array and compute the hash.
byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));

// Create a new Stringbuilder to collect the bytes
// and create a string.
StringBuilder sBuilder = new StringBuilder();

// Loop through each byte of the hashed data
// and format each one as a hexadecimal string.
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}

// Return the hexadecimal string.
return sBuilder.ToString();
}
}


这是一段 MSDN 官方的 MD5 示例,例子很简单且很容易理解。但是,这个例子也有很多的问题,首先上例至少创建了 3 个临时缓存区!且每次执行 GetMd5Hash 都会创建一个 MD5 实例,并在方法执行完成后释放它。这些都造成了很大的系统资源浪费和增加了 GC 的压力。


鉴于官方给的 Demo 并不优秀,且网上也没有给出很好使用方式,这里我就拿出我多年使用的MD5打开方式,这个方法同时支持 SHA1,SHA256 等,


即支持 System.Security.Cryptography 命名空间下的 HashAlgorithm(哈希算法) 实现。也同时支持 .Net Framework 2.0 之后的所有 .Net 平台。 


我不想看你的鬼废话,直接给我上最终代码


先说明,这个文章是基于 System.Security.Cryptography 命名空间的实现,不是自己写一个MD5算法哦。


现在我们开始,首先我们先定义一个辅助类:


using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;

static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm
{
/// <summary>
/// 线程静态变量。
/// 即:这个变量在每个线程中都是唯一的。
/// 再结合泛型类实现:该变量在不同泛型或不同的线程下的值都是不一样的。
/// 这样做的目的是为了避开多线程问题。
/// 关于垃圾回收:当 .NET 线程被释放时,程序中的所有线程静态变量都会被回收,GC 回收时同时将释放资源,所以不必担心释放问题,GC 会帮助我们的。
/// 这里描述的 .NET 线程释放不是指 .NET 线程回收至线程池。很多时候 .NET 的线程在程序关闭之前都不会真正释放,而是在线程池中继续驻留。
/// 线程唯一真的能避免多线程问题吗?答:多个线程所用存储空间都不一样,那么脏值就不可能存在,如果这都能出现多线程问题,我直播吃....猪红(本人极其厌恶吃猪红🌚)。
/// </summary>h
[ThreadStatic]
static THashAlgorithm instance;

public static THashAlgorithm Instance => instance ?? Create(); // C# 语法糖,低版本可以改为 { get { return instance != null ? instance : Create(); } }
/// <summary>
/// 寻找 THashAlgorithm 类型下的 Create 静态方法,并执行它。
/// 如果没找到,则执行 Activator.CreateInstance 调用构造方法创建实例。
/// 如果 Activator.CreateInstance 方法执行失败,它会抛出异常。
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
static THashAlgorithm Create()
{
var createMethod = typeof(THashAlgorithm).GetMethod(
nameof(HashAlgorithm.Create), // 这段代码同 "Create",低版本 C# 可以替换掉
BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly,
Type.DefaultBinder,
Type.EmptyTypes,
null);
if (createMethod != null)
{
instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { });
}
else
{
instance = Activator.CreateInstance<THashAlgorithm>();
}
return instance;
}
}


该辅助类帮助我们避开多线程问题,且帮助我们创建指定的 HashAlgorithm 实例。


这里说明一下,HashAlgorithm.ComputeHash (同 MD5.ComputeHash) 方法绝对不是线程安全的!


大家使用它的时候必须要注意,在未线程同步下调用同一实例的 ComputeHash 方法得到的结果是错误的!


关于线程唯一和泛型唯一:


还记得老师教我们的时候强调静态变量就是唯一的,可是现在就突然出现了两个反例,与之对立,这让初学者一下子难以接受,其实这也很容易理解的:


  • 首先 [ThreadStatic] 特性我们可以理解为将字段封装为了 ThreadLocal<T>,它在内部区分线程,然后返回不同的值。


  • 然后泛型唯一,举例:我们都知道 List<int> 和 List<string> 它们不是一个类型!那么它们的字段 List<int>.thread_field 和 List<string>.thread_field 也同理不是一个字段,那么它们的值当然也不是同一个啦。


接下来我们再定义实现类:

public static class HashAlgorithmHelper
{
public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm
{
var data = THashAlgorithmInstances<THashAlgorithm>.Instance.ComputeHash(Encoding.UTF8.GetBytes(input));
var sBuilder = new StringBuilder();
foreach (var item in data)
{
sBuilder.Append(item.ToString("x2"));
}
return sBuilder.ToString();
}
}


到这里我们入门级的 MD5 打开方式就完成了,使用方法:HashAlgorithmHelper.ComputeHash<MD5>("Hello World!")。


我们来先测试一下:


static void Main(string[] args)
{
Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"));
Console.WriteLine(GetMd5Hash("Hello World!"));

while (true)
{
var stopwatch = Stopwatch.StartNew();

for (int i = 0; i < 1000000; i++)
{
HashAlgorithmHelper.ComputeHash<MD5>("Hello World!");
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

stopwatch = Stopwatch.StartNew();

for (int i = 0; i < 1000000; i++)
{
GetMd5Hash("Hello World!");
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);
}
}


输出结果:



可以看出我们的性能已经超官方 Demo 近一倍了。


接下来我们将进入进阶级打开方式,我们现在需要自己写一个简单的 byte[] To string 方法,我们先打开 C# 项目的 “允许不安全代码” 选项。


在解决方案中右键项目->属性->生成->勾选“允许不安全代码”。


然后我们在 HashAlgorithmHelper 类中定义新的 ToString 方法。


static string ToString(byte[] bytes)
{
unsafe
{
const int byte_len = 2; // 表示一个 byte 的字符长度。
var str = new string('\0', byte_len * bytes.Length); // 创建一个指定长度的空字符串。
fixed(char* pStr = str)
{
var pStr2 = pStr; // fixed pStr 是只读的,所以我们定义一个变量。
foreach (var item in bytes)
{
*pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2;
*pStr2 = Digitals[item & 15/* byte low */]; ++pStr2;
}
}
return str;
}
}


然后我们修改 ComputeHash 方法为如下:


public static string ComputeHash<THashAlgorithm>
(string input) where THashAlgorithm : HashAlgorithm
{
var bytes = Encoding.UTF8.GetBytes(input);
var data = THashAlgorithmInstances<THashAlgorithm>
.Instance.ComputeHash(bytes);
return ToString(data);
}


现在我们再测试就会发现已经比官方 Demo 快 4 倍了!现在这个 MD5 打开方式已经适合绝大多数人了,如果您不喜欢不安全代码,也可以用数组代替,效率只差一丢丢而已,该方式我会在下方给出完整代码。


接下来我们使用 .Net Core 以最优的方式打开,我们修改 HashAlgorithmHelper 为如下:(这里就不再支持 .Net Framework 了)


public static class HashAlgorithmHelper
{
static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
// 在这个函数里要用到的 bytes 成员 ReadOnlySpan<byte> 与 byte[] 的一致,所以我们只需要修改参数类型即可。
static string ToString(ReadOnlySpan<byte> bytes)
{
unsafe
{
const int byte_len = 2; // 表示一个 byte 的字符长度。
var str = new string('\0', byte_len * bytes.Length);
fixed(char* pStr = str)
{
var pStr2 = pStr; // fixed pStr 是只读的,所以我们定义一个变量。
foreach (var item in bytes)
{
*pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2;
*pStr2 = Digitals[item & 15/* byte low */]; ++pStr2;
}
}
return str;
}
}
public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm
{
var instance = THashAlgorithmInstances<THashAlgorithm>.Instance; // 避免二次取值,微微提高效率(自我感觉)。
var encoding = Encoding.UTF8;
// 我们在这里声明一个足量的 byte 数组,足以容下字符串的 utf-8 字节码和 hash 值的字节码。
var bytes = new byte[Encoding.UTF8.GetMaxByteCount(Math.Max(input.Length, instance.HashSize / 2))];
var bytesCount = encoding.GetBytes(input, bytes);
var source = new ReadOnlySpan<byte>(bytes, 0, bytesCount); // source: utf-8 bytes region.
var destination = new Span<byte>(bytes, bytesCount, bytes.Length - bytesCount); // destination: buffer region.
if (bytes.Length - bytesCount > instance.HashSize && instance.TryComputeHash(source, destination, out var bytesWritten))
{
return ToString(destination.Slice(0, bytesWritten));
}
else
{
// 通常情况下这里就很有可能抛出异常了,但是我们封装工具方法必须有一个原则,我们尽量不要自行抛出异常。
// 用户的参数执行到这里我们依然调用 HashAlgorithm.ComputeHash,由它内部抛出异常。这样可以避免很多问题和歧义。
return ToString(instance.ComputeHash(bytes, 0, bytesCount));
}
}
}


我们再次测试,结果如下:



现在我们已经超官方示例达 5 倍了!这就是最终版本了。


最后附上各个版本实现的完整代码:


1、Core 2.1+ 包含不安全代码版本


using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;

static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm
{
/// <summary>
/// 线程静态变量。
/// 即:这个变量在每个线程中都是唯一的。
/// 再结合泛型类实现了该变量在不同泛型或不同的线程先的变量都是唯一的。
/// 这样做的目的是为了避开多线程问题。
/// </summary>
[ThreadStatic]

static THashAlgorithm instance;
public static THashAlgorithm Instance => instance ?? Create(); // C# 语法糖,低版本可以改为 { get { return instance != null ? instance : Create(); } }

/// <summary>
/// 寻找 THashAlgorithm 类型下的 Create 静态方法,并执行它。
/// 如果没找到,则执行 Activator.CreateInstance 调用构造方法创建实例。
/// 如果 Activator.CreateInstance 方法执行失败,它会抛出异常。
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
static THashAlgorithm Create()
{
var createMethod = typeof(THashAlgorithm).GetMethod(
nameof(HashAlgorithm.Create), // 这段代码同 "Create",低版本 C# 可以替换掉
BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly,
Type.DefaultBinder,
Type.EmptyTypes,
null);
if (createMethod != null)
{
instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { });

}
else
{
instance = Activator.CreateInstance<THashAlgorithm>();
}
return instance;
}
}


public static class HashAlgorithmHelper
{
static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
static string ToString(ReadOnlySpan<byte> bytes)
{
unsafe
{
const int byte_len = 2; // 表示一个 byte 的字符长度。
var str = new string('\0', byte_len * bytes.Length);
fixed(char* pStr = str)
{
var pStr2 = pStr; // fixed pStr 是只读的,所以我们定义一个变量。
foreach (var item in bytes)
{
*pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2;
*pStr2 = Digitals[item & 15/* byte low */]; ++pStr2;
}
}
return str;
}
}

public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm
{
var instance = THashAlgorithmInstances<THashAlgorithm>.Instance;

var encoding = Encoding.UTF8;
var bytes = new byte[Encoding.UTF8.GetMaxByteCount(Math.Max(input.Length, instance.HashSize / 2))];
var bytesCount = encoding.GetBytes(input, bytes);
var source = new ReadOnlySpan<byte>(bytes, 0, bytesCount); // source: utf-8 bytes region.
var destination = new Span<byte>(bytes, bytesCount, bytes.Length - bytesCount); // destination: buffer region.
if (bytes.Length - bytesCount > instance.HashSize && instance.TryComputeHash(source, destination, out var bytesWritten))
{
return ToString(destination.Slice(0, bytesWritten));
}
else
{
return ToString(instance.ComputeHash(bytes, 0, bytesCount));
}
}
}

class Program
{
public static string GetMd5Hash(string input)
{
using (MD5 md5Hash = MD5.Create())
{
// Convert the input string to a byte array and compute the hash.
byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));
// Create a new Stringbuilder to collect the bytes
// and create a string.
StringBuilder sBuilder = new StringBuilder();
// Loop through each byte of the hashed data
// and format each one as a hexadecimal string.
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
// Return the hexadecimal string.
return sBuilder.ToString();
}
}
static void Main(string[] args)
{
Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"));
Console.WriteLine(GetMd5Hash("Hello World!"));
while (true)
{
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
HashAlgorithmHelper.ComputeHash<MD5>("Hello World!");
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
GetMd5Hash("Hello World!");
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
}
}
}


2、包含不安全代码的通用版本


using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;

static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm
{
/// <summary>
/// 线程静态变量。
/// 即:这个变量在每个线程中都是唯一的。
/// 再结合泛型类实现了该变量在不同泛型或不同的线程先的变量都是唯一的。
/// 这样做的目的是为了避开多线程问题。
/// </summary>
[ThreadStatic]
static THashAlgorithm instance;

public static THashAlgorithm Instance => instance ?? Create(); // C# 语法糖,低版本可以改为 { get { return instance != null ? instance : Create(); } }

/// <summary>
/// 寻找 THashAlgorithm 类型下的 Create 静态方法,并执行它。
/// 如果没找到,则执行 Activator.CreateInstance 调用构造方法创建实例。
/// 如果 Activator.CreateInstance 方法执行失败,它会抛出异常。
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
static THashAlgorithm Create()
{
var createMethod = typeof(THashAlgorithm).GetMethod(
nameof(HashAlgorithm.Create), // 这段代码同 "Create",低版本 C# 可以替换掉
BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly,
Type.DefaultBinder,
Type.EmptyTypes,
null);
if (createMethod != null)
{
instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { });
}
else
{
instance = Activator.CreateInstance<THashAlgorithm>();
}
return instance;
}
}

public static class HashAlgorithmHelper
{
static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
static string ToString(byte[] bytes)
{
unsafe
{
const int byte_len = 2; // 表示一个 byte 的字符长度。
var str = new string('\0', byte_len * bytes.Length);
fixed(char* pStr = str)
{
var pStr2 = pStr; // fixed pStr 是只读的,所以我们定义一个变量。
foreach (var item in bytes)
{
*pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2;
*pStr2 = Digitals[item & 15/* byte low */]; ++pStr2;
}
}
return str;
}
}
public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm
{
var bytes = Encoding.UTF8.GetBytes(input);
return ToString(THashAlgorithmInstances<THashAlgorithm>.Instance.ComputeHash(bytes));
}
}

class Program
{
public static string GetMd5Hash(string input)
{
using (MD5 md5Hash = MD5.Create())
{
// Convert the input string to a byte array and compute the hash.
byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));
// Create a new Stringbuilder to collect the bytes
// and create a string.
StringBuilder sBuilder = new StringBuilder();

// Loop through each byte of the hashed data
// and format each one as a hexadecimal string.
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
// Return the hexadecimal string.
return sBuilder.ToString();
}
}
static void Main(string[] args)
{
Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"));
Console.WriteLine(GetMd5Hash("Hello World!"));
while (true)
{
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
HashAlgorithmHelper.ComputeHash<MD5>("Hello World!");
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
GetMd5Hash("Hello World!");
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
}
}
}


3、不包含不安全代码的通用版本


using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;

static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm
{
/// <summary>
/// 线程静态变量。
/// 即:这个变量在每个线程中都是唯一的。
/// 再结合泛型类实现了该变量在不同泛型或不同的线程先的变量都是唯一的。
/// 这样做的目的是为了避开多线程问题。
/// </summary>
[ThreadStatic]
static THashAlgorithm instance;

public static THashAlgorithm Instance => instance ?? Create(); // C# 语法糖,低版本可以改为 { get { return instance != null ? instance : Create(); } }

/// <summary>
/// 寻找 THashAlgorithm 类型下的 Create 静态方法,并执行它。
/// 如果没找到,则执行 Activator.CreateInstance 调用构造方法创建实例。
/// 如果 Activator.CreateInstance 方法执行失败,它会抛出异常。
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
static THashAlgorithm Create()
{
var createMethod = typeof(THashAlgorithm).GetMethod(
nameof(HashAlgorithm.Create), // 这段代码同 "Create",低版本 C# 可以替换掉
BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly,
Type.DefaultBinder,
Type.EmptyTypes,
null);
if (createMethod != null)
{
instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { });

}
else
{
instance = Activator.CreateInstance<THashAlgorithm>();
}
return instance;
}
}

public static class HashAlgorithmHelper
{
static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
static string ToString(byte[] bytes)
{
const int byte_len = 2; // 表示一个 byte 的字符长度。
var chars = new char[byte_len * bytes.Length];
var index = 0;
foreach (var item in bytes)
{
chars[index] = Digitals[item >> 4/* byte high */]; ++index;
chars[index] = Digitals[item & 15/* byte low */]; ++index;
}
return new string(chars);
}
public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm
{
var bytes = Encoding.UTF8.GetBytes(input);
return ToString(THashAlgorithmInstances<THashAlgorithm>.Instance.ComputeHash(bytes));
}
}
class Program
{
public static string GetMd5Hash(string input)
{
using (MD5 md5Hash = MD5.Create())
{
// Convert the input string to a byte array and compute the hash.
byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));
// Create a new Stringbuilder to collect the bytes
// and create a string.
StringBuilder sBuilder = new StringBuilder();
// Loop through each byte of the hashed data
// and format each one as a hexadecimal string.
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
// Return the hexadecimal string.
return sBuilder.ToString();
}
}
static void Main(string[] args)
{
Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"));
Console.WriteLine(GetMd5Hash("Hello World!"));
while (true)
{
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
HashAlgorithmHelper.ComputeHash<MD5>("Hello World!");
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
GetMd5Hash("Hello World!");
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
}
}
}


不包含不安全代码通用版本的性能:(性能依然极佳,建议使用此版本)



注:测试结果仅来自个人电脑,不同平台或硬件可能会有差异!


推荐阅读

(点击标题可跳转阅读)

.NET World—gPRC概览

微服务引擎Surging 2.0的链路跟踪和新增功能

.NET中SignalR自托管全解(使用Self-Host)


看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能 

好文章,我在看❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存