Console.WriteLine缓慢

32

我需要运行数百万条记录,并且有时需要使用Console.WriteLine来调试并查看代码执行情况。

然而,Console.WriteLine非常慢,比写入文件要慢得多。

但是,它非常方便 - 有没有人知道如何加速它的执行速度?


1
系统 IO 本质上是慢的。 - Woot4Moo
11
我认为没有比减少写入更多的方式来加速它的方法了。 - Peter
2
我同意Petoj的观点。每当我输出实时数据时,我会加入计数器/定时器,以仅输出1%左右的数据。 - Martin Hennings
6
为什么不直接输出到日志文件呢?如果是错误的话,或许只需要将信息输出到控制台? - jb.
@jb。也许是因为如果一个人立即看到问题所在,他们会修复它而不是打印出来? :) - Hi-Angel
显示剩余6条评论
8个回答

15
如果仅用于调试目的,应该使用 Debug.WriteLine。 这通常比使用 Console.WriteLine 更快。
Debug.WriteLine("There was an error processing the data.");

1
它写入到哪里了?也就是说,我该如何查看它? - ManInMoon
@MainInMoon,您的“输出窗口”。 - Filip Ekberg
2
为什么屏幕重绘比Console.WriteLine花费的时间更短? - ManInMoon
7
菲利普说的没错,Debug.WriteLine更快。但它不仅仅是“稍微快一点”……它要快得多。 - HappyNomad
这对我来说不比Console.WriteLine更快 - Ahmad
问题在于Debug.WriteLine功能受限。除非计算机安装了IDE,否则它将无法正常工作。 - Hi-Angel

12
您可以使用OutputDebugString API函数将字符串发送到调试器。它不会等待任何内容重新绘制,这可能是您在不太深入了解低级别东西的情况下获得的最快速度。
您提供给此函数的文本将进入Visual Studio输出窗口。
[DllImport("kernel32.dll")]
static extern void OutputDebugString(string lpOutputString);
然后,您只需调用OutputDebugString("Hello world!");

即使输出屏幕需要重新绘制,您认为这比写入到相同位置的Console.WriteLine更快吗? - ManInMoon
1
冒充者 - 刚刚测试了一下 - 但它没有写入输出窗口 - 它可能在其他地方吗? - ManInMoon
据我所知,您的问题是Console.WriteLine执行时间过长,并且会阻塞程序流程直到它完成。无论输出窗口是否已经重绘,OutputDebugString都会立即返回。如果您担心函数调用和实际看到消息之间的时间,那就是另一回事了。 - Oleg Tarasov
但是,OutputDebugString输出到哪里? - ManInMoon
1
你还可以使用DebugView (http://technet.microsoft.com/en-us/sysinternals/bb896647)查看这些消息。我刚刚在一个.NET 4控制台应用程序中进行了测试,它运行得非常顺畅。 - Oleg Tarasov
显示剩余4条评论

10

做类似于这样的事情:

public static class QueuedConsole
{
    private static StringBuilder _sb = new StringBuilder();
    private static int _lineCount;

    public void WriteLine(string message)
    {
        _sb.AppendLine(message);
        ++_lineCount;
        if (_lineCount >= 10)
           WriteAll();
    }

    public void WriteAll()
    {
        Console.WriteLine(_sb.ToString());
        _lineCount = 0;
        _sb.Clear();
    }
}

QueuedConsole.WriteLine("This message will not be written directly, but with nine other entries to increase performance.");

//after your operations, end with write all to get the last lines.
QueuedConsole.WriteAll();

这里是另一个例子:Console.WriteLine是否会阻塞?


谢谢。我有一个使用文件的快速编写器。我只是想知道为什么这样一个有用的工具会很慢 - 并迫使我们想出创造性的解决方案... - ManInMoon
请查看我添加的另一个答案链接。 - jgauffin
2
最好创建一个类,这样析构函数可以在程序崩溃时打印剩余的缓冲区。 - user159335
如果应用程序崩溃了,您确定控制台仍然完好无损吗? - jgauffin
我在考虑可能会在更高层次上捕获的异常,而不是任何灾难性的事情。 - user159335
jgauffin,你已经深入研究了这个问题。你知道为什么在运行时关闭输出并不能提高速度吗? - ManInMoon

9

最近我在.NET 4.8上进行了一项基准测试。测试包括许多在本页提到的建议,包括Async和BCL和自定义代码的阻塞变体,然后大部分都有和没有专用线程,并最终在2的幂缓冲区大小范围内扩展。

现在在我的项目中使用的最快方法是,直接从.NET以64K宽的(Unicode)字符为单位缓冲,使用Win32函数WriteConsoleW发送数据而无需复制或硬固定。填充并冲洗一个缓冲区后大于64K的余数也被直接、原地发送。这种做法故意绕过了Stream/TextWriter模型,因此它可以(显然)将.NET文本直接提供给(native) Unicode API,而无需所有超fluous memory copying/shuffling和对于首次“解码”成字节流的byte[]数组分配。

如果有兴趣(也许因为缓冲逻辑略微复杂),我可以提供上面的源代码; 它只有大约80行。然而,我的测试确定有一种更简单的方法可以获得几乎相同的性能,并且由于它不需要任何Win32调用,我将展示这种后者技术。

以下比Console.Write 快得多:

public static class FastConsole
{
    static readonly BufferedStream str;

    static FastConsole()
    {
        Console.OutputEncoding = Encoding.Unicode;  // crucial

        // avoid special "ShadowBuffer" for hard-coded size 0x14000 in 'BufferedStream' 
        str = new BufferedStream(Console.OpenStandardOutput(), 0x15000);
    }

    public static void WriteLine(String s) => Write(s + "\r\n");

    public static void Write(String s)
    {
        // avoid endless 'GetByteCount' dithering in 'Encoding.Unicode.GetBytes(s)'
        var rgb = new byte[s.Length << 1];
        Encoding.Unicode.GetBytes(s, 0, s.Length, rgb, 0);

        lock (str)   // (optional, can omit if appropriate)
            str.Write(rgb, 0, rgb.Length);
    }

    public static void Flush() { lock (str) str.Flush(); }
};

请注意,这是一个缓冲写入器,因此当您没有更多文本要写入时,必须调用 Flush()
我还应该提到,正如所示,技术上此代码假定使用了16位Unicode(UCS-2),而不是UTF-16,因此无法正确处理在基本多语言平面之外的字符的4字节转义代理。虽然相比于控制台文本显示的更极端限制而言,这点似乎并不重要,但对于管道/重定向可能仍然有影响。 使用方法:
FastConsole.WriteLine("hello world.");
// etc...
FastConsole.Flush();

在我的机器上,在相同条件下,使用普通的Console.WriteLine只能达到5,200行/秒(混合长度),而使用这种方法可以达到77,000行/秒。这意味着速度提升了近15倍。

这些只是对比结果,需要注意的是控制台输出性能的绝对测量值因控制台窗口设置和运行时条件(包括大小、布局、字体、DWM剪切等)而高度可变。


1
我尝试在每个FastConsole.WriteLine之后调用FastConsole.Flush,性能几乎与本机的Console.WriteLine相同。我想这是可以预料的。性能提升基本上来自于省略每次写入后刷新流。 - Theodor Zoulias

6

为什么控制台输出很慢:

  • 控制台输出实际上是由操作系统管理的IO流。大多数IO类(如FileStream)都有异步方法,但Console类从未更新过,因此在写入时始终会阻塞线程。

  • Console.WriteLineSyncTextWriter支持,它使用全局锁来防止多个线程写入部分行。这是一个主要瓶颈,迫使所有线程等待彼此完成写入。

  • 如果控制台窗口在屏幕上可见,则可能会出现显着的减速,因为在将控制台输出视为已刷新之前,需要重新绘制窗口。

解决方案:

使用StreamWriter包装控制台流,然后使用异步方法:

var sw = new StreamWriter(Console.OpenStandardOutput());
await sw.WriteLineAsync("...");

如果需要使用同步方法,您还可以设置更大的缓冲区。当缓冲区满时,调用将偶尔被阻塞,并刷新到流中。

// set a buffer size
var sw = new StreamWriter(Console.OpenStandardOutput(), Encoding.UTF8, 8192);
// this write call will block when buffer is full
sw.Write("...")

如果您想要最快的写入速度,您需要创建自己的缓冲区类,该类会异步地将数据写入内存并在后台刷新到控制台,使用单个线程而不锁定。.NET Core 2.1中的新Channel<T>使这变得简单且快速。有很多其他问题显示了该代码,但如果您需要提示,请留言。

我想问一下,这个操作不管怎样都会阻塞线程,对吗?这不就跟把同步操作包装在 Task.Run 里面一样吗?我看到 OpenStandardOutput 返回一个提供异步 API 的 Stream,所以我错了,这确实是异步的。 - E. Shcherbo
@E.Shcherbo 不是的。第一种选项使用标准的异步方法来写入流,而stdout只是一个流。你所要做的就是初始化自己的 StreamWriter 而不是使用静态的 Console 类。如果你写整行,这仍然会使用全局锁,因此会有争用,这就是我在最后谈论的内容。 - Mani Gandham

4
一个有点老的帖子,也许不完全符合OP的要求,但最近我在实时处理音频数据时遇到了同样的问题。
我使用这个代码比较了Console.WriteLineDebug.WriteLine,并使用DebugView作为一个dos盒子的替代品。它只是一个可执行文件(没有安装),可以以非常整洁的方式进行自定义(过滤器和颜色!)。它没有任何关于日志的泄漏(即使经过数天的记录)。
在不同的环境下进行了一些测试(例如:虚拟机、IDE、后台进程运行等),我得出了以下观察结果:
  • Debug几乎总是更快的。
  • 对于小量的行(<1000),它大约快10倍。
  • 对于更大的块,它似乎收敛到大约3倍。
  • 如果Debug输出到IDE,则Console更快:-)
  • 如果未运行DebugView,则Debug速度更快。
  • 对于连续的大量输出(>10000),Debug会变慢,Console保持不变。我认为这是由于Debug必须分配内存而Console没有。
  • 显然,如果DebugView实际上是“在视图”中的,则许多GUI更新对系统的整体性能有重大影响,而控制台只是挂起,无论是否可见。但很难给出具体数字...
我没有尝试多个线程写入Console,因为我认为这通常应该避免。从多个线程写入Debug时,我从来没有(性能)问题。
如果使用发布设置进行编译,通常所有Debug语句都会被省略,Trace应该产生与Debug相同的行为。
我使用了VS2017.Net 4.6.1 抱歉代码太多了,但我必须对它进行相当多的调整才能测量我想要的内容。如果您发现代码存在任何问题(偏见等),请发表评论。我希望获得更精确的真实系统数据。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespace Console_vs_Debug {
 class Program {
  class Trial {
   public string name;
   public Action console;
   public Action debug;
   public List < float > consoleMeasuredTimes = new List < float > ();
   public List < float > debugMeasuredTimes = new List < float > ();
  }

  static Stopwatch sw = new Stopwatch();
  private static int repeatLoop = 1000;
  private static int iterations = 2;
  private static int dummy = 0;

  static void Main(string[] args) {
   if (args.Length == 2) {
    repeatLoop = int.Parse(args[0]);
    iterations = int.Parse(args[1]);
   }

   // do some dummy work
   for (int i = 0; i < 100; i++) {
    Console.WriteLine("-");
    Debug.WriteLine("-");
   }

   for (int i = 0; i < iterations; i++) {
    foreach(Trial trial in trials) {
     Thread.Sleep(50);
     sw.Restart();
     for (int r = 0; r < repeatLoop; r++)
      trial.console();
     sw.Stop();
     trial.consoleMeasuredTimes.Add(sw.ElapsedMilliseconds);
     Thread.Sleep(1);
     sw.Restart();
     for (int r = 0; r < repeatLoop; r++)
      trial.debug();
     sw.Stop();
     trial.debugMeasuredTimes.Add(sw.ElapsedMilliseconds);

    }
   }
   Console.WriteLine("---\r\n");
   foreach(Trial trial in trials) {
    var consoleAverage = trial.consoleMeasuredTimes.Average();
    var debugAverage = trial.debugMeasuredTimes.Average();
    Console.WriteLine(trial.name);
    Console.WriteLine($ "    console: {consoleAverage,11:F4}");
    Console.WriteLine($ "      debug: {debugAverage,11:F4}");
    Console.WriteLine($ "{consoleAverage / debugAverage,32:F2} (console/debug)");
    Console.WriteLine();
   }

   Console.WriteLine("all measurements are in milliseconds");
   Console.WriteLine("anykey");
   Console.ReadKey();
  }

  private static List < Trial > trials = new List < Trial > {
   new Trial {
    name = "constant",
     console = delegate {
      Console.WriteLine("A static and constant string");
     },
     debug = delegate {
      Debug.WriteLine("A static and constant string");
     }
   },
   new Trial {
    name = "dynamic",
     console = delegate {
      Console.WriteLine("A dynamically built string (number " + dummy++ + ")");
     },
     debug = delegate {
      Debug.WriteLine("A dynamically built string (number " + dummy++ + ")");
     }
   },
   new Trial {
    name = "interpolated",
     console = delegate {
      Console.WriteLine($ "An interpolated string (number {dummy++,6})");
     },
     debug = delegate {
      Debug.WriteLine($ "An interpolated string (number {dummy++,6})");
     }
   }
  };
 }
}

1

如果你的意思是 Debug.WriteLine,那不是这样的。这似乎是一个普遍的误解。除非电脑有一个集成开发环境,否则它根本行不通。 - Hi-Angel

1
有时候我会用一个小技巧:如果你通过打开另一个窗口将焦点从控制台窗口移开,并且保持这种状态直到程序执行完毕,它在重新获得焦点之前不会重新绘制窗口,从而显著加快速度。只需确保缓冲区设置足够高,以便可以回滚查看所有的输出内容。

1
我没有发现那有什么帮助...即使我关闭输出窗口 - 速度也是一样的。 - ManInMoon
1
我认为这也没有什么区别。 - HappyNomad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接