为什么 Func<> 委托这么慢

Ric*_*y G 9 .net c# performance benchmarking .net-core

我正在使用 funcs 将重复的算术代码移动到可重用的块中,但是当我运行一个简单的测试来测试它是否会更慢时,我很惊讶它的速度是原来的两倍。

为什么计算表达式的速度要慢两倍

using System;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Running;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Calculations>();

            var logger = ConsoleLogger.Default;
            MarkdownExporter.Console.ExportToLog(summary, logger);

            Console.WriteLine(summary);
        }
    }

    public class Calculations
    {
        public Random RandomGeneration = new Random();

        [Benchmark]
        public void CalculateNormal()
        {
           var s =  RandomGeneration.Next() * RandomGeneration.Next();
        }

        [Benchmark]
        public void CalculateUsingFunc()
        {
            Calculate(() => RandomGeneration.Next() * RandomGeneration.Next());
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int Calculate(Func<int> expr)
        {
            return expr();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是基准: 在此处输入图片说明

Jon*_*eet 14

您在每次调用时都创建一个新的委托对象。这并不奇怪,它有相当多的开销。

如果您使用不捕获this或任何局部变量的 lambda 表达式(在这种情况下编译器可以将其缓存在静态字段中),或者如果您显式创建单个实例并将其存储在自己的字段中,则大部分开销消失了。

这是您的测试的修改版本:

public class Calculations
{
    public Random RandomGeneration = new Random();
    private Func<int> exprField;
    
    public Calculations()
    {
        exprField = () => RandomGeneration.Next() * RandomGeneration.Next();
    }
    
    [Benchmark]
    public void CalculateNormal()
    {
       var s =  RandomGeneration.Next() * RandomGeneration.Next();
    }

    [Benchmark]
    public void CalculateUsingFunc()
    {
        Calculate(() => RandomGeneration.Next() * RandomGeneration.Next());
    }
    
    [Benchmark]
    public void CalculateUsingFuncField()
    {
        Calculate(exprField);
    }

    
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int Calculate(Func<int> expr)
    {
        return expr();
    }
}
Run Code Online (Sandbox Code Playgroud)

我机器上的结果:

|                  Method |     Mean |    Error |   StdDev |
|------------------------ |---------:|---------:|---------:|
|         CalculateNormal | 27.61 ns | 0.438 ns | 0.388 ns |
|      CalculateUsingFunc | 48.74 ns | 1.009 ns | 0.894 ns |
| CalculateUsingFuncField | 32.53 ns | 0.698 ns | 0.717 ns |
Run Code Online (Sandbox Code Playgroud)

所以仍然有一些开销,但比以前少了很多。

  • 我责怪@EricLippert,我们必须处理这个问题,并且编译器不会在没有开销的情况下进行优化,特别是在唯一捕获的变量是“魔法”this 的情况下。 (4认同)
  • @RandRandom 我不这么认为。并且“Random”不是线程安全的,因此不鼓励使用“static Random”。 (3认同)
  • @RickyG:重点是*避免*捕获`this` - 但基本上,如果您正在访问使用实例中的某些内容(`RandomGeneration`)的任何内容,则必须在每个实例的基础上缓存或创建委托每次都很新鲜。 (3认同)
  • @RandRandom:性能并不“糟糕”——你只需要了解正在发生的事情并解释这一点。在许多情况下(例如通常在 LINQ 中),您创建委托的单个实例并多次执行它。这不是您的基准测试中发生的情况,其中每个委托都执行一次......是的,这会导致开销。如果您不想要能够将行为封装在对象中的额外灵活性,那么只需直接调用该方法... (3认同)