与将字符串连接在一起并一次调用相比,经常调用println()有多糟糕?


23

我知道输出到控制台是一项昂贵的操作。出于代码可读性的考虑,有时最好调用一个函数两次输出文本,而不是将一长串文本作为参数。

例如,效率降低多少

System.out.println("Good morning.");
System.out.println("Please enter your name");

System.out.println("Good morning.\nPlease enter your name");

在该示例中,区别仅在于一个呼叫,println()如果更多,该怎么办?

与此相关的是,如果要打印的文本很长,则在查看源代码时,涉及打印文本的语句可能看起来很奇怪。假设文本本身不能简短,该怎么办?是否应该出现多次println()通话的情况?曾经有人告诉我,代码行不应超过80个字符(IIRC),那么您将如何处理

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

对于C / C ++这样的语言,是否同样如此,因为每次将数据写入输出流时,都必须进行系统调用,并且该过程必须进入内核模式(这非常昂贵)?


即使这是很少的代码,我也不得不说我一直在想同样的事情。一劳永逸地确定答案的人
Simon Forsberg 2014年

@SimonAndréForsberg我不确定它是否适用于Java,因为它在虚拟机上运行,​​但是在较低级的语言(例如C / C ++)中,我想这会很昂贵,因为每次将某些内容写入输出流时,系统调用必须制作。


1
我必须说,我在这里看不到重点。通过终端与用户进行交互时,我无法想象任何性能问题,因为通常不需要打印太多内容。具有GUI或Webapp的应用程序应写入日志文件(通常使用框架)。
安迪

1
如果您要说早安,则每天要做一两次。优化不是问题。如果还有其他问题,则需要进行分析以了解是否有问题。除非您构建多行缓冲区并在一次调用中转储文本,否则我在日志记录中使用的代码会使代码无法使用。
mattnz

Answers:


29

这里有两个“压力”,是紧张的:性能与可读性。

但是,让我们首先解决第三个问题,排长队:

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

做到这一点并保持可读性的最佳方法是使用字符串连接:

System.out.println("Good morning everyone. I am here today to present you "
                 + "with a very, very lengthy sentence in order to prove a "
                 + "point about how it looks strange amongst other code.");

字符串常量连接将在编译时发生,并且对性能完全没有影响。线条清晰易读,您可以继续前进。

现在,关于:

System.out.println("Good morning.");
System.out.println("Please enter your name");

System.out.println("Good morning.\nPlease enter your name");

第二个选项明显更快。我会建议快2倍...。为什么?

因为90%的工作(有很大的误差)与将字符转储到输出无关,但与确保输出写入数据所需的开销有关。

同步化

System.out是一个PrintStream。我知道的所有Java实现都在内部同步PrintStream:请参见GrepCode上的代码!

这对您的代码意味着什么?

这意味着,每次调用System.out.println(...)同步存储模型时,您都在检查并等待锁定。任何其他调用System.out的线程也将被锁定。

在单线程应用程序中,其影响System.out.println()通常受系统的IO性能,写入文件的速度限制。在多线程应用程序中,锁定比IO可能更多。

冲洗

每个println都被刷新。这将导致缓冲区被清除,并触发控制台级别的缓冲区写入。此处完成的工作量取决于实现方式,但是通常可以理解,刷新的性能仅在很小一部分上与要刷新的缓冲区的大小有关。与刷新相关的开销很大,内存刷新被标记为已脏,虚拟机正在执行IO,等等。一次优化的开销是一次而不是两次。

一些数字

我整理了以下小测试:

public class ConsolePerf {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            benchmark("Warm " + i);
        }
        benchmark("real");
    }

    private static void benchmark(String string) {
        benchString(string + "short", "This is a short String");
        benchString(string + "long", "This is a long String with a number of newlines\n"
                  + "in it, that should simulate\n"
                  + "printing some long sentences and log\n"
                  + "messages.");

    }

    private static final int REPS = 1000;

    private static void benchString(String name, String value) {
        long time = System.nanoTime();
        for (int i = 0; i < REPS; i++) {
            System.out.println(value);
        }
        double ms = (System.nanoTime() - time) / 1000000.0;
        System.err.printf("%s run in%n    %12.3fms%n    %12.3f lines per ms%n    %12.3f chars per ms%n",
                name, ms, REPS/ms, REPS * (value.length() + 1) / ms);

    }


}

该代码相对简单,它反复打印一个短字符串或长字符串以输出。长字符串中包含多个换行符。它测量每个打印1000次迭代所需的时间。

如果我在unix(Linux)命令提示符下运行它,并将其重定向STDOUT/dev/null,并将实际结果打印到STDERR,则可以执行以下操作:

java -cp . ConsolePerf > /dev/null 2> ../errlog

输出(在errlog中)如下所示:

Warm 0short run in
           7.264ms
         137.667 lines per ms
        3166.345 chars per ms
Warm 0long run in
           1.661ms
         602.051 lines per ms
       74654.317 chars per ms
Warm 1short run in
           1.615ms
         619.327 lines per ms
       14244.511 chars per ms
Warm 1long run in
           2.524ms
         396.238 lines per ms
       49133.487 chars per ms
.......
Warm 99short run in
           1.159ms
         862.569 lines per ms
       19839.079 chars per ms
Warm 99long run in
           1.213ms
         824.393 lines per ms
      102224.706 chars per ms
realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

这是什么意思?让我重复最后一个“节”:

realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

这意味着,就所有意图和目的而言,即使“长”行的长度大约是5倍,并且包含多个换行符,其输出时间也仅与短线一样长。

从长远来看,每秒的字符数是5倍,并且经过的时间大约是相同的.....

换句话说,你的表现尺度相对你有printlns的,没有什么他们打印。

更新:如果您重定向到文件而不是/ dev / null,会发生什么?

realshort run in
           2.592ms
         385.815 lines per ms
        8873.755 chars per ms
reallong run in
           2.686ms
         372.306 lines per ms
       46165.955 chars per ms

它的速度要慢很多,但是比例大致相同。


添加了一些性能数字。
rolfl 2014年

您还必须考虑"\n"可能不是正确的行终结器的问题。println会自动以正确的字符终止行,但是\n直接将a 插入字符串可能会导致问题。如果您想正确执行操作,则可能需要使用字符串格式或line.separatorsystem属性println更干净。
user2357112支持Monica 2014年

3
所有这些都是很好的分析,因此可以肯定为+1,但是我认为一旦您致力于控制台输出,这些微小的性能差异就会消失。如果程序的算法运行速度比输出结果快(在这种很小的输出水平上),则可以逐个打印每个字符,而不会注意到差异。
David Harkness,2014年

我相信这与Java和C / C ++之间的区别在于输出是同步的。我之所以这样说,是因为我记得编写一个多线程程序,并且如果不同的线程尝试写入控制台时会出现输出乱码的问题。有人可以验证吗?

6
同样重要的是要记住,将速度放在等待用户输入的功能旁边时,速度根本不重要。
vmrob

2

我不认为一堆printlns根本不是设计问题。我的看法是,如果确实存在问题,可以使用静态代码分析器明确地完成此操作。

但这不是问题,因为大多数人不这样做。当他们确实需要做很多IO时,当输入被缓冲时,他们使用缓冲的IO(BufferedReader,BufferedWriter等),您将看到性能足够相似,您不必担心拥有一个IO。一束println或几束println

所以要回答原来的问题。我会说,如果您println像大多数人一样习惯打印一些东西,那还不错println


1

在C和C ++等高级语言中,这比Java中的问题少。

首先,C和C ++定义了编译时字符串连接,因此您可以执行以下操作:

std::cout << "Good morning everyone. I am here today to present you with a very, "
    "very lengthy sentence in order to prove a point about how it looks strange "
    "amongst other code.";

在这种情况下,串联字符串不仅是一种优化,通常还取决于编译器来进行优化。相反,它是C和C ++标准直接要求的(翻译的第6阶段:“串联相邻字符串文字标记”。)。

尽管以牺牲编译器和实现的额外复杂性为代价,但是C和C ++的作用还在于隐藏了有效地产生来自程序员的输出的复杂性。Java更像是汇编语言-每个调用System.out.println将更直接地转换为对基础操作的调用,以将数据写入控制台。如果要缓冲以提高效率,则必须单独提供。

这意味着,例如,在C ++中,将前面的示例重写为如下所示:

std::cout << "Good morning everyone. I am here today to present you with a very, ";
std::cout << "very lengthy sentence in order to prove a point about how it looks ";       
std::cout << "strange amongst other code.";

...通常1对效率几乎没有影响。每次使用cout只会将数据存储到缓冲区中。当缓冲区填满或代码尝试从使用中读取输入时(例如with std::cin),该缓冲区将刷新到基础流。

iostreamsync_with_stdio属性还可以确定iostream的输出是否与C风格的输入(例如getchar)同步。默认情况下sync_with_stdio设置为true,因此,如果,例如,你写std::cout,然后通过阅读getchar,你写的数据cout将被刷新的时候getchar被调用。您可以将其设置sync_with_stdio为false以禁用该功能(通常是为了提高性能)。

sync_with_stdio还控制线程之间的同步程度。如果打开了同步(默认设置),则从多个线程向iostream写入可能会导致线程中的数据交错,但会阻止任何竞争情况。IOW,您的程序将运行并产生输出,但是如果一次有多个线程写入流,则来自不同线程的数据的任意混合通常会使输出变得毫无用处。

如果关闭同步,那么同步来自多个线程的访问也完全由您负责。来自多个线程的并发写入可能/将导致数据争用,这意味着代码具有未定义的行为。

摘要

C ++默认尝试平衡速度与安全性。对于单线程代码,结果是相当成功的,但对于多线程代码,结果却不太成功。多线程代码通常需要确保一次只将一个线程写入流以产生有用的输出。


1.可以关闭流的缓冲,但是实际上这样做是非常不寻常的,并且当/如果有人这样做,可能是出于相当具体的原因,例如确保尽管对性能有影响,但仍要立即捕获所有输出。在任何情况下,只有在代码明确执行此操作时,才会发生这种情况。


13
在C和C ++等高级语言中,这比Java中的问题少。 ”-什么?C和C ++是比Java更底层的语言。另外,您忘记了行终止符。
user2357112支持Monica 2014年

1
我始终指出Java是较低级语言的客观基础。不知道您在说什么终止符。
杰里·科芬

2
Java也进行编译时串联。例如,"2^31 - 1 = " + Integer.MAX_VALUE将其存储为单个插入字符串(JLS Sec 3.10.515.28)。
200_success

2
@ 200_success:Java在编译时进行字符串连接似乎可以归结为§15.18.1:“除非创建的表达式是编译时常量表达式(第15.28节),否则将重新创建String对象(第12.5节)。” 这似乎允许但不要求在编译时进行串联。即,如果输入不是编译时常数,则必须重新创建结果,但是如果输入不是编译时常数,则不需要在任何方向上进行输入。要要求编译时级联,您必须将其(暗含的)“ if”读为真正的意思是“ if and only if”。
杰里·科芬

2
@Phoshi:尝试资源甚至与RAII都不一样。RAII允许类管理资源,但是尝试使用资源需要客户端代码来管理资源。一种语言具有的功能(抽象性,更准确地说)是完全相关的-实际上,它们正是使一种语言比另一种语言具有更高水平的原因。
杰里·科芬

1

虽然性能并不是真正的问题,但是一堆println语句的可读性差表明缺少设计方面。

为什么我们要编写许多println语句的序列?如果它只是一个固定的文本块(如--help控制台命令中的文本),则将其作为单独的资源并按需读取并写入屏幕会更好。

但是通常它是动态和静态部分的混合。假设我们一方面有一些裸订单数据,另一方面有一些固定的静态文本部分,这些东西必须混合在一起以形成订单确认表。同样,在这种情况下,最好有一个单独的资源文本文件:资源将是一个模板,其中包含某种符号(占位符),这些符号在运行时将被实际的订单数据替换。

编程语言与自然语言的分离具有许多优势-国际化是其中的优势:如果要与软件成为多语言,则可能必须翻译文本。另外,如果只想进行文本更正,为什么要执行编译步骤,比如说解决一些拼写错误。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.