Lambda 操作或 AggressiveInlining 以避免评估复杂字符串并将其传递给单独的静态类?

问题描述 投票:0回答:2

我试图避免评估字符串并将其传递给单独的静态类,如果该类中设置的标志无论如何都会跳过使用字符串。使用

System.Diagnostics.Stopwatch
进行基本性能测量。一个 C# .NET Framework 4.8 库,它是语音识别应用程序的插件。

插件对静态类进行多次调用,传递各种评估字符串。根据该类中设置的静态状态过滤不同的调用,因此仅当匹配的静态布尔为真时才使用字符串。例如

Logger.MenuWrite(string msg)
只会在
Logger.MenuItems
为真时记录字符串。

根据 Stopwatch 测量,我认为无论 Logger 类是否将不使用它们,字符串总是得到评估(也就是说,我不认为 JIT 不是内联的)。虽然这对性能的影响很小,但我正在努力争取每一毫秒,因为我可以扩大规模。

到目前为止我已经尝试和测试的内容:

我在一些循环周围添加了秒表测量,这些循环在

Logger.MenuWrite()
为假时进行了大量
Logger.MenuItems
调用,然后通过检查 Logger.MenuItems 为每个调用内联完成测量了相同的循环,并看到了明确的、可重复的差异 -对于只有一个评估字段的字符串,每 1000 次调用减少大约一毫秒。

我首先在 Logger 类中的静态方法上尝试了

[MethodImpl(MethodImplOptions.AggressiveInlining)]
,如下所示:

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void MenuWrite(string msg)
        {
            if (s_MenuItems )
            {   vaProxy.WriteToLog(s_Prefix + msg); }
        }

这将循环时间缩短了一半,但仍然比我在循环中进行实际直接检查时多了大约 1/2 毫秒,例如:

if (Logger.MenuItems) { Logger.MenuWrite(msg); }

所以我尝试使用代表,像这样:

        static Action<string> LogIfMenu = (msg) =>
        {
            if (Logger.MenuItems) { Logger.MenuWrite(msg); }
        };

但是使用

LogIfMenu
调用似乎与使用
[MethodImpl(MethodImplOptions.AggressiveInlining)]
具有相同或更差的性能。

关于导致性能命中的原因的任何想法 - 字符串评估/创建,方法调用,其他?除了手动内联所有调用之外,我们将不胜感激任何建议或选择。谢谢。

编辑:

  • 通过评估字符串,我的意思是引入其他数据,例如:
    $"Passed: {Cmd.Observable} and {Cmd.Dist}"
  • 我将尝试查看列出的其他性能工具,但确实需要测量发布版本中经过的时间
  • 恐怕我必须使用动态对象进行日志记录,因为这是我的插件所提供的应用程序。也就是说,我不认为这是这个问题的一部分,所以从代码片段中删除了它。

编辑:修改为控制台应用程序的小型可重现示例。

// File1.cs
namespace CS_Console_Test_05
{
    static public class Logger
    {
        public static bool MenuItems = false;
        public static void MenuWrite(string msg)
        {
            if (MenuItems) { Console.WriteLine(msg); }
        }
    }
}

// File2.cs
namespace CS_Console_Test_05
{
    internal class Program
    {
        public static void LoopMessagesInline()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 10000; i++)
            {
                if (Logger.MenuItems)
                { Logger.MenuWrite($"Counting Down to the time {sw.Elapsed}"); }
            }
            sw.Stop();
            Console.WriteLine($"Inline Elapsed = {sw.Elapsed}");
        }

        public static void LoopMessagesCall()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 10000; i++)
            {
                Logger.MenuWrite($"Counting Down to the time {sw.Elapsed}");
            }
            sw.Stop();
            Console.WriteLine($"Called Elapsed = {sw.Elapsed}");
        }

        static void Main(string[] args)
        {
            do
            {
                Console.WriteLine("Enter Value for MenuItems:");
                string miRead = Console.ReadLine();
                Logger.MenuItems = (miRead.Equals("Kludge"));    // so JIT won't know if true or false
                Console.WriteLine("'x' to quit, SPACE for Inline, nothing for Call, then ENTER: ");
                string way = Console.ReadLine();
                way = way.ToLower();
                if (way.Equals(" "))
                { LoopMessagesCall(); }
                else if (way.Equals("x"))
                { return; }
                else
                { LoopMessagesInline(); }

            } while (true);
        }
    }
}

调用 LoopMessageInline() 大约需要 7-8 毫秒。调用 LoopMessageCall() 不到 1 毫秒。

如上所述,无论是 MethodImplOptions.AggressiveInlining 还是使用 Delegates 似乎都无济于事。

c# function inline
2个回答
1
投票

根据@HansPassant 的评论,可以通过将 Logger 类中的方法更改为:

来避免性能命中
public static void MenuWrite(Func<string> msg) 
{ 
    if (MenuItems) Console.Write(msg()); 
}

并将调用更改为:

Logger.MenuWrite(() => $"Counting Down to the time {sw.Elapsed}");

sw.Elapsed 的评估当然只是针对repro case。是时候多读一些关于委托的文章了——要记住这不是 C++。

编辑:Per @GuruStron 不得不将

ConsoleWrite(msg)
更改为
ConsoleWrite(msg())

Perf 测试非常基础,在问题中列出的最小重现项目中使用 Stopwatch。


1
投票

首先使用适当的基准测试工具——比如 BenchmarkDotNet.

我提出了以下基准:

namespace CS_Console_Test_05
{
    static public class Logger
    {
        public static bool MenuItems = false;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void MenuWrite(string msg)
        {
            if (MenuItems)
            {
                Console.WriteLine(msg);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void MenuWriteFormattableString(FormattableString msg)
        {
            if (MenuItems)
            {
                Console.WriteLine(msg);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void MenuWriteFunc(Func<string> msg)
        {
            if (MenuItems)
            {
                Console.WriteLine(msg());
            }
        }
    }
}
[MemoryDiagnoser]
public class LoggerWrapperBench
{
    public static string Value = "TestVal";
    private const int Iterations = 1000;

    [Benchmark]
    public void LoopMessagesInline()
    {
        for (int i = 0; i < Iterations; i++)
        {
            if (Logger.MenuItems)
            {
                Console.WriteLine($"Counting Down to the time {Value}");
            }
        }
    }

    [Benchmark]
    public void LoopMessagesInlineFormatableString()
    {
        for (int i = 0; i < Iterations; i++)
        {
            Logger.MenuWriteFormattableString($"Counting Down to the time {Value}");
        }
    }
    
    [Benchmark]
    public void LoopMessagesInlineFunc()
    {
        for (int i = 0; i < Iterations; i++)
        {
            Logger.MenuWriteFunc(() => $"Counting Down to the time {Value}");
        }
    }

    [Benchmark]
    public void LoopMessagesCall()
    {
        for (int i = 0; i < Iterations; i++)
        {
            Logger.MenuWrite($"Counting Down to the time {Value}");
        }
    }
}

在我的机器上给出:

方法 平均 错误 标准差 Gen0 已分配
LoopMessagesInline 524.7 纳秒 10.10 纳秒 10.37 纳秒 - -
LoopMessagesInlineFormatableString 10,908.3 纳秒 215.37 纳秒 328.89 纳秒 10.1929 64000 乙
LoopMessagesInlineFunc 1,031.8 纳秒 18.34 纳秒 21.12 纳秒 - -
LoopMessagesCall 14,523.6 纳秒 286.28 纳秒 391.86 纳秒 14.0228 88000 乙

使惰性函数方法最接近内联方法(尽管我有点想知道为什么它不分配任何东西)。

请注意,在

MenuWrite
MenuWriteFormattableString
的情况下,内联对字符串评估没有太大影响,因为:

var s = DoSomething(); // like build string
if(...)
{
    Console.WriteLine(s);
}

if(...)
{
    Console.WriteLine(DoSomething());
}

在一般情况下在功能上不等价(由于函数调用可能产生的副作用),所以内联不应该改变程序的正确性,所以字符串格式被调用(至少这是我对这个主题的理论)。

更新

还有一个值得一提的方法(虽然我无法让它执行得更快,并且在多个插值元素的情况下它甚至可以执行得更慢) - 自 .NET 6 以来,您可以创建一个自定义插值字符串处理程序

[InterpolatedStringHandler]
public readonly ref struct LogInterpolatedStringHandler
{
    readonly StringBuilder? _builder;

    public LogInterpolatedStringHandler(int literalLength, int formattedCount)
    {
        if (Logger.MenuItems)
        {
            _builder = new StringBuilder(literalLength);
        }
    }

    public void AppendLiteral(string s) => _builder?.Append(s);

    public void AppendFormatted<T>(T t) => _builder?.Append(t?.ToString());

    internal string GetFormattedText()
    {
        if (_builder is not null)
        {
            var format = _builder.ToString();
            Console.WriteLine(format);
            return format;
        }

        return string.Empty;
    }
}

和用法:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MenuWriteInterpolatedStringHandler(ref LogInterpolatedStringHandler msg)
{
    if(MenuItems) msg.GetFormattedText();
}
[Benchmark]
public void LoopMenuWriteInterpolatedStringHandler()
{
    for (int i = 0; i < Iterations; i++)
    {
        Logger.MenuWriteInterpolatedStringHandler($"Counting Down to the time {Value}");
    }
}

在我的机器上给出:

方法 平均 错误 标准差 已分配
LoopMenuWriteInterpolatedStringHandler 1,690.0 纳秒 32.63 纳秒 36.27 纳秒 -
LoopMessagesInline 534.2 纳秒 10.39 纳秒 15.22 纳秒 -
© www.soinside.com 2019 - 2024. All rights reserved.