使用现有方法或重写 - 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 是否已经内联它因为它知道?是否所有这些都由编译器处理,并没有真正像人们想象的那样跳过方法?

复杂的函数很容易出错,我会保留一个位置。

编辑: 为了澄清微优化的概念以及为什么我认为考虑这些事情是有效的。

我认为如果知道以某种方式编写代码可以带来更好的结果 "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 纳秒,这看起来太小了,不用担心手动内联代码。