按字符逐个写入控制台,最快的方法是什么?

7
在我目前的项目中,我需要解析一个字符串,并将其部分内容写入控制台。在测试如何在没有太多开销的情况下完成此操作时,我发现一种我正在测试的方法实际上比Console.WriteLine更快,这让我有点困惑。
我知道这不是基准测试的“正确”方式,但通常我可以通过运行几次来粗略地判断“这比那个更快”。
static void Main(string[] args)
{
    var timer = new Stopwatch();

    timer.Restart();
    Test1("just a little test string.");
    timer.Stop();
    Console.WriteLine(timer.Elapsed);

    timer.Restart();
    Test2("just a little test string.");
    timer.Stop();
    Console.WriteLine(timer.Elapsed);

    timer.Restart();
    Test3("just a little test string.");
    timer.Stop();
    Console.WriteLine(timer.Elapsed);
}

static void Test1(string str)
{
    Console.WriteLine(str);
}

static void Test2(string str)
{
    foreach (var c in str)
        Console.Write(c);
    Console.Write('\n');
}

static void Test3(string str)
{
    using (var stream = new StreamWriter(Console.OpenStandardOutput()))
    {
        foreach (var c in str)
            stream.Write(c);
        stream.Write('\n');
    }
}

正如您所看到的,Test1使用了Console.WriteLine。我的第一个想法是每个字符都调用Write,看看Test2。但结果花费的时间大约是两倍。我猜测它在每次写入后都会刷新,这使它变得更慢。所以我尝试了Test3,使用StreamWriter(AutoFlush关闭),结果比Test1快大约25%,我真的很好奇为什么会这样。还是说无法正确地对控制台写入进行基准测试?(添加更多测试用例时注意到一些奇怪的数据...)
有人能启发我吗?
另外,如果有更好的方法(浏览字符串并仅将其部分写入控制台),请随时评论。

1
最后运行Test1,看是否有明显的差异。我猜测可能存在某种缓存机制。 - keyboardP
4
我认为你需要循环执行每个操作数数千次才能获得准确的基准测试结果? - James
你是否过早地进行了优化? - BAF
优化控制台输出并没有太多意义,因为它总是比人类阅读速度更快。 - Hans Passant
供以后参考:如果你想改变控制台的颜色,你必须在更改颜色后使用stream.Flush()。例如:Console.ForegroundColor = ConsoleColor.Green; stream.Write("This text is green"); stream.Flush(); Console.ForegroundColor = ConsoleColor.Red; stream.Write("This text will be red"); stream.Flush(); - Stefnotch
4个回答

6

首先,我同意其他评论者的观点,您的测试工具有所欠缺... 我对其进行了重写,并在下面包含了它。重写后的结果显然胜出:

//Test 1 = 00:00:03.7066514
//Test 2 = 00:00:24.6765818
//Test 3 = 00:00:00.8609692

你是对的,缓冲流写入器比普通写入器快25%。仅仅因为它使用了缓存。内部实现时,StreamWriter使用默认缓存大小大约为1~4kb(取决于流类型)。如果你使用8字节缓冲创建StreamWriter(允许的最小缓冲),你会发现大部分性能提升都消失了。你也可以在每次写入后使用Flush()方法查看这一点。

下面是获取以上数字的重写测试代码:

    private static StreamWriter stdout = new StreamWriter(Console.OpenStandardOutput());
    static void Main(string[] args)
    {
        Action<string>[] tests = new Action<string>[] { Test1, Test2, Test3 };
        TimeSpan[] timming = new TimeSpan[tests.Length];

        // Repeat the entire sequence of tests many times to accumulate the result
        for (int i = 0; i < 100; i++)
        {
            for( int itest =0; itest < tests.Length; itest++)
            {
                string text = String.Format("just a little test string, test = {0}, iteration = {1}", itest, i);
                Action<string> thisTest = tests[itest];

                //Clear the console so that each test begins from the same state
                Console.Clear();
                var timer = Stopwatch.StartNew();
                //Repeat the test many times, if this was not using the console 
                //I would use a much higher number, say 10,000
                for (int j = 0; j < 100; j++)
                    thisTest(text);
                timer.Stop();
                //Accumulate the result, but ignore the first run
                if (i != 0)
                    timming[itest] += timer.Elapsed;

                //Depending on what you are benchmarking you may need to force GC here
            }
        }

        //Now print the results we have collected
        Console.Clear();
        for (int itest = 0; itest < tests.Length; itest++)
            Console.WriteLine("Test {0} = {1}", itest + 1, timming[itest]);
        Console.ReadLine();
    }

    static void Test1(string str)
    {
        Console.WriteLine(str);
    }

    static void Test2(string str)
    {
        foreach (var c in str)
            Console.Write(c);
        Console.Write('\n');
    }

    static void Test3(string str)
    {
        foreach (var c in str)
            stdout.Write(c);
        stdout.Write('\n');
    }

这个线程安全吗?另外,我知道Win10的操作系统架构已经改变了,有没有其他高效的方法? - TheGeekZn

3

我已经对你的测试进行了10000次,以下是在我的机器上得到的结果:

test1 - 0.6164241  
test2 - 8.8143273    
test3 - 0.9537039  

这是我使用的脚本:
 static void Main(string[] args)
        {
            Test1("just a little test string.");     // warm up
            GC.Collect();  // compact Heap
            GC.WaitForPendingFinalizers(); // and wait for the finalizer queue to empty
            Stopwatch timer = new Stopwatch();
            timer.Start();
            for (int i = 0; i < 10000; i++)
            {
                Test1("just a little test string.");
            }
            timer.Stop();
            Console.WriteLine(timer.Elapsed);
        }

2

我将代码更改为每个测试运行1000次。

    static void Main(string[] args) {
        var timer = new Stopwatch();

        timer.Restart();
        for (int i = 0; i < 1000; i++)
            Test1("just a little test string.");
        timer.Stop();
        TimeSpan elapsed1 = timer.Elapsed;

        timer.Restart();
        for (int i = 0; i < 1000; i++)
            Test2("just a little test string.");
        timer.Stop();
        TimeSpan elapsed2 = timer.Elapsed;

        timer.Restart();
        for (int i = 0; i < 1000; i++)
            Test3("just a little test string.");
        timer.Stop();
        TimeSpan elapsed3 = timer.Elapsed;

        Console.WriteLine(elapsed1);
        Console.WriteLine(elapsed2);
        Console.WriteLine(elapsed3);

        Console.Read();
    }

我的输出:

00:00:05.2172738
00:00:09.3893525
00:00:05.9624869

1
我认为这更多是我的控制台设置问题。是的,将控制台窗口缩小回80x40可以使1和2恢复到亚秒级别。 - Blorgbeard

2
我也运行了这个程序10000次,并得到了以下结果:
00:00:00.6947374
00:00:09.6185047
00:00:00.8006468

这似乎与其他人观察到的情况相符。我很好奇为什么Test3Test1慢,所以写了第四个测试:

timer.Start();
using (var stream = new StreamWriter(Console.OpenStandardOutput()))
{
    for (int i = 0; i < testSize; i++)
    {
        Test4("just a little test string.", stream);
    }
}
timer.Stop();

这个方法在每次测试时重复使用流,因此避免了每次重新创建流的开销。结果:
00:00:00.4090399

尽管这是最快的方法,但它会在using块的末尾写入所有输出,这可能不是您想要的。我想这种方法也会消耗更多的内存。

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