Skip to content

Commit 6e20338

Browse files
author
Brian Burkhalter
committed
8358533: Improve performance of java.io.Reader.readAllLines
Reviewed-by: rriggs, sherman
1 parent 6249259 commit 6e20338

File tree

3 files changed

+198
-16
lines changed

3 files changed

+198
-16
lines changed

src/java.base/share/classes/java/io/Reader.java

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@
2727

2828
import java.nio.CharBuffer;
2929
import java.nio.ReadOnlyBufferException;
30+
import java.util.ArrayList;
31+
import java.util.Collections;
3032
import java.util.List;
3133
import java.util.Objects;
34+
import jdk.internal.util.ArraysSupport;
3235

3336
/**
3437
* Abstract class for reading character streams. The only methods that a
@@ -397,16 +400,6 @@ public int read(char[] cbuf) throws IOException {
397400
*/
398401
public abstract int read(char[] cbuf, int off, int len) throws IOException;
399402

400-
private String readAllCharsAsString() throws IOException {
401-
StringBuilder result = new StringBuilder();
402-
char[] cbuf = new char[TRANSFER_BUFFER_SIZE];
403-
int nread;
404-
while ((nread = read(cbuf, 0, cbuf.length)) != -1) {
405-
result.append(cbuf, 0, nread);
406-
}
407-
return result.toString();
408-
}
409-
410403
/**
411404
* Reads all remaining characters as lines of text. This method blocks until
412405
* all remaining characters have been read and end of stream is detected,
@@ -457,7 +450,57 @@ private String readAllCharsAsString() throws IOException {
457450
* @since 25
458451
*/
459452
public List<String> readAllLines() throws IOException {
460-
return readAllCharsAsString().lines().toList();
453+
List<String> lines = new ArrayList<>();
454+
char[] cb = new char[1024];
455+
456+
int start = 0;
457+
int pos = 0;
458+
int limit = 0;
459+
boolean skipLF = false;
460+
int n;
461+
while ((n = read(cb, pos, cb.length - pos)) != -1) {
462+
limit = pos + n;
463+
while (pos < limit) {
464+
if (skipLF) {
465+
if (cb[pos] == '\n') {
466+
pos++;
467+
start++;
468+
}
469+
skipLF = false;
470+
}
471+
while (pos < limit) {
472+
char c = cb[pos++];
473+
if (c == '\n' || c == '\r') {
474+
lines.add(new String(cb, start, pos - 1 - start));
475+
skipLF = (c == '\r');
476+
start = pos;
477+
break;
478+
}
479+
}
480+
if (pos == limit) {
481+
int len = limit - start;
482+
if (len >= cb.length/2) {
483+
// allocate larger buffer and copy chars to beginning
484+
int newLength = ArraysSupport.newLength(cb.length,
485+
TRANSFER_BUFFER_SIZE, cb.length);
486+
char[] tmp = new char[newLength];
487+
System.arraycopy(cb, start, tmp, 0, len);
488+
cb = tmp;
489+
} else if (start != 0 && len != 0) {
490+
// move fragment to beginning of buffer
491+
System.arraycopy(cb, start, cb, 0, len);
492+
}
493+
pos = limit = len;
494+
start = 0;
495+
break;
496+
}
497+
}
498+
}
499+
// add a string if EOS terminates the last line
500+
if (limit > start)
501+
lines.add(new String(cb, start, limit - start));
502+
503+
return Collections.unmodifiableList(lines);
461504
}
462505

463506
/**
@@ -499,7 +542,13 @@ public List<String> readAllLines() throws IOException {
499542
* @since 25
500543
*/
501544
public String readAllAsString() throws IOException {
502-
return readAllCharsAsString();
545+
StringBuilder result = new StringBuilder();
546+
char[] cbuf = new char[TRANSFER_BUFFER_SIZE];
547+
int nread;
548+
while ((nread = read(cbuf, 0, cbuf.length)) != -1) {
549+
result.append(cbuf, 0, nread);
550+
}
551+
return result.toString();
503552
}
504553

505554
/** Maximum skip-buffer size */

test/jdk/java/io/Reader/ReadAll.java

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,55 @@ public static void setup() throws IOException {
6666
int size = rnd.nextInt(2, 16386);
6767

6868
int plen = PHRASE.length();
69-
List<String> strings = new ArrayList<String>(size);
69+
StringBuilder sb = new StringBuilder(plen);
70+
List<String> strings = new ArrayList<>(size);
7071
while (strings.size() < size) {
7172
int fromIndex = rnd.nextInt(0, plen / 2);
7273
int toIndex = rnd.nextInt(fromIndex, plen);
73-
strings.add(PHRASE.substring(fromIndex, toIndex));
74+
String s = PHRASE.substring(fromIndex, toIndex);
75+
sb.append(s);
76+
int bound = toIndex - fromIndex;
77+
if (bound > 0) {
78+
int offset = bound/2;
79+
int n = rnd.nextInt(0, bound);
80+
for (int i = 0; i < n; i++) {
81+
String f = null;
82+
switch (rnd.nextInt(7)) {
83+
case 0 -> f = "";
84+
case 1 -> f = "\r";
85+
case 2 -> f = "\n";
86+
case 3 -> f = "\r\n";
87+
case 4 -> f = "\r\r";
88+
case 5 -> f = "\n\n";
89+
case 6 -> f = " ";
90+
}
91+
sb.insert(offset, f);
92+
}
93+
}
94+
strings.add(sb.toString());
95+
sb.setLength(0);
7496
}
97+
98+
String p4096 = PHRASE.repeat((4096 + plen - 1)/plen);
99+
String p8192 = PHRASE.repeat((8192 + plen - 1)/plen);
100+
String p16384 = PHRASE.repeat((16384 + plen - 1)/plen);
101+
102+
for (int i = 0; i < 64; i++) {
103+
for (int j = 0; j < 32; j++) {
104+
switch (rnd.nextInt(8)) {
105+
case 0 -> sb.append("");
106+
case 1 -> sb.append(" ");
107+
case 2 -> sb.append("\n");
108+
case 3 -> sb.append(PHRASE);
109+
case 5 -> sb.append(p4096);
110+
case 6 -> sb.append(p8192);
111+
case 7 -> sb.append(p16384);
112+
}
113+
}
114+
strings.add(sb.toString());
115+
sb.setLength(0);
116+
}
117+
75118
Files.write(path, strings);
76119
System.out.println(strings.size() + " lines written");
77120
}
@@ -85,16 +128,29 @@ public static void cleanup() throws IOException {
85128
@Test
86129
public void readAllLines() throws IOException {
87130
// Reader implementation
131+
System.out.println("Reader implementation");
88132
List<String> lines;
89133
try (FileReader fr = new FileReader(file)) {
90134
lines = fr.readAllLines();
91135
}
92136
System.out.println(lines.size() + " lines read");
93137

94138
List<String> linesExpected = Files.readAllLines(path);
95-
assertEquals(linesExpected, lines);
139+
int count = linesExpected.size();
140+
if (lines.size() != count)
141+
throw new RuntimeException("Size mismatch: " + lines.size() + " != " + count);
142+
for (int i = 0; i < count; i++) {
143+
String expected = linesExpected.get(i);
144+
String actual = lines.get(i);
145+
if (!actual.equals(expected)) {
146+
String msg = String.format("%d: \"%s\" != \"%s\"",
147+
i, actual, expected);
148+
throw new RuntimeException(msg);
149+
}
150+
}
96151

97152
// Reader.of implementation
153+
System.out.println("Reader.of implementation");
98154
String stringExpected = Files.readString(path);
99155
int n = rnd.nextInt(stringExpected.length()/2);
100156
String substringExpected = stringExpected.substring(n);
@@ -103,7 +159,18 @@ public void readAllLines() throws IOException {
103159
r.skip(n);
104160
lines = r.readAllLines();
105161
}
106-
assertEquals(linesExpected, lines);
162+
count = linesExpected.size();
163+
if (lines.size() != count)
164+
throw new RuntimeException("Size mismatch: " + lines.size() + " != " + count);
165+
for (int i = 0; i < count; i++) {
166+
String expected = linesExpected.get(i);
167+
String actual = lines.get(i);
168+
if (!actual.equals(expected)) {
169+
String msg = String.format("%d: \"%s\" != \"%s\"",
170+
i, actual, expected);
171+
throw new RuntimeException(msg);
172+
}
173+
}
107174
}
108175

109176
@Test
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
package org.openjdk.bench.java.io;
24+
25+
import java.io.CharArrayReader;
26+
import java.io.IOException;
27+
import java.io.Reader;
28+
import java.util.List;
29+
import java.util.Random;
30+
31+
import org.openjdk.jmh.annotations.Benchmark;
32+
import org.openjdk.jmh.annotations.Scope;
33+
import org.openjdk.jmh.annotations.Setup;
34+
import org.openjdk.jmh.annotations.State;
35+
36+
@State(Scope.Benchmark)
37+
public class ReaderReadAllLines {
38+
39+
private char[] chars = null;
40+
41+
@Setup
42+
public void setup() throws IOException {
43+
final int len = 128_000;
44+
chars = new char[len];
45+
Random rnd = new Random(System.nanoTime());
46+
int off = 0;
47+
while (off < len) {
48+
int lineLen = 40 + rnd.nextInt(8192);
49+
if (lineLen > len - off) {
50+
off = len;
51+
} else {
52+
chars[off + lineLen] = '\n';
53+
off += lineLen;
54+
}
55+
}
56+
}
57+
58+
@Benchmark
59+
public List<String> readAllLines() throws IOException {
60+
List<String> lines;
61+
try (Reader reader = new CharArrayReader(chars)) {
62+
lines = reader.readAllLines();
63+
}
64+
return lines;
65+
}
66+
}

0 commit comments

Comments
 (0)