【问题标题】:Performance difference between Windows and macOS .Net console appWindows 和 macOS .Net 控制台应用程序之间的性能差异
【发布时间】:2022-11-09 08:45:28
【问题描述】:

在比较 Windows 和 macOS 中完全相同的 .net 代码时,我遇到了一个有趣的性能问题。我不明白为什么会有如此显着的性能差异,我不确定最好的方法。

该代码适用于我一直在使用 Visual Studio for Mac 在 macOS 上开发的 .net (v6) 控制台应用程序 (C# v9)。这是一款等待用户输入的回合制游戏,仅在提示键盘输入之前重绘控制台窗口。我使用后备存储来执行此操作,并且只更新需要重新绘制的控制台窗口部分(通常只有几个字符)。结果,在 macOS 下性能似乎不错。

然后,我将代码复制到 Windows 并在 Visual Studio 2022 中重新编译。令我惊讶的是,性能非常差 - 无法使用。

因此,我使用 Stopwatch 类开始了一些简单的性能调查,从写入控制台窗口的方法开始:在 Windows 上,更新控制台窗口需要 98-108 毫秒。 macOS 上的相同代码始终被测量为花费 0 毫秒。

显然,0ms 值没有用处,因此为了获得更好的数字,我查看了秒表刻度而不是 ms,并很快确定这些不能直接比较:我在 Windows 上测量了大约 10134729 刻度时的 1000ms 线程延迟,但是macOS 上的 1018704390 滴答声。 MSDN 库说“秒表类使用的计时器取决于系统硬件和操作系统”(在 Windows 和 macOS 中,Stopwatch.IsHighResolutionTimer 为“真”)。假设这个比率应该延续到我在同一个应用程序中使用秒表类的所有其他性能测试(?),我们可以说 - 为了比较 macOS 和 Windows 之间的数字 - 我必须将 macOS 数字除以(大约)100 .

当我以刻度为控制台窗口更新计时时,我得到如下粗略的平均值:

  • 窗口:988,000-1,020,000 滴答声
  • macOS:595,000-780,000 刻 (请记住,将 macOS 除以 100 以与 Windows 进行比较,即非常粗略地170x性能差异)

笔记:

  • 我在 VMWare Fusion 中以访客身份运行 Windows 10。 macOS 是主机。主机和来宾都不应受到资源限制。更新:我尝试在真实硬件上运行下面的最小可重现代码示例,我得到了一致的结果(Windows 比 macOS 慢得多)
  • 我正在使用 80x25 控制台窗口进行测试。
  • 我尝试在 Windows 中调整控制台窗口属性,但没有任何效果。缓冲区大小与控制台窗口大小相同。
  • 应用程序以编程方式将控制台输出编码设置为 UTF8,将光标设置为“不可见”,并将 TreatControlCAsInput 设置为“真”。将所有这些作为默认设置没有任何区别。
  • 我没有在 Windows 中使用“旧版控制台”。
  • 我尝试在 Windows 下发布专门针对 Windows 和我的计算机体系结构的发布版本。没有明显的区别。
  • Windows 中的调试版本针对“任何 CPU”。
  • 如果我打开光标,我可以看到它在屏幕上“滑动”(在 Windows 中是从左到右,从上到下)。

这似乎不是我可以优化掉的那种差异(无论如何,我想理解它)。鉴于两个操作系统上的代码相同,什么可以解释如此显着的性能差异?有人遇到过这种情况么?

有问题的代码如下(两种方法):

private void FlushLine (int y)
{
    ConsoleColor? lastForegroundColour = null;
    ConsoleColor? lastBackgroundColour = null;
    int lastX = -1, lastY = -1;

    for (int x = 0; x < Math.Min (this.Width, this.currentLargestWindowWidth); ++x)
    {
        // write only when the current backing store is different from the previous backing store
        if (ConsoleWindow.primary.characters[y][x] != ConsoleWindow.previous.characters[y][x]
            || ConsoleWindow.primary.foreground[y][x] != ConsoleWindow.previous.foreground[y][x]
            || ConsoleWindow.primary.background[y][x] != ConsoleWindow.previous.background[y][x])
        {
            // only change the current console foreground and/or background colour 
            // if necessary because it's expensive
            if (!lastForegroundColour.HasValue || lastForegroundColour != ConsoleWindow.primary.foreground[y][x])
            {
                Console.ForegroundColor = ConsoleWindow.primary.foreground[y][x];
                lastForegroundColour = ConsoleWindow.primary.foreground[y][x];
            }

            if (!lastBackgroundColour.HasValue || lastBackgroundColour != ConsoleWindow.primary.background[y][x])
            {
                Console.BackgroundColor = ConsoleWindow.primary.background[y][x];
                lastBackgroundColour = ConsoleWindow.primary.background[y][x];
            }

            // only set the cursor position if necessary because it's expensive
            if (x != lastX + 1 || y != lastY)
            {
                Console.SetCursorPosition(x, y);
                lastX = x; lastY = y;
            }

            Console.Write(ConsoleWindow.primary.characters[y][x]);

            ConsoleWindow.previous.foreground[y][x] = ConsoleWindow.primary.foreground[y][x];
            ConsoleWindow.previous.background[y][x] = ConsoleWindow.primary.background[y][x];
            ConsoleWindow.previous.characters[y][x] = ConsoleWindow.primary.characters[y][x];
        }
    }
}

public void FlushBuffer ()
{
    int cursorX = Console.CursorLeft;
    int cursorY = Console.CursorTop;

    for (int y = 0; y < Math.Min (this.Height, this.currentLargestWindowHeight); ++y)
    {
        this.FlushLine (y);
    }

    Console.SetCursorPosition (cursorX, cursorY);
}

一个最小可重复的示例 - 用字母“A”填充控制台窗口

using System.Diagnostics;

Stopwatch stopwatch = new ();
stopwatch.Restart ();
Thread.Sleep (1000);
Debug.WriteLine ($"Thread Sleep 1000ms = {stopwatch.ElapsedTicks} ticks");

while (true)
{
    stopwatch.Restart ();

    for (int y = 0; y < Console.WindowHeight; ++y)
    {
        Console.SetCursorPosition (0, y);
        for (int x = 0; x < Console.WindowWidth; ++x)
        {
            Console.Write ('A');
        }
    }

    stopwatch.Stop ();
    Debug.WriteLine ($"{stopwatch.ElapsedTicks}");
}

【问题讨论】:

  • 你能加一个minimal reproducible example吗?
  • 还可以尝试在“本机”Win 10 机器上运行该应用程序。从理论上讲,这可能是一个虚拟化问题。
  • 我添加了一个最小的可重现示例,并通过在同事的开发计算机上进行测试,确认虚拟化不是一个因素 - Windows 下的性能不佳仍然存在。
  • 有关的; google.com/search?q=+windows+console+Grand+Overhaul 但是 AFAIK C# 仍在使用旧的 api。

标签: c# .net macos performance console


【解决方案1】:

TBH 不是这里的专家,可能这个问题应该在dotnet runtime github page 上得到更好的解决,但是对于你最小的可重现示例,有两个技巧有助于显着减少每次迭代的滴答声:

  1. 将尺寸计算移出循环:
    while (true)
    {
        stopwatch.Restart ();
        var windowHeight = Console.WindowHeight;
        var windowWidth = Console.WindowWidth;
        for (int y = 0; y < windowHeight; ++y)
        {
            Console.SetCursorPosition(0, y);
            for (int x = 0; x < windowWidth; ++x)
            {
                Console.Write ('A');
            }
        }
    
        stopwatch.Stop ();
        Debug.WriteLine ($"{stopwatch.ElapsedTicks}");
    }
    
    1. 在内存中构建字符串,而不是一一打印字符:
    while (true)
    {
        stopwatch.Restart ();
        var windowHeight = Console.WindowHeight;
        var windowWidth = Console.WindowWidth;
        for (int y = 0; y < windowHeight; ++y)
        {
            Console.SetCursorPosition(0, y);
            Console.Write(new string('A', windowWidth));
        }
    
        stopwatch.Stop();
        Debug.WriteLine ($"{stopwatch.ElapsedTicks}");
    }
    

    我的疯狂猜测是,由于 Windows 比基于 Unix 的操作系统更优先使用 GUI,因此命令行的性能可能会差很多。

【讨论】:

  • 了解 - 最小复制示例并非旨在提高性能。关键是,就像我的(性能优化的)代码一样,它在 Windows 上的运行速度比在 macOS 上慢得多。但感谢建议重新:.net 运行时 GitHub 页面 - 我会考虑在那里发布。
  • @PlunderBunny 没有看到整个代码很难说,但您可以尝试利用相同的方法 - 尝试构建字符串而不是逐个打印字符(尽管由于颜色处理会更复杂,但取决于实际数据 - 仍然可能)。至于为什么-“执行代码”实际上并不相同,您在具有不同内部工作原理的不同系统上运行不同的运行时。
  • @PlunderBunny 你也可以像建议的here 那样考虑使用 WinAPI
  • @PlunderBunny 也请看this one
【解决方案2】:

这似乎是由于 macOS 上的 .net 运行时实现缓存与控制台关联的值的方式不同,包括控制台窗口的宽度和高度。有关我在 GitHub 上的问题,请参阅 this answer

【讨论】:

    猜你喜欢
    • 2013-07-22
    • 1970-01-01
    • 1970-01-01
    • 2020-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-23
    • 1970-01-01
    相关资源
    最近更新 更多