前言
我们上一篇讲了一下枚举类型的优化,本篇来看下DateTime也就是时间格式的优化。
概述
DateTime 和 DateTimeOffset 为例。dotnet/runtime#84963 改进了 DateTime{Offset} 格式化的各种方面:
- 格式化逻辑具有用作回退的一般支持,并支持任何自定义格式,但也有用于最流行格式的专用例程,允许对其进行优化和调整。对于非常流行的“r”(RFC1123模式)和“o”(往返日期/时间模式)格式,已经存在专用例程;此 PR 在与固定区域性一起使用时为默认格式 (“G”)、“s”格式(可排序日期/时间模式)和“u”格式(通用可排序日期/时间模式)添加了专用例程,所有这些格式在各种域中都经常使用。
- 对于“U”格式(通用的完整日期/时间模式),实现最终将始终分配新实例和实例,从而导致大量分配,即使仅在极少数回退情况下才需要它。这修复了它,只在真正需要时才分配。DateTimeFormatInfoGregorianCalendar
- 当没有专用的格式化例程时,格式化将完成到一个内部调用中,该调用从提供的 span 缓冲区(通常从 )开始,然后根据需要随内存一起增长。格式设置完成后,该生成器将复制到目标范围或新字符串中,具体取决于触发格式设置的方法。但是,如果我们只为构建器提供目标范围的种子,则可以避免目标范围的复制。然后,如果构建器在格式化完成时仍然包含初始跨度(没有从中生长出来),我们知道所有数据都适合,我们可以跳过复制,因为所有数据都已经存在。ref structValueListBuilder<T>stackallocArrayPool
以下示例展现出一些影响:
// dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Globalization;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
[MemoryDiagnoser(displayGenColumns: false)]
public class Tests
{
private readonly DateTime _dt = new DateTime(2023, 9, 1, 12, 34, 56);
private readonly char[] _chars = new char[100];
[Params(null, "s", "u", "U", "G")]
public string Format { get; set; }
[Benchmark] public string DT_ToString() => _dt.ToString(Format);
[Benchmark] public string DT_ToStringInvariant() => _dt.ToString(Format, CultureInfo.InvariantCulture);
[Benchmark] public bool DT_TryFormat() => _dt.TryFormat(_chars, out _, Format);
[Benchmark] public bool DT_TryFormatInvariant() => _dt.TryFormat(_chars, out _, Format, CultureInfo.InvariantCulture);
}
性能测试如下:
Method |
Runtime |
Format |
Mean |
Ratio |
Allocated |
Alloc Ratio |
DT_ToString |
.NET 7.0 |
? |
166.64 ns |
1.00 |
64 B |
1.00 |
DT_ToString |
.NET 8.0 |
? |
102.45 ns |
0.62 |
64 B |
1.00 |
DT_ToStringInvariant |
.NET 7.0 |
? |
161.94 ns |
1.00 |
64 B |
1.00 |
DT_ToStringInvariant |
.NET 8.0 |
? |
28.74 ns |
0.18 |
64 B |
1.00 |
DT_TryFormat |
.NET 7.0 |
? |
151.52 ns |
1.00 |
– |
NA |
DT_TryFormat |
.NET 8.0 |
? |
78.57 ns |
0.52 |
– |
NA |
DT_TryFormatInvariant |
.NET 7.0 |
? |
140.35 ns |
1.00 |
– |
NA |
DT_TryFormatInvariant |
.NET 8.0 |
? |
18.26 ns |
0.13 |
– |
NA |
DT_ToString |
.NET 7.0 |
G |
162.86 ns |
1.00 |
64 B |
1.00 |
DT_ToString |
.NET 8.0 |
G |
109.49 ns |
0.68 |
64 B |
1.00 |
DT_ToStringInvariant |
.NET 7.0 |
G |
162.20 ns |
1.00 |
64 B |
1.00 |
DT_ToStringInvariant |
.NET 8.0 |
G |
102.71 ns |
0.63 |
64 B |
1.00 |
DT_TryFormat |
.NET 7.0 |
G |
148.32 ns |
1.00 |
– |
NA |
DT_TryFormat |
.NET 8.0 |
G |
83.60 ns |
0.57 |
– |
NA |
DT_TryFormatInvariant |
.NET 7.0 |
G |
145.05 ns |
1.00 |
– |
NA |
DT_TryFormatInvariant |
.NET 8.0 |
G |
79.77 ns |
0.55 |
– |
NA |
DT_ToString |
.NET 7.0 |
s |
186.44 ns |
1.00 |
64 B |
1.00 |
DT_ToString |
.NET 8.0 |
s |
29.35 ns |
0.17 |
64 B |
1.00 |
DT_ToStringInvariant |
.NET 7.0 |
s |
182.15 ns |
1.00 |
64 B |
1.00 |
DT_ToStringInvariant |
.NET 8.0 |
s |
27.67 ns |
0.16 |
64 B |
1.00 |
DT_TryFormat |
.NET 7.0 |
s |
165.08 ns |
1.00 |
– |
NA |
DT_TryFormat |
.NET 8.0 |
s |
15.53 ns |
0.09 |
– |
NA |
DT_TryFormatInvariant |
.NET 7.0 |
s |
155.24 ns |
1.00 |
– |
NA |
DT_TryFormatInvariant |
.NET 8.0 |
s |
15.50 ns |
0.10 |
– |
NA |
DT_ToString |
.NET 7.0 |
u |
184.71 ns |
1.00 |
64 B |
1.00 |
DT_ToString |
.NET 8.0 |
u |
29.62 ns |
0.16 |
64 B |
1.00 |
DT_ToStringInvariant |
.NET 7.0 |
u |
184.01 ns |
1.00 |
64 B |
1.00 |
DT_ToStringInvariant |
.NET 8.0 |
u |
26.98 ns |
0.15 |
64 B |
1.00 |
DT_TryFormat |
.NET 7.0 |
u |
171.73 ns |
1.00 |
– |
NA |
DT_TryFormat |
.NET 8.0 |
u |
16.08 ns |
0.09 |
– |
NA |
DT_TryFormatInvariant |
.NET 7.0 |
u |
158.42 ns |
1.00 |
– |
NA |
DT_TryFormatInvariant |
.NET 8.0 |
u |
15.58 ns |
0.10 |
– |
NA |
DT_ToString |
.NET 7.0 |
U |
1,622.28 ns |
1.00 |
1240 B |
1.00 |
DT_ToString |
.NET 8.0 |
U |
206.08 ns |
0.13 |
96 B |
0.08 |
DT_ToStringInvariant |
.NET 7.0 |
U |
1,567.92 ns |
1.00 |
1240 B |
1.00 |
DT_ToStringInvariant |
.NET 8.0 |
U |
207.60 ns |
0.13 |
96 B |
0.08 |
DT_TryFormat |
.NET 7.0 |
U |
1,590.27 ns |
1.00 |
1144 B |
1.00 |
DT_TryFormat |
.NET 8.0 |
U |
190.98 ns |
0.12 |
– |
0.00 |
DT_TryFormatInvariant |
.NET 7.0 |
U |
1,560.00 ns |
1.00 |
1144 B |
1.00 |
DT_TryFormatInvariant |
.NET 8.0 |
U |
184.11 ns |
0.12 |
– |
0.00 |
解析也有了有意义的改进。例如改进了自定义格式字符串中“ddd”(一周中某天的缩写名称)、“dddd”(一周中某天的全名)、“MMM”(月份的缩写名称)和“MMMM”(月份的全名)的处理;这些在各种常用格式字符串中都有出现,比如在 RFC1123 格式的扩展定义中:ddd, dd MMM yyyy HH':'mm':'ss 'GMT'。当通用解析例程在格式字符串中遇到这些时,它需要查阅提供的 CultureInfo / DateTimeFormatInfo,以获取该语言区域设置的相关月份和日期名称,例如 DateTimeFormatInfo.GetAbbreviatedMonthName,然后需要对每个名称和输入文本进行语言忽略大小写的比较;开销很大。然而,如果我们得到的是一个不变的语言区域设置,我们可以做得更快,快得多。以“MMM”为例,代表缩写的月份名称。我们可以读取接下来的三个字符(uint m0 = span[0], m1 = span[1], m2 = span[2]),确保它们都是 ASCII ((m0 | m1 | m2) <= 0x7F),然后将它们全部合并成一个单独的 uint,使用之前讨论过的相同的 ASCII 大小写技巧 ((m0 << 16) | (m1 << 8) | m2 | 0x202020)。我们可以对每个月份名称做同样的事情,这些对于不变的语言区域设置我们提前知道,整个查找变成了一个单一的数字切换:
switch ((m0 << 16) | (m1 << 8) | m2 | 0x202020)
{
case 0x6a616e: /* 'jan' */ result = 1; break;
case 0x666562: /* 'feb' */ result = 2; break;
case 0x6d6172: /* 'mar' */ result = 3; break;
case 0x617072: /* 'apr' */ result = 4; break;
case 0x6d6179: /* 'may' */ result = 5; break;
case 0x6a756e: /* 'jun' */ result = 6; break;
case 0x6a756c: /* 'jul' */ result = 7; break;
case 0x617567: /* 'aug' */ result = 8; break;
case 0x736570: /* 'sep' */ result = 9; break;
case 0x6f6374: /* 'oct' */ result = 10; break;
case 0x6e6f76: /* 'nov' */ result = 11; break;
case 0x646563: /* 'dec' */ result = 12; break;
default: maxMatchStrLen = 0; break; // undo match assumption
}
优雅,而且速度更快。
// dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Globalization;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
[MemoryDiagnoser(displayGenColumns: false)]
public class Tests
{
private const string Format = "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'";
private readonly string _s = new DateTime(1955, 11, 5, 6, 0, 0, DateTimeKind.Utc).ToString(Format, CultureInfo.InvariantCulture);
[Benchmark]
public void ParseExact() => DateTimeOffset.ParseExact(_s, Format, CultureInfo.InvariantCulture, DateTimeStyles.AllowInnerWhite | DateTimeStyles.AssumeUniversal);
}
性能对比:
方法 |
运行时 |
平均值 |
比率 |
分配 |
分配比率 |
ParseExact |
.NET 7.0 |
1,139.3 ns |
1.00 |
80 B |
1.00 |
ParseExact |
.NET 8.0 |
318.6 ns |
0.28 |
– |
0.00 |