使用现有方法或重写 - Helper Library
Use Existing Method or Rewrite - Helper Library
我有一个 class 库,其中充满了我经常使用的典型方法,尤其是在我的领域中。在这一点上,这个图书馆的大部分内容多年来一直保持不变。添加多于更改。
在库中,库中有很多调用方法 - 使用自身。
从完整的优化角度来看 - 因为这个库被大量使用,所以更有意义:
使用库中的方法或重新编写每个方法中的代码以避免调用另一个方法。与在被调用方法中调用代码相比,调用另一个方法或两个方法的成本是多少。
例如,这说明了一个常见的场景。此方法将通常丑陋的 URL 参数(编码 html)变成更简单、可破解的带破折号的日期。所以它被数百个用户在一个页面上调用了数百次甚至数千次(所以可能不是微不足道的次数?)。
我认为这不是预优化或微优化的原因(因此我要问)是因为这是一个库并且被许多应用程序使用,在同一台服务器上,有 100s的用户,"micro" 节省的钱真的可以加起来。
string GeturlDate(this DateTime date)
{
return date.GetUrlDate(date, "-");
}
string GetUrlDate(this DateTime date, string delimiter)
{
return DateHelper.GetUrlDate(date, delimiter);
}
string DateHelper.GetUrlDate(DateTime date, string delimiter)
{
return string.format("{0}-{1}-{2}", date...);
}
在这种情况下,可以在每个方法中直接完成带有 string.format 的最终方法。避免最上面的方法进入两个方法。前两个是扩展方法,最后一个是直接调用。
让我们跳过重载选项(已使用)。虽然上面的代码无疑更利于维护——最后一段代码在一个地方,但效率会高多少。 IL 是否已经内联它因为它知道?是否所有这些都由编译器处理,并没有真正像人们想象的那样跳过方法?
复杂的函数很容易出错,我会保留一个位置。
编辑:
为了澄清微优化的概念以及为什么我认为考虑这些事情是有效的。
- 很多时候您不知道自己有性能问题 - 这并不意味着没有 - 或者只有在特定条件下才会出现
- 有微优化过快的情况出现"this is the way I've always done it and it works (& there's no perf issues)"而不是考虑:"is there a better way to do this"(可能在某些条件下)
- 对于热路径来说,小赢很重要。导致 100ns 改进或内存消耗减少 1K、100K 或 1M 的更改可能会导致明显的差异。
我认为如果知道以某种方式编写代码可以带来更好的结果 "x" 那么这是一个有效的考虑,也许是实施(特别是如果您是第一次这样做而不是重构)。
最终,这个问题的答案表明,除其他事项外,编译器的作用以及 BCL 本身的编写方式,没有理由进行更改。
这是个好消息:)
IMO - 请原谅我的无礼 - 这实际上 是 不必要的微优化的完美示例。如果您实际上没有看到性能问题,并且您正在编写合理的代码(您确实如此),那么优化的理由就很薄弱。调用嵌套是一个真正可以忽略不计的性能考虑因素,对于像这样的简单函数,你不应该浪费时间考虑 IL!。如果您真的认为存在性能问题,您应该对代码进行基准测试以证明它。我想你会发现你在浪费大脑周期。
您是否 A) 确定了 B) 可归因于这些特定方法的实际性能问题,特别是 C) overloads/wrappers?如果不是,这似乎是在猜测您所猜测的问题的解决方案。
此外,看看 string.Format()
method overloads 他们自己;毫无疑问,性能对于那些人来说至关重要,因为它们在整个 .NET 中 无处不在 ,但它们都包含对 FormatHelper()
的调用,而不是重复实现。
考虑到这一点,您可以通过更改此删除一层方法调用...
public static class DateExtensions
{
public static string GetUrlDate(this DateTime date)
{
return date.GetUrlDate("-");
}
public static string GetUrlDate(this DateTime date, string delimiter)
{
return DateHelper.GetUrlDate(date, delimiter);
}
}
...到此...
public static class DateExtensions
{
public static string GetUrlDate(this DateTime date)
{
return DateHelper.GetUrlDate(date, "-");
}
public static string GetUrlDate(this DateTime date, string delimiter)
{
return DateHelper.GetUrlDate(date, delimiter);
}
}
...所以他们都直接调用 DateHelper.GetUrlDate(DateTime, String)
而不是一个重载调用另一个。不过,在您展开包装器方法之前,请考虑这个 BenchmarkDotNet 基准...
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace SO59976711
{
public static class DateExtensions
{
public static string GetUrlDate(this DateTime date)
{
return date.GetUrlDate("-");
}
public static string GetUrlDate(this DateTime date, string delimiter)
{
return DateHelper.GetUrlDate(date, delimiter);
}
}
public static class DateHelper
{
public static string GetUrlDate(DateTime date, string delimiter)
{
return string.Format("{0:yyyy}{1}{0:MM}{1}{0:dd}", date, delimiter);
}
}
[SimpleJob(RuntimeMoniker.Net48)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
public class DateFormattingBenchmarks
{
private static readonly DateTime TestDate = DateTime.Today;
private const string TestDelimiter = "-";
[Benchmark(Baseline = true)]
public string String_Format()
{
// Use the same implementation as DateHelper.GetUrlDate() as a baseline
return string.Format("{0:yyyy}{1}{0:MM}{1}{0:dd}", TestDate, TestDelimiter);
}
[Benchmark()]
public string DateExtensions_GetUrlDate_DefaultDelimiter()
{
return TestDate.GetUrlDate();
}
[Benchmark()]
public string DateExtensions_GetUrlDate_CustomDelimiter()
{
return TestDate.GetUrlDate(TestDelimiter);
}
[Benchmark()]
public string DateHelper_GetUrlDate()
{
return DateHelper.GetUrlDate(TestDate, TestDelimiter);
}
}
public static class Program
{
public static void Main()
{
BenchmarkDotNet.Running.BenchmarkRunner.Run<DateFormattingBenchmarks>();
}
}
}
...产生这些结果...
// * Summary *
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
Intel Core i7 CPU 860 2.80GHz (Nehalem), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
Job-RTUGNF : .NET Framework 4.8 (4.8.4075.0), X64 RyuJIT
Job-NPEBBX : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
| Method | Runtime | Mean | Error | StdDev | Ratio |
|------------------------------------------- |-------------- |-----------:|--------:|--------:|------:|
| String_Format | .NET 4.8 | 1,044.6 ns | 6.71 ns | 5.60 ns | 1.00 |
| DateExtensions_GetUrlDate_DefaultDelimiter | .NET 4.8 | 1,040.0 ns | 4.55 ns | 4.26 ns | 1.00 |
| DateExtensions_GetUrlDate_CustomDelimiter | .NET 4.8 | 1,045.6 ns | 8.31 ns | 6.49 ns | 1.00 |
| DateHelper_GetUrlDate | .NET 4.8 | 1,045.0 ns | 6.18 ns | 5.47 ns | 1.00 |
| | | | | | |
| String_Format | .NET Core 3.1 | 623.7 ns | 4.92 ns | 4.36 ns | 1.00 |
| DateExtensions_GetUrlDate_DefaultDelimiter | .NET Core 3.1 | 624.9 ns | 2.89 ns | 2.71 ns | 1.00 |
| DateExtensions_GetUrlDate_CustomDelimiter | .NET Core 3.1 | 618.5 ns | 2.48 ns | 2.07 ns | 0.99 |
| DateHelper_GetUrlDate | .NET Core 3.1 | 621.4 ns | 2.97 ns | 2.48 ns | 1.00 |
如您所见,调用三个辅助方法中的任何一个与 string.Format()
本身之间 没有性能差异。这意味着您应该以最易于维护且冗余最少的方式实现您的方法(即保持它们的原样),因为复制代码或 "pre-inlining" 可内联方法没有任何好处。
在我修正你的代码后,我尝试使用启用了发布模式调试的 VS,并在遇到断点后使用 Goto Disassembly,使用 [MethodImpl(MethodImplOptions.AggressiveInlining)]
和 [MethodImpl(MethodImplOptions.NoInlining)]
并且没有提示编译器。
对于 AggressiveInlining
,似乎 JIT 将所有从 GetUrlDate()
到 DateHelper.GetUrlDate(DateTime date, string delimiter)
的调用内联到 Main
方法中。在没有设置 MethodImplOptions
的情况下,Main
方法似乎直接调用 DateHelper.GetUrlDate
。使用 NoInlining
,每个方法都被调用,这也是 IL 显示的内容。
但是,时间显示在 Release AggressiveInlining
和 Debug NoInlining
之间的切换每次调用只增加了 46 纳秒,这看起来太小了,不用担心手动内联代码。
我有一个 class 库,其中充满了我经常使用的典型方法,尤其是在我的领域中。在这一点上,这个图书馆的大部分内容多年来一直保持不变。添加多于更改。
在库中,库中有很多调用方法 - 使用自身。
从完整的优化角度来看 - 因为这个库被大量使用,所以更有意义:
使用库中的方法或重新编写每个方法中的代码以避免调用另一个方法。与在被调用方法中调用代码相比,调用另一个方法或两个方法的成本是多少。
例如,这说明了一个常见的场景。此方法将通常丑陋的 URL 参数(编码 html)变成更简单、可破解的带破折号的日期。所以它被数百个用户在一个页面上调用了数百次甚至数千次(所以可能不是微不足道的次数?)。
我认为这不是预优化或微优化的原因(因此我要问)是因为这是一个库并且被许多应用程序使用,在同一台服务器上,有 100s的用户,"micro" 节省的钱真的可以加起来。
string GeturlDate(this DateTime date)
{
return date.GetUrlDate(date, "-");
}
string GetUrlDate(this DateTime date, string delimiter)
{
return DateHelper.GetUrlDate(date, delimiter);
}
string DateHelper.GetUrlDate(DateTime date, string delimiter)
{
return string.format("{0}-{1}-{2}", date...);
}
在这种情况下,可以在每个方法中直接完成带有 string.format 的最终方法。避免最上面的方法进入两个方法。前两个是扩展方法,最后一个是直接调用。
让我们跳过重载选项(已使用)。虽然上面的代码无疑更利于维护——最后一段代码在一个地方,但效率会高多少。 IL 是否已经内联它因为它知道?是否所有这些都由编译器处理,并没有真正像人们想象的那样跳过方法?
复杂的函数很容易出错,我会保留一个位置。
编辑: 为了澄清微优化的概念以及为什么我认为考虑这些事情是有效的。
- 很多时候您不知道自己有性能问题 - 这并不意味着没有 - 或者只有在特定条件下才会出现
- 有微优化过快的情况出现"this is the way I've always done it and it works (& there's no perf issues)"而不是考虑:"is there a better way to do this"(可能在某些条件下)
- 对于热路径来说,小赢很重要。导致 100ns 改进或内存消耗减少 1K、100K 或 1M 的更改可能会导致明显的差异。
我认为如果知道以某种方式编写代码可以带来更好的结果 "x" 那么这是一个有效的考虑,也许是实施(特别是如果您是第一次这样做而不是重构)。
最终,这个问题的答案表明,除其他事项外,编译器的作用以及 BCL 本身的编写方式,没有理由进行更改。
这是个好消息:)
IMO - 请原谅我的无礼 - 这实际上 是 不必要的微优化的完美示例。如果您实际上没有看到性能问题,并且您正在编写合理的代码(您确实如此),那么优化的理由就很薄弱。调用嵌套是一个真正可以忽略不计的性能考虑因素,对于像这样的简单函数,你不应该浪费时间考虑 IL!。如果您真的认为存在性能问题,您应该对代码进行基准测试以证明它。我想你会发现你在浪费大脑周期。
您是否 A) 确定了 B) 可归因于这些特定方法的实际性能问题,特别是 C) overloads/wrappers?如果不是,这似乎是在猜测您所猜测的问题的解决方案。
此外,看看 string.Format()
method overloads 他们自己;毫无疑问,性能对于那些人来说至关重要,因为它们在整个 .NET 中 无处不在 ,但它们都包含对 FormatHelper()
的调用,而不是重复实现。
考虑到这一点,您可以通过更改此删除一层方法调用...
public static class DateExtensions
{
public static string GetUrlDate(this DateTime date)
{
return date.GetUrlDate("-");
}
public static string GetUrlDate(this DateTime date, string delimiter)
{
return DateHelper.GetUrlDate(date, delimiter);
}
}
...到此...
public static class DateExtensions
{
public static string GetUrlDate(this DateTime date)
{
return DateHelper.GetUrlDate(date, "-");
}
public static string GetUrlDate(this DateTime date, string delimiter)
{
return DateHelper.GetUrlDate(date, delimiter);
}
}
...所以他们都直接调用 DateHelper.GetUrlDate(DateTime, String)
而不是一个重载调用另一个。不过,在您展开包装器方法之前,请考虑这个 BenchmarkDotNet 基准...
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace SO59976711
{
public static class DateExtensions
{
public static string GetUrlDate(this DateTime date)
{
return date.GetUrlDate("-");
}
public static string GetUrlDate(this DateTime date, string delimiter)
{
return DateHelper.GetUrlDate(date, delimiter);
}
}
public static class DateHelper
{
public static string GetUrlDate(DateTime date, string delimiter)
{
return string.Format("{0:yyyy}{1}{0:MM}{1}{0:dd}", date, delimiter);
}
}
[SimpleJob(RuntimeMoniker.Net48)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
public class DateFormattingBenchmarks
{
private static readonly DateTime TestDate = DateTime.Today;
private const string TestDelimiter = "-";
[Benchmark(Baseline = true)]
public string String_Format()
{
// Use the same implementation as DateHelper.GetUrlDate() as a baseline
return string.Format("{0:yyyy}{1}{0:MM}{1}{0:dd}", TestDate, TestDelimiter);
}
[Benchmark()]
public string DateExtensions_GetUrlDate_DefaultDelimiter()
{
return TestDate.GetUrlDate();
}
[Benchmark()]
public string DateExtensions_GetUrlDate_CustomDelimiter()
{
return TestDate.GetUrlDate(TestDelimiter);
}
[Benchmark()]
public string DateHelper_GetUrlDate()
{
return DateHelper.GetUrlDate(TestDate, TestDelimiter);
}
}
public static class Program
{
public static void Main()
{
BenchmarkDotNet.Running.BenchmarkRunner.Run<DateFormattingBenchmarks>();
}
}
}
...产生这些结果...
// * Summary *
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
Intel Core i7 CPU 860 2.80GHz (Nehalem), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
Job-RTUGNF : .NET Framework 4.8 (4.8.4075.0), X64 RyuJIT
Job-NPEBBX : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
| Method | Runtime | Mean | Error | StdDev | Ratio |
|------------------------------------------- |-------------- |-----------:|--------:|--------:|------:|
| String_Format | .NET 4.8 | 1,044.6 ns | 6.71 ns | 5.60 ns | 1.00 |
| DateExtensions_GetUrlDate_DefaultDelimiter | .NET 4.8 | 1,040.0 ns | 4.55 ns | 4.26 ns | 1.00 |
| DateExtensions_GetUrlDate_CustomDelimiter | .NET 4.8 | 1,045.6 ns | 8.31 ns | 6.49 ns | 1.00 |
| DateHelper_GetUrlDate | .NET 4.8 | 1,045.0 ns | 6.18 ns | 5.47 ns | 1.00 |
| | | | | | |
| String_Format | .NET Core 3.1 | 623.7 ns | 4.92 ns | 4.36 ns | 1.00 |
| DateExtensions_GetUrlDate_DefaultDelimiter | .NET Core 3.1 | 624.9 ns | 2.89 ns | 2.71 ns | 1.00 |
| DateExtensions_GetUrlDate_CustomDelimiter | .NET Core 3.1 | 618.5 ns | 2.48 ns | 2.07 ns | 0.99 |
| DateHelper_GetUrlDate | .NET Core 3.1 | 621.4 ns | 2.97 ns | 2.48 ns | 1.00 |
如您所见,调用三个辅助方法中的任何一个与 string.Format()
本身之间 没有性能差异。这意味着您应该以最易于维护且冗余最少的方式实现您的方法(即保持它们的原样),因为复制代码或 "pre-inlining" 可内联方法没有任何好处。
在我修正你的代码后,我尝试使用启用了发布模式调试的 VS,并在遇到断点后使用 Goto Disassembly,使用 [MethodImpl(MethodImplOptions.AggressiveInlining)]
和 [MethodImpl(MethodImplOptions.NoInlining)]
并且没有提示编译器。
对于 AggressiveInlining
,似乎 JIT 将所有从 GetUrlDate()
到 DateHelper.GetUrlDate(DateTime date, string delimiter)
的调用内联到 Main
方法中。在没有设置 MethodImplOptions
的情况下,Main
方法似乎直接调用 DateHelper.GetUrlDate
。使用 NoInlining
,每个方法都被调用,这也是 IL 显示的内容。
但是,时间显示在 Release AggressiveInlining
和 Debug NoInlining
之间的切换每次调用只增加了 46 纳秒,这看起来太小了,不用担心手动内联代码。