在.Net 中使用正则表达式注意事项
起因
在项目看到使用很多地方使用正则表达式,觉得使用方式不是很好,关于正则表达式曾有提到过 写更好的CSharp代码 和 .Net 5 正则表达式性能改进,后边这一篇没有翻译完,是因为涉及到正则表达式的底层原理和实现,加上本人英语水平有限(已经在提升英语单词的数量),所以只是翻译了一部分就暂停了,停了半年有余,这里只是通过基准测试,看看正则表达式在.Net几个版本中的性能,还有简单字符串替换(需要性能的地方)是没有必要使用正则表达式的.1. 正则表达式(没有实例缓存/有实例缓存)和通过字符串替换进行对比
using BenchmarkDotNet.Attributes;
using System.Text.RegularExpressions;
namespace dotnet_perf
{
[MemoryDiagnoser]
[DisassemblyDiagnoser(printSource: true)]
public class RegexTest1
{
[Params(4096)]
public int Count { get; set; }
public static string OrderNum = "FA210609";
[Benchmark(Baseline = true)]
public void Replace1()
{
for (int i = 0; i < Count; i++)
{
RegexReplace(OrderNum);
}
}
[Benchmark]
public void Replace2()
{
for (int i = 0; i < Count; i++)
{
Regex2Replace(OrderNum);
}
}
[Benchmark]
public void Replace3()
{
for (int i = 0; i < Count; i++)
{
ArrayPlace(OrderNum);
}
}
//Regex.Replace内部每次创建一个Regex实例
//https://referencesource.microsoft.com/#System/regex/system/text/regularexpressions/Regex.cs,f993512d88527623,references
public string RegexReplace(string input)
{
return Regex.Replace(input, @"(?<=[A-Z])\d(?=\d+$)", "R");
}
//这里没有使用RegexOptions.Compiled,是因为编译正则表达式是可以提高性能,编译后是生成dll,太多的dll会影响程序性能
//这里只是减少正则表达式的实例
public Regex regex = new Regex(@"(?<=[A-Z])\d(?=\d+$)", RegexOptions.IgnoreCase);
public string Regex2Replace(string input)
{
return regex.Replace(input, "R");
}
//使用字符数组
public string ArrayPlace(string input)
{
int len = input.Length;
int postion = -1;
char[] arr = new char[len];
for (int i = 0; i < len; i++)
{
if (input[i] >= 'A' && input[i] <= 'Z')
{
arr[i] = input[i];
}
else
{
postion = i;
break;
}
}
if (postion > 0)
{
arr[postion] = 'R';
}
int start = postion + 1;
for (int i = start; i < len; i++)
{
arr[i] = input[i];
}
return new string(arr, 0, len);
}
}
}
# benchmark.dotnet 多版本测试
dotnet run -c Release -f net48 --runtimes net48 netcoreapp31 net60 --filter *RegexText* --join
版本我们先从.Net Framework 4.8和.Net Core 3.1最后是.Net 6来说耗时/GC以及内存使用大小.
在.Net Framework 4.8中,Replace1(没有缓存实例)作为基准,分别和Replace2(缓存正则表达式实例)以及Replace3(字符串查找替换)对比:
- Replace1最耗时,内存使用量也是最大的,在0代GC是也是最多的.
- Replace2经过缓存正则表达式实例,耗时减少了8%,内存使用减少50%,0代GC也减少50%.
- Replace3通过分配字符数组,循环查找替换,耗时只是Replace1的4%,内存和0代GC也只是Replace1的12.3%左右.
在.Net Core 3.1中:
- 得助于.Net开源社区在底层改进和优化,Replace1相同的代码和.Net Framework 4.8在耗时上提升了31%,在内存使用和0代GC回收上提高了50%.
- Replace2缓存正则表达式实例,和Replace在耗时上没有提升,还有点下降,在内存使用和0代GC上还是提高了30%.
- Replace3和Replace1对比,耗时只是Replace1的4%,内存和0代GC也只是Replace1的22.7%左右.
得出结论: Replace2(正则表达式缓存)在.Net Core3.1中只是减少内存使用和降低0代GC回收频率,Replace3和Replace1在耗时上还是有23倍的提升,在内存使用和0代GC上变为了4.4倍左右.
在.Net 6中:
- 得助于.Net开源社区在.Net 5对正则表达式进行改进,Replace1相同的代码和.Net Core 3.1在耗时上提升了45%,在内存使用和0代GC回收上提高了将近9倍.
- Replace2缓存正则表达式实例,和Replace1基本没有区别,是因为在.Net 5中对正则表达式进行优化,加入缓存,所以在.Net5(包含)之后的版本,可以不用缓存正则表达式实例,.Net内部有缓存机制.
- Replace3和Replace2,在耗时上提升了37.7倍,但在内存使用和0代GC上竟然降低了1倍.
我们看一下Replace1在.Net 6中JIT生成汇编代码是否缓存存在:
; dotnet_perf.RegexText.Replace1()
push r14
push rdi
push rsi
push rbp
push rbx
sub rsp,20
mov rsi,rcx
; for (int i = 0; i < Count; i++)
; ^^^^^^^^^
xor edi,edi
cmp dword ptr [rsi+10],0
jle short M00_L01
mov rcx,7FF83B9BDE88
mov edx,4
call CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE
mov rcx,24735FA9638
mov rbx,[rcx]
mov rcx,24735FAAAF8
mov rbp,[rcx]
; RegexReplace(OrderNum);
; ^^^^^^^^^^^^^^^^^^^^^^^
M00_L00:
mov rcx,24735FA2E48
mov r14,[rcx]
mov rcx,rbx
call System.Text.RegularExpressions.RegexCache.GetOrAdd(System.String)
mov rcx,rax
mov r8,rbp
mov rdx,r14
cmp [rcx],ecx
call System.Text.RegularExpressions.Regex.Replace(System.String, System.String)
inc edi
cmp edi,[rsi+10]
jl short M00_L00
M00_L01:
add rsp,20
pop rbx
pop rbp
pop rsi
pop rdi
pop r14
ret
; Total bytes of code 122
果然是有缓存的存在,具体分别在:
结论
Replace1在.Net 6和.Net Framework4.8代码没有改变情况下,耗时性能提高2.6倍,内存使用降低了17倍,0代GC降低了18倍.
在需要性能的时候,还是去实现特定的算法,去针对性的优化.如Replace3对比Replace1在耗时性能提高99倍,内存使用和0代GC提高8倍多.
这里测试主要针对正则表达式,其他地方相差可能没有那么大,但.Net 5/.Net 6在性能优化的道路上越走越远.
- 现有项目有性能瓶颈,且不想添硬件的情况
- 没有依赖系统特性的,比如说组件依赖Windows
- 需要跨平台,如Linux平台,可以先用Mono在Linux运行起来,后面分阶段迁移为.Net 5或.Net 6
秋风
2021-06-19