序文前回の記事では列挙型の最適化について説明しました。今回は時刻形式である DateTime の最適化について見ていきます。 概要例として DateTime と DateTimeOffset を示します。 dotnet/runtime#84963 は、DateTime{Offset} フォーマットのさまざまな側面を改善します。 - フォーマット ロジックには、フォールバックとして使用するための一般的なサポートがあり、任意のカスタム フォーマットをサポートしますが、最も一般的なフォーマット専用のルーチンもあり、それらを最適化および調整できます。非常に人気のある「r」(RFC1123 パターン)および「o」(ラウンドトリップ日付/時刻パターン)形式には、すでに専用ルーチンが存在します。この PR では、さまざまなドメインで頻繁に使用される、不変カルチャで使用されるデフォルトの形式(「G」)、"s" 形式(並べ替え可能な日付/時刻パターン)、および "u" 形式(ユニバーサルな並べ替え可能な日付/時刻パターン)用の専用ルーチンを追加します。
- 「U」形式 (ユニバーサル完全日付/時刻パターン) の場合、実装では常に新しいインスタンスとインスタンスが割り当てられることになり、まれなフォールバックの場合にのみ必要な場合でも、割り当てが大量に発生します。これにより問題は修正され、本当に必要な場合にのみ割り当てられるようになります。日付時刻形式情報グレゴリオ暦
- 専用のフォーマット ルーチンがない場合、フォーマットは、指定されたスパン バッファ (通常は から) で開始され、必要に応じてメモリとともに拡大する内部呼び出しに対して実行されます。フォーマットが完了すると、フォーマットをトリガーしたメソッドに応じて、ジェネレーターは宛先範囲または新しい文字列にコピーされます。ただし、ビルダーにターゲット範囲のシードのみを提供する場合は、ターゲット範囲のコピーを回避できます。次に、フォーマットが完了したときにビルダーにまだ初期スパンが含まれている場合 (そこから何も拡張されていない)、すべてのデータが収まることがわかり、すべてのデータがすでにそこにあるため、コピーをスキップできます。 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); } パフォーマンステストは次のとおりです。 方法 | ランタイム | 形式 | 平均 | 比率 | 割り当て済み | 割り当て比率 | DT_文字列 | .NET 7.0 | ? | 166.64ナノ秒 | 1.00 | 64 B | 1.00 | DT_文字列 | .NET 8.0 | ? | 102.45ナノ秒 | 0.62 | 64 B | 1.00 |
|
|
|
|
|
|
| DT_ToString不変 | .NET 7.0 | ? | 161.94ナノ秒 | 1.00 | 64 B | 1.00 | DT_ToString不変 | .NET 8.0 | ? | 28.74ナノ秒 | 0.18 | 64 B | 1.00 |
|
|
|
|
|
|
| DT_TryFormat | .NET 7.0 | ? | 151.52ナノ秒 | 1.00 | – | 該当なし | DT_TryFormat | .NET 8.0 | ? | 78.57ナノ秒 | 0.52 | – | 該当なし |
|
|
|
|
|
|
| DT_TryFormatInvariant | .NET 7.0 | ? | 140.35ナノ秒 | 1.00 | – | 該当なし | DT_TryFormatInvariant | .NET 8.0 | ? | 18.26ナノ秒 | 0.13 | – | 該当なし |
|
|
|
|
|
|
| DT_文字列 | .NET 7.0 | グ | 162.86ナノ秒 | 1.00 | 64 B | 1.00 | DT_文字列 | .NET 8.0 | グ | 109.49ナノ秒 | 0.68 | 64 B | 1.00 |
|
|
|
|
|
|
| DT_ToString不変 | .NET 7.0 | グ | 162.20ナノ秒 | 1.00 | 64 B | 1.00 | DT_ToString不変 | .NET 8.0 | グ | 102.71ナノ秒 | 0.63 | 64 B | 1.00 |
|
|
|
|
|
|
| DT_TryFormat | .NET 7.0 | グ | 148.32ナノ秒 | 1.00 | – | 該当なし | DT_TryFormat | .NET 8.0 | グ | 83.60ナノ秒 | 0.57 | – | 該当なし |
|
|
|
|
|
|
| DT_TryFormatInvariant | .NET 7.0 | グ | 145.05ナノ秒 | 1.00 | – | 該当なし | DT_TryFormatInvariant | .NET 8.0 | グ | 79.77ナノ秒 | 0.55 | – | 該当なし |
|
|
|
|
|
|
| DT_文字列 | .NET 7.0 | s | 186.44ナノ秒 | 1.00 | 64 B | 1.00 | DT_文字列 | .NET 8.0 | s | 29.35ナノ秒 | 0.17 | 64 B | 1.00 |
|
|
|
|
|
|
| DT_ToString不変 | .NET 7.0 | s | 182.15ナノ秒 | 1.00 | 64 B | 1.00 | DT_ToString不変 | .NET 8.0 | s | 27.67ナノ秒 | 0.16 | 64 B | 1.00 |
|
|
|
|
|
|
| DT_TryFormat | .NET 7.0 | s | 165.08ナノ秒 | 1.00 | – | 該当なし | DT_TryFormat | .NET 8.0 | s | 15.53ナノ秒 | 0.09 | – | 該当なし |
|
|
|
|
|
|
| DT_TryFormatInvariant | .NET 7.0 | s | 155.24ナノ秒 | 1.00 | – | 該当なし | DT_TryFormatInvariant | .NET 8.0 | s | 15.50ナノ秒 | 0.10 | – | 該当なし |
|
|
|
|
|
|
| DT_文字列 | .NET 7.0 | あなた | 184.71ナノ秒 | 1.00 | 64 B | 1.00 | DT_文字列 | .NET 8.0 | あなた | 29.62ナノ秒 | 0.16 | 64 B | 1.00 |
|
|
|
|
|
|
| DT_ToString不変 | .NET 7.0 | あなた | 184.01ナノ秒 | 1.00 | 64 B | 1.00 | DT_ToString不変 | .NET 8.0 | あなた | 26.98ナノ秒 | 0.15 | 64 B | 1.00 |
|
|
|
|
|
|
| DT_TryFormat | .NET 7.0 | あなた | 171.73ナノ秒 | 1.00 | – | 該当なし | DT_TryFormat | .NET 8.0 | あなた | 16.08ナノ秒 | 0.09 | – | 該当なし |
|
|
|
|
|
|
| DT_TryFormatInvariant | .NET 7.0 | あなた | 158.42ナノ秒 | 1.00 | – | 該当なし | DT_TryFormatInvariant | .NET 8.0 | あなた | 15.58ナノ秒 | 0.10 | – | 該当なし |
|
|
|
|
|
|
| DT_文字列 | .NET 7.0 | あなた | 1,622.28ナノ秒 | 1.00 | 1240年 | 1.00 | DT_文字列 | .NET 8.0 | あなた | 206.08ナノ秒 | 0.13 | 96 B | 0.08 |
|
|
|
|
|
|
| DT_ToString不変 | .NET 7.0 | あなた | 1,567.92ナノ秒 | 1.00 | 1240年 | 1.00 | DT_ToString不変 | .NET 8.0 | あなた | 207.60ナノ秒 | 0.13 | 96 B | 0.08 |
|
|
|
|
|
|
| DT_TryFormat | .NET 7.0 | あなた | 1,590.27ナノ秒 | 1.00 | 1144年 | 1.00 | DT_TryFormat | .NET 8.0 | あなた | 190.98ナノ秒 | 0.12 | – | 0.00 |
|
|
|
|
|
|
| DT_TryFormatInvariant | .NET 7.0 | あなた | 1,560.00ナノ秒 | 1.00 | 1144年 | 1.00 | DT_TryFormatInvariant | .NET 8.0 | あなた | 184.11ナノ秒 | 0.12 | – | 0.00 |
解析にも大きな改善が見られました。たとえば、カスタム書式文字列内の「ddd」(曜日の略称)、「dddd」(曜日の正式名称)、「MMM」(月の略称)、「MMMM」(月の正式名称)の処理が改善されました。これらは、RFC1123 形式の拡張定義(ddd、dd MMM yyyy HH':'mm':'ss 'GMT')など、さまざまな一般的な書式文字列に表示されます。汎用解析ルーチンが書式文字列でこれらに遭遇すると、提供された CultureInfo / DateTimeFormatInfo を参照して、その言語ロケールの関連する月と日の名前 (例: DateTimeFormatInfo.GetAbbreviatedMonthName) を取得し、次に各名前と入力テキストの大文字と小文字を区別しない比較を行う必要があります。これはコストがかかります。ただし、不変の言語ロケールを取得すれば、はるかに高速に実行できます。はるかに高速です。たとえば、「MMM」は月の略称を表します。次の3文字(uint m0 = span[0]、m1 = span[1]、m2 = span[2])を読み取り、それらがすべてASCIIであること((m0 | m1 | m2)<= 0x7F)を確認してから、前に説明したのと同じASCII大文字小文字のトリックを使用して、それらすべてを1つのuintにマージします((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); } パフォーマンス比較: 方法 | ランタイム | 平均値 | 比率 | 配布する | 配分比率 | パース正確 | .NET 7.0 | 1,139.3ナノ秒 | 1.00 | 80 B | 1.00 | パース正確 | .NET 8.0 | 318.6ナノ秒 | 0.28 | – | 0.00 |
|