为什么 System.out.println 很慢?

17

这是否是所有编程语言都普遍存在的情况?做多个print后面跟一个println似乎更快,但将所有内容移动到字符串中并仅打印该字符串似乎最快。为什么?

编辑:例如,Java可以在不到一秒钟的时间内找到1百万以内的所有质数-但逐个打印它们上一个单独的println可能需要几分钟!打印10亿个数字可能需要几个小时!

例子:

package sieveoferatosthenes;
public class Main {
    public static void main(String[] args) {
        int upTo = 10000000;
        boolean primes[] = new boolean[upTo];
        for( int b = 0; b < upTo; b++ ){
            primes[b] = true;
        }
        primes[0] = false;
        primes[1] = false;

        int testing = 1;

        while( testing <= Math.sqrt(upTo)){
            testing ++;
            int testingWith = testing;
            if( primes[testing] ){
                while( testingWith < upTo ){
                    testingWith = testingWith + testing;
                    if ( testingWith >= upTo){
                    }
                    else{
                        primes[testingWith] = false;
                    }

                }
            }
        }
        for( int b = 2; b < upTo; b++){
            if( primes[b] ){
                System.out.println( b );
            }
        }
    }
}

能解释一下吗?我一直觉得println非常快... - froadie
@DasWood "似乎"? 请提供一些基准测试 (代码+时间)。 - NPE
它在*nix上往往很快,在Windows上很慢。换句话说,这取决于这些操作系统的控制台实现。 - skaffman
用禁止打印的方式测试我粘贴的示例。然后再次进行测试,但不要禁止打印。巨大的差异,而且它只运行一次数字线,而不是像实际筛子那样运行9999998次。 - user328898
你是指 println 和不打印输出哪个,还是printlnprint之间的区别?它们是非常不同的问题。 - Josh Lee
1
不要重复使用println,将所有内容放入一个字符串中,然后再进行println。这样更快。 - user328898
7个回答

30

println 并不慢,而是与控制台连接的底层 PrintStream 与提供操作系统相关。

您可以自行验证:比较将大型文本文件转储到控制台与将相同的文本文件传输到另一个文件进行管道处理:

cat largeTextFile.txt
cat largeTextFile.txt > temp.txt

读写文件的时间复杂度都为O(n),与文件大小成正比,唯一的区别在于目标不同(控制台和文件)。System.out也是同理。


操作系统底层的操作(在控制台窗口上显示字符)很慢,因为:

  1. 字节必须发送到控制台应用程序(这应该非常快)
  2. 通常需要使用真实类型字体来呈现每个字符(这很慢,关闭抗锯齿可提高性能)
  3. 显示区域可能需要滚动才能将新行附加到可见区域(最好的情况:位块传输操作,最坏的情况:重新渲染完整文本区域)

好的,那为什么底层IO操作如此缓慢呢? - user328898
在控制台输出方面,理论上是否有可能进行优化呢?例如,将输出缓冲到“一页”大小的缓冲区中,这样只需要执行一次滚动操作即可显示所有内容。我知道实际上输出缓冲的责任在于控制台的实现,只是好奇而已。 - Michael Schnerring

5

System.out 是一个静态的 PrintStream 类。 PrintStream 有很多方法,你可能非常熟悉,比如 print()println() 等。

在输入和输出操作方面,Java 并不是唯一需要花费很长时间的语言。"long." 打印或写入到 PrintStream 中只需要几分之一秒,但超过 100 亿次的打印可能会累加成相当多的时间!

这就是为什么 "将所有内容移至字符串中" 是最快的方式。你构建了一个巨大的字符串,但只打印了它一次。当然,这是一个巨大的打印,但你花费的时间实际上是在打印上,而不是在与 print()println() 相关的开销上。

正如 Dvd Prd 所提到的,字符串是不可变的。这意味着每当你将新的字符串赋值给旧的字符串时,但重用引用时,你实际上会销毁对旧字符串的引用并创建对新字符串的引用。因此,你可以使用可变的 StringBuilder 类使整个操作更快。这将减少与构建最终要打印的字符串相关的开销。


在你的回答和Alfred的回答之间,给出的答案非常完整。谢谢。 - user328898
1
创建字符串不是问题。如果只是创建字符串而不打印它们,它将以接近相同的速度运行。 - Peter Lawrey
请看我的回复,它显示将对象转换为字符串或写入文件所需的时间非常短,而将其写入控制台则需要更长的时间(即使仅显示带有值的文件)。 - Peter Lawrey
我提到这将使其“更快”,对于大约100亿个字符串连接来说是这样,正如OP所提到的那样。我绝不会说创建String需要最多的时间。 - JBirch

4
我认为这是由于缓冲的原因。文章中的一句话:
另一个缓冲方面涉及到向终端窗口输出文本。默认情况下,System.out(一个PrintStream)是行缓冲的,这意味着当遇到换行符时,输出缓冲区将被刷新。这对于交互性非常重要,因为您希望在实际输入任何内容之前显示输入提示。
维基百科解释缓冲的一句话:
在计算机科学中,缓冲区是用于在数据从一个地方移动到另一个地方时临时存储数据的内存区域。通常,数据在从输入设备(例如鼠标)检索出来或发送到输出设备(例如扬声器)之前存储在缓冲区中。
public void println()

通过编写行分隔符字符串来终止当前行。行分隔符字符串由系统属性line.separator定义,并且不一定是单个换行符('\n')。

因此,当您执行println时,缓冲区会被刷新,这意味着需要分配新的内存等操作,从而使打印变慢。您指定的其他方法需要较少地刷新缓冲区,因此更快。


将缓冲到文件或控制台是相同的(它们都是底层的文件描述符),但文件快了多达300倍,这意味着缓冲成本是相当小的一部分。 - Peter Lawrey
@Peter 我认为你说得对。光盘的吞吐量比屏幕高得多? - Alfred

3

默认情况下,System.out.print() 仅进行行缓冲并执行与 Unicode 处理相关的大量工作。由于其缓冲区大小较小,System.out.println() 不适合批处理模式下处理许多重复输出。每行都会立即刷新。如果您的输出主要基于 ASCII,则通过删除与 Unicode 相关的活动,整体执行时间将更好。


1
你遇到的问题是屏幕显示非常昂贵,特别是在图形窗口/X窗口环境下(而不是纯文本终端)。仅渲染一个数字的字体比你正在进行的计算要昂贵得多。当你发送数据到屏幕比它能够显示的更快时,它会缓冲数据并迅速阻塞。即使写入文件与计算相比也很重要,但它比在屏幕上显示快10倍至100倍。
顺便说一句,math.sqrt()非常昂贵,使用循环比使用模数即%来确定一个数字是否为多个数字要慢得多。BitSet可以比boolean[]节省8倍的空间,并且对于多位操作(例如计数或搜索位)更快。
如果我将输出转储到文件中,它会很快,但是写入控制台很慢,如果我将数据写入控制台,它需要大约相同的时间。
Took 289 ms to examine 10,000,000 numbers.
Took 149 ms to toString primes up to 10,000,000.
Took 306 ms to write to a file primes up to 10,000,000.
Took 61,082 ms to write to a System.out primes up to 10,000,000.

time cat primes.txt

real    1m24.916s
user    0m3.619s
sys     0m12.058s

代码

int upTo = 10*1000*1000;
long start = System.nanoTime();
BitSet nonprimes = new BitSet(upTo);
for (int t = 2; t * t < upTo; t++) {
    if (nonprimes.get(t)) continue;
    for (int i = 2 * t; i <= upTo; i += t)
        nonprimes.set(i);
}
PrintWriter report = new PrintWriter("report.txt");
long time = System.nanoTime() - start;
report.printf("Took %,d ms to examine %,d numbers.%n", time / 1000 / 1000, upTo);

long start2 = System.nanoTime();
for (int i = 2; i < upTo; i++) {
    if (!nonprimes.get(i))
        Integer.toString(i);
}
long time2 = System.nanoTime() - start2;
report.printf("Took %,d ms to toString primes up to %,d.%n", time2 / 1000 / 1000, upTo);

long start3 = System.nanoTime();
PrintWriter pw = new PrintWriter(new BufferedOutputStream(new FileOutputStream("primes.txt"), 64*1024));
for (int i = 2; i < upTo; i++) {
    if (!nonprimes.get(i))
        pw.println(i);
}
pw.close();
long time3 = System.nanoTime() - start3;
report.printf("Took %,d ms to write to a file primes up to %,d.%n", time3 / 1000 / 1000, upTo);

long start4 = System.nanoTime();
for (int i = 2; i < upTo; i++) {
    if (!nonprimes.get(i))
        System.out.println(i);
}
long time4 = System.nanoTime() - start4;
report.printf("Took %,d ms to write to a System.out primes up to %,d.%n", time4 / 1000 / 1000, upTo);
report.close();

1
"BitSet比boolean[]更高效,可以节省8倍的空间。"(为了明确起见)我相当确定你在这里指的是空间效率。你把它放在了一个关于sqrt()和模运算速度效率的陈述的结尾,所以似乎你在说BitSet也更快。只有当巨大的布尔数组涉及页面错误时,使用长数组中的位不会发生,此时BitSet才更快。 - alife
@alife 说得好,如果你正在获取/设置单个位,就像这个例子一样,那么它并不会更快。然而,如果你正在使用搜索或掩码操作,即用单个操作触及许多位,那么它可以比原来快8倍以上。 - Peter Lawrey

1

如果你是在控制台窗口打印,而不是输出到文件,那将会是致命的。

每个字符都必须被绘制,每一行都要滚动整个窗口。如果窗口部分重叠其他窗口,还需要进行剪切。

这将比你的程序所做的工作要花费更多的周期。

通常来说,这不是一个坏的代价,因为控制台输出应该是为了你的阅读愉悦 :)


0

这里的大多数答案都是正确的,但它们没有涵盖最重要的一点:系统调用。这是导致更多开销的操作。

当您的软件需要访问某些硬件资源(例如您的屏幕)时,它需要询问操作系统(或虚拟机监控程序)是否可以访问硬件。这会耗费很多成本:

以下是关于系统调用的有趣博客,其中最后一个专门介绍了syscall和Java。

http://arkanis.de/weblog/2017-01-05-measurements-of-system-call-performance-and-overhead http://www.brendangregg.com/blog/2014-05-11/strace-wow-much-syscall.html https://blog.packagecloud.io/eng/2017/03/14/using-strace-to-understand-java-performance-improvement/


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