AtCoderにて、System.out.printlnを複数回呼び出すと、AtCoder228_CやAtCoder231_Cのように最悪$10^5$行くらいの文字列を出力する場合、TLEになる場合がありました。
標準出力の複数回呼び出しによるオーバーヘッドの時間がかかりTLEになったものだと思います。本記事では同じ長さの文字列について、System.out.printlnを複数回呼び出して標準出力した場合の時間と、一度に標準出力した場合の時間を実際に比較しようと思います。
計測するコードの実装内容は$0~10^5$をインクリメントした文字列を改行して出力する処理です。この処理を100回繰り返して各実行毎の実行時間を取得します。$10^5$にした理由はAtCoderで出力する行数として、よく見かけるからです。
計測する手法は以下の三パターンです。
- 手法-1 System.out.printlnの複数回呼び出して出力する。
- 手法-2 StringBuilderにより文字を連結させてから一度に出力する。
- 手法-3 PrintWriterオブジェクトを利用し、フラッシュ(標準出力)されるのを1回にする。
手法-1 System.out.printlnの複数回呼び出し
単純にSystem.out.printlnを複数回呼び出します。計測に利用するコードは以下のものです。
public class PrintTest1 {
public static void main(String[] args) throws IOException {
// 出力する時刻を格納するリスト
List<String> results = new ArrayList<String>();
System.out.println("計算開始");
for (int i = 0; i < 100; i++) {
// 開始時刻の取得(ミリ秒)
long startTime = System.currentTimeMillis();
// 計算
caliculate();
// 計算時刻の格納
results.add(String.valueOf(System.currentTimeMillis() - startTime));
try {
//1秒待機する
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
// 結果の出力
Files.write(Paths.get("output1.log"), results);
System.out.println("計算終了");
}
/**
* System.out.printlnを用いて0~10^5の数字を標準出力する
*/
public static void caliculate() {
for (int i = 0; i <= 100000; i++) {
System.out.println(i);
}
}
}
手法-2 StringBuilderによる文字連結
StringBuilderによって文字を連結させた後、一度にSystem.out.printlnで出力します。計測に利用するコードは以下のものです。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class PrintTest2 {
public static void main(String[] args) throws IOException {
// 出力する時刻を格納するリスト
List<String> results = new ArrayList<String>();
System.out.println("計算開始");
for (int i = 0; i < 100; i++) {
// 開始時刻の取得(ミリ秒)
long startTime = System.currentTimeMillis();
// 計算
caliculate();
// 計算時刻の格納
results.add(String.valueOf(System.currentTimeMillis() - startTime));
try {
//1秒待機する
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
// 結果の出力
Files.write(Paths.get("output2.log"), results);
System.out.println("計算終了");
}
/**
* StringBuilderを用いて0~10^5の数字を標準出力する
*/
public static void caliculate() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i <= 100000; i++) {
sb.append(i).append('\r').append('\n');
}
System.out.print(sb.toString());
}
}
手法-3 PrintWriterを利用する
PrintWriterオブジェクトを利用して、flushメソッドによりフラッシュ(標準出力)されるタイミングを調整する。計測に利用するコードは以下のものです。
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class PrintTest3 {
public static void main(String[] args) throws IOException {
// 出力する時刻を格納するリスト
List<String> results = new ArrayList<String>();
System.out.println("計算開始");
for (int i = 0; i < 100; i++) {
// 開始時刻の取得(ミリ秒)
long startTime = System.currentTimeMillis();
// 計算
caliculate();
// 計算時刻の格納
results.add(String.valueOf(System.currentTimeMillis() - startTime));
try {
//1秒待機する
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
// 結果の出力
Files.write(Paths.get("output3.log"), results);
System.out.println("計算終了");
}
/**
* PrintWriterとflushメソッドを用いて0~10^5の数字を標準出力する
*/
public static void caliculate() {
PrintWriter pw = new PrintWriter(System.out);
for (int i = 0; i <= 100000; i++) {
pw.println(i);
}
pw.flush();
}
}
計測結果と考察
open-jdk-11.0.2で実行しました。実行結果の分布の箱ひげ図は以下のものになりました。

また、実行結果の平均値・中央値・最大値・最小値・95%の信頼区間と99%の信頼区間は以下のものです。
| (単位:ミリ秒) | 手法1-System.out.println複数 | 手法2-StringBuilder | 手法3-PrintWriter |
|---|---|---|---|
| 平均値 | 563.51 | 208 | 246.13 |
| 中央値 | 573.5 | 192 | 250.5 |
| 最大値 | 864 | 354 | 374 |
| 最小値 | 448 | 94 | 135 |
| 標準偏差 | 62.4157606 | 75.35840291 | 61.73168644 |
| 95%信頼区間 | 12.38464102 | 14.95274205 | 12.24890587 |
| 95%信頼区間の最大値 | 575.894641 | 222.952742 | 258.3789059 |
| 95%信頼区間の最小値 | 551.125359 | 193.047258 | 233.8810941 |
| 99%信頼区間 | 16.39290943 | 19.79217207 | 16.21324382 |
| 99%信頼区間の最大値 | 579.9029094 | 227.7921721 | 262.3432438 |
| 99%信頼区間の最小値 | 547.1170906 | 188.2078279 | 229.9167562 |
計測した環境ではSystem.out.printlnを複数呼び出した場合、他の手法と比較して約2倍以上かかってしまいました。最大でも800ミリ秒かかってしまうのでAtCoderの実行時間制限が2secの場合、致命傷になってしまいそうですね。System.out.printlnを複数回呼び出すのは避けた方がいいでしょう。
付録
実際に計測した生データが以下のものです。
| 手法1-System.out.println複数 | 手法2-StringBuilder | 手法3-PrintWriter |
|---|---|---|
| 572 | 141 | 154 |
| 595 | 99 | 141 |
| 558 | 94 | 135 |
| 567 | 100 | 144 |
| 589 | 99 | 143 |
| 603 | 102 | 152 |
| 583 | 109 | 164 |
| 578 | 108 | 169 |
| 550 | 114 | 152 |
| 552 | 110 | 172 |
| 586 | 128 | 140 |
| 597 | 120 | 143 |
| 566 | 117 | 165 |
| 864 | 118 | 162 |
| 583 | 119 | 163 |
| 594 | 122 | 160 |
| 480 | 124 | 178 |
| 487 | 124 | 169 |
| 463 | 128 | 174 |
| 487 | 132 | 175 |
| 472 | 135 | 174 |
| 503 | 140 | 191 |
| 475 | 133 | 192 |
| 480 | 147 | 197 |
| 713 | 138 | 197 |
| 477 | 143 | 206 |
| 474 | 147 | 191 |
| 505 | 146 | 194 |
| 448 | 160 | 201 |
| 488 | 150 | 213 |
| 471 | 155 | 202 |
| 539 | 342 | 205 |
| 561 | 160 | 205 |
| 613 | 156 | 216 |
| 584 | 154 | 215 |
| 592 | 166 | 208 |
| 590 | 167 | 228 |
| 507 | 164 | 218 |
| 668 | 160 | 232 |
| 620 | 164 | 235 |
| 598 | 171 | 227 |
| 593 | 168 | 240 |
| 641 | 170 | 234 |
| 649 | 179 | 229 |
| 610 | 180 | 242 |
| 606 | 182 | 257 |
| 631 | 186 | 249 |
| 642 | 180 | 248 |
| 626 | 180 | 236 |
| 605 | 190 | 251 |
| 566 | 186 | 244 |
| 467 | 194 | 270 |
| 466 | 195 | 270 |
| 478 | 194 | 258 |
| 493 | 198 | 269 |
| 573 | 205 | 250 |
| 588 | 204 | 258 |
| 583 | 197 | 260 |
| 576 | 197 | 269 |
| 572 | 258 | 290 |
| 512 | 206 | 273 |
| 543 | 282 | 265 |
| 545 | 244 | 269 |
| 563 | 248 | 274 |
| 584 | 257 | 272 |
| 586 | 271 | 277 |
| 583 | 288 | 282 |
| 580 | 284 | 292 |
| 597 | 270 | 299 |
| 605 | 275 | 278 |
| 556 | 294 | 290 |
| 582 | 280 | 291 |
| 597 | 280 | 290 |
| 545 | 290 | 281 |
| 574 | 291 | 295 |
| 629 | 293 | 292 |
| 604 | 291 | 313 |
| 620 | 298 | 295 |
| 504 | 234 | 299 |
| 455 | 234 | 300 |
| 485 | 241 | 316 |
| 549 | 238 | 313 |
| 504 | 248 | 324 |
| 591 | 242 | 307 |
| 627 | 308 | 313 |
| 539 | 247 | 314 |
| 558 | 284 | 307 |
| 524 | 304 | 316 |
| 547 | 301 | 348 |
| 621 | 328 | 334 |
| 593 | 317 | 335 |
| 596 | 321 | 315 |
| 539 | 325 | 321 |
| 604 | 336 | 325 |
| 548 | 315 | 319 |
| 606 | 325 | 343 |
| 547 | 326 | 374 |
| 616 | 339 | 338 |
| 518 | 354 | 348 |
| 478 | 342 | 350 |