Java中如何通过标准输出提高输出速度?

8
在一个在线编程竞赛中,我需要通过标准输出输出多达50,000行的内容,并在1秒内完成(此外还需读取多达200,000个整数对,我使用了缓冲区)。我的逻辑似乎是正确的,但我一直因为超过1秒运行时间而被拒绝提交。我简化了我的代码逻辑,只输出一个常量字符串,但仍然超过了时间限制。 有没有比每行输出使用 System.out.println(String s) 更快的输出方式?

3
如果这是一场比赛,你不应该在网上征求帮助,对吗? - Hovercraft Full Of Eels
1
哈哈,没关系。这个比赛早就结束了。这是一个在线评测问题,我解决它只能获得一些荣誉分数。我只是提到它是为了给问题一些背景,以解释为什么我需要这样做。 - Tozar
1
如果您尝试通过StringBuilder创建一个大型多行字符串并将其打印出来,会发生什么?您已经自己测试过这个过程的时间了吗? - Hovercraft Full Of Eels
@Hovercraft 已经可以工作了,我之前以为我试过了,但可能是我做错了什么。你能回答一下这个问题吗?这样我就可以给你点赞了。 - Tozar
4个回答

9
我会使用单个System.out.print调用(或者尽可能少的调用,可以通过基准测试找到最佳方案),如下所示:
```java System.out.print("要输出的内容"); ```
String str = "line1\nline2\nline3\n ...";
System.out.print(str);

编辑:

    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 500000; i++) {
        sb.append(i).append("\n");
    }
    String str = sb.toString();
    long nt = System.nanoTime();
    System.out.print(str);
    nt = System.nanoTime() - nt;
    System.out.print("\nTime(ms): " + (double)nt / 1000000);

sb.toString()不是一个免费的操作。

在我的笔记本上,以上操作需要大约650ms(使用500,000而不是请求的50,000)。

编辑2:如果填充时间很重要,则可以使用其他两个技巧:

  • construct the StringBuilder with a sufficient capacity
  • don't append for every line (the code below appends 200 lines every time, for this it uses a temp sb1); only possible if every line can have the same content. Enjoy.

    long nt = System.nanoTime();
    StringBuilder sb1 = new StringBuilder(400);
    for (int i = 0; i < 200; i++) {
        sb1.append("l").append("\n");
    }
    String strSb1 = sb1.toString();
    
    StringBuilder sb = new StringBuilder(1000000);
    for (int i = 0; i < 2500; i++) {
        sb.append(strSb1);
    }
    
    System.out.print(sb.toString());
    nt = System.nanoTime() - nt;
    System.out.print("\nTime(ms): " + (double)nt / 1000000);
    

在我的情况下,大约需要500毫秒。


解决方案还需要一个 StringBuilder,根据我在之前对 OP 的评论。 - Hovercraft Full Of Eels
请确保在 System.out.print 之前分配 String str = stringBuilder.toString(),以便 toString() 的成本不计入输出所需的时间。 - Marius Burz
无论如何都会调用toString。 - Hovercraft Full Of Eels
一般的想法是,逐行追加字符串并将该字符串一次性写入标准输出的成本要低于每行调用System.out.print()的成本。 - Tozar

5

如上所述,解决方案是使用StringBuilder类构建您的String,然后使用StringBuilder.toString()方法获取结果。
您应该自行测试此功能。


4
很可能你没有使用足够的缓冲区。如果你写入System.out,它会自动刷新每一行,因此在写入之前将几行分组可以添加一些缓冲。更好的方法是使用适当的缓冲区。
使用StringBuffer有一个固有的成本,即增长缓冲区。
long start = System.nanoTime();
 StringBuilder sb = new StringBuilder();
for(long i=0;i<50*1000;i++)
  sb.append("Hello World!\n");
System.out.print(sb);
long time = System.nanoTime() - start;
System.err.printf("Took %d ms%n", time/1000000);

打印

Took 30 ms.

然而在Linux上,如果你知道stdout是一个文件,你可以直接向其写入内容。

long start = System.nanoTime();
String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
FileOutputStream out = new FileOutputStream("/proc/" + processId + "/fd/1");
BufferedOutputStream bos = new BufferedOutputStream(out);
final byte[] str = "Hello World!\n".getBytes();
for (long i = 0; i < 50 * 1000; i++) {
  bos.write(str);
}
bos.close();
long time = System.nanoTime() - start;
System.err.printf("Took %d ms%n", time/1000000);

打印

Took 9 ms.

然而,您必须小心地优化代码的数量,因为您可能会打破基准测试的隐含规则并且无法得到批准。 ;)


直接使用stdout确实有一定的优点,但在这种情况下为什么要使用Java呢?不如选择C,而不是使用依赖于平台的Java。 - Marius Burz
你不必像第二个例子那样极端。你只需要缓冲System.out并向其写入字节即可。即使在廉价笔记本电脑上,时间非常快,几乎没有影响。 - Peter Lawrey
缓冲输出似乎是最好的选择,这也是我推荐的,请参见我的答案。 - Marius Burz
我认为你建议使用StringBuilder,它类似于缓冲,但更明显的选择可能是BufferedOutputStream - Peter Lawrey
在我的机器上,通过使用标准缓冲区为8K的BufferedOutputStream缓冲System.out需要大约60毫秒,而使用100000字节的缓冲区只需要大约24毫秒(每行看起来像“l\n”,50k行)。因此,是的,缓冲输出流可以非常快,比我的方法(~50ms)更快,只要使用适当的缓冲区。谢谢! - Marius Burz

0

这也取决于底层操作系统。如果我是你,我会直接写入文件,这样速度更快。

如果你不能这样做,并且你的应用程序在具有阻塞控制台输出(而不是非阻塞控制台输出)的操作系统上运行,那么你可以从编写自己的异步记录器中受益。例如,Log4j有实现此功能的记录器。

基本上,您将拥有一个执行器服务,您可以向其中提交字符串,让它写入控制台。当该线程正在写入时,您的原始工作线程将继续工作,从而减少等待消息刷新的损失。

控制台输出也得到更好的利用,因为在完成刷新时,新消息已经准备好了。

这样,您可能能够增加吞吐量。


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