scanner example definicion java bufferedreader benchmarking

example - filereader java



¿Por qué BufferedReader read() es mucho más lento que readLine()? (6)

Así que esta es la respuesta práctica a mi propia pregunta: No use BufferedReader.read() use FileChannel en FileChannel lugar. (Obviamente no respondo el POR QUÉ puse el título). Aquí está el punto de referencia rápido y sucio, con suerte otros lo encontrarán útil:

@Test public void testFileChannel() throws IOException{ FileChannel fileChannel = FileChannel.open(Paths.get("chr1.fa")); long n= 0; int noOfBytesRead = 0; long t0= System.nanoTime(); while(noOfBytesRead != -1){ ByteBuffer buffer = ByteBuffer.allocate(10000); noOfBytesRead = fileChannel.read(buffer); buffer.flip(); while ( buffer.hasRemaining() ) { char x= (char)buffer.get(); n++; } } long t1= System.nanoTime(); System.err.println((float)(t1-t0) / 1e6); // ~ 250 ms System.err.println("nchars: " + n); // 254235640 chars read }

Con ~ 250 ms para leer todo el archivo char por char, esta estrategia es considerablemente más rápida que BufferedReader.readLine() (~ 700 ms), y mucho menos read() . Agregar declaraciones if en el ciclo para verificar x == ''/n'' y x == ''>'' hace poca diferencia. También poner un StringBuilder para reconstruir líneas no afecta demasiado el tiempo. Así que esto es muy bueno para mí (al menos por ahora).

Gracias a @ Marco13 por mencionar FileChannel.

Necesito leer un archivo de a un carácter a la vez y estoy usando el método read() de BufferedReader . *

Descubrí que read() es aproximadamente 10 veces más lento que readLine() . Es esto esperado? ¿O estoy haciendo algo mal?

Aquí hay un punto de referencia con Java 7. El archivo de prueba de entrada tiene alrededor de 5 millones de líneas y 254 millones de caracteres (~ 242 MB) **:

El método read() tarda unos 7000 ms para leer todos los caracteres:

@Test public void testRead() throws IOException, UnindexableFastaFileException{ BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa"))); long t0= System.currentTimeMillis(); int c; while( (c = fa.read()) != -1 ){ // } long t1= System.currentTimeMillis(); System.err.println(t1-t0); // ~ 7000 ms }

El método readLine() toma solo ~ 700 ms:

@Test public void testReadLine() throws IOException{ BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa"))); String line; long t0= System.currentTimeMillis(); while( (line = fa.readLine()) != null ){ // } long t1= System.currentTimeMillis(); System.err.println(t1-t0); // ~ 700 ms }

* Objetivo práctico : necesito saber la longitud de cada línea, incluidos los caracteres de nueva línea ( /n o /r/n ) Y la longitud de la línea después de quitarlos. También necesito saber si una línea comienza con el carácter > . Para un archivo dado, esto se hace solo una vez al comienzo del programa. Dado que BufferedReader.readLine() no devuelve caracteres EOL, estoy recurriendo al método read() . Si hay mejores formas de hacerlo, por favor diga.

** El archivo gzip está aquí http://hgdownload.cse.ucsc.edu/goldenpath/hg19/chromosomes/chr1.fa.gz . Para aquellos que pueden estar preguntándose, estoy escribiendo una clase para indexar archivos Fasta.


Gracias @Voo por la corrección. Lo que mencioné a continuación es correcto desde FileReader#read() v / s BufferedReader#readLine() punto de vista PERO no es correcto desde BufferedReader#read() v / s BufferedReader#readLine() punto de vista, por lo que he atacado la respuesta.

Usar el método read() en BufferedReader no es una buena idea, no te haría ningún daño, pero ciertamente desperdicia el propósito de la clase.

Todo el propósito en la vida de BufferedReader es reducir la E / S almacenando en búfer el contenido. Puedes leer here en los tutoriales de Java. También puede observar que el método read() en BufferedReader en realidad se hereda de Reader mientras que readLine() es el propio método de BufferedReader .

Si desea utilizar el método read() , le diría que es mejor utilizar FileReader , que es para ese propósito. Puedes read aquí en los tutoriales de Java.

Por lo tanto, creo que la respuesta a su pregunta es muy simple (sin entrar en el benchmarking y todas las explicaciones) -

  • Cada read() es manejada por el SO subyacente y desencadena el acceso al disco, la actividad de la red u otra operación que es relativamente costosa.
  • Cuando utiliza readLine() entonces guarda todos estos gastos generales, por lo que readLine() siempre será más rápido que read() , puede no ser sustancialmente para datos pequeños, sino más rápido.

Java JIT optimiza los bucles de bucles vacíos, por lo que sus bucles en realidad se ven así:

while((c = fa.read()) != -1);

y

while((line = fa.readLine()) != null);

Te sugiero que leas sobre el benchmarking here y la optimización de los loops here .

En cuanto a por qué el tiempo tomado difiere:

  • Motivo uno (Esto solo se aplica si los cuerpos de los bucles contienen código): en el primer ejemplo, estás haciendo una operación por línea, en el segundo, estás haciendo una operación por personaje. Esto suma las líneas / caracteres que tienes.

    while((c = fa.read()) != -1){ //One operation per character. } while((line = fa.readLine()) != null){ //One operation per line. }

  • Razón dos: en la clase BufferedReader , el método readLine() no usa read() detrás de las escenas: usa su propio código. El método readLine() realiza menos operaciones por carácter para leer una línea, que tomaría leer una línea con el método read() ; esta es la razón readLine() cual readLine() es más rápido al leer un archivo completo.

  • Razón tres: se requieren más iteraciones para leer cada carácter, que leer cada línea (a menos que cada carácter esté en una nueva línea); read() se llama más veces que readLine() .


Lo importante al analizar el rendimiento es tener un punto de referencia válido antes de comenzar. Comencemos con un punto de referencia de JMH simple que muestre cuál sería nuestro rendimiento esperado después del calentamiento.

Una cosa que tenemos que considerar es que, dado que a los sistemas operativos modernos les gusta almacenar en caché los datos de los archivos a los que se accede regularmente, necesitamos alguna manera de borrar los cachés entre las pruebas. En Windows hay una pequeña utilidad pequeña que hace justamente esto : en Linux deberías poder hacerlo escribiendo en algún pseudo archivo en alguna parte.

El código se ve así:

import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Mode; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; @BenchmarkMode(Mode.AverageTime) @Fork(1) public class IoPerformanceBenchmark { private static final String FILE_PATH = "test.fa"; @Benchmark public int readTest() throws IOException, InterruptedException { clearFileCaches(); int result = 0; try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) { int value; while ((value = reader.read()) != -1) { result += value; } } return result; } @Benchmark public int readLineTest() throws IOException, InterruptedException { clearFileCaches(); int result = 0; try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) { String line; while ((line = reader.readLine()) != null) { result += line.chars().sum(); } } return result; } private void clearFileCaches() throws IOException, InterruptedException { ProcessBuilder pb = new ProcessBuilder("EmptyStandbyList.exe", "standbylist"); pb.inheritIO(); pb.start().waitFor(); } }

y si lo ejecutamos con

chcp 65001 # set codepage to utf-8 mvn clean install; java "-Dfile.encoding=UTF-8" -server -jar ./target/benchmarks.jar

obtenemos los siguientes resultados (se necesitan aproximadamente 2 segundos para borrar los cachés y estoy ejecutando esto en un HDD, por eso es mucho más lento que para ti):

Benchmark Mode Cnt Score Error Units IoPerformanceBenchmark.readLineTest avgt 20 3.749 ± 0.039 s/op IoPerformanceBenchmark.readTest avgt 20 3.745 ± 0.023 s/op

¡Sorpresa! Como se esperaba, no hay diferencia de rendimiento aquí después de que la JVM se haya establecido en un modo estable. Pero hay un caso atípico en el método readCharTest:

# Warmup Iteration 1: 6.186 s/op # Warmup Iteration 2: 3.744 s/op

que es exactamente el problema que está viendo. La razón más probable que puedo pensar es que OSR no está haciendo un buen trabajo aquí o que el JIT solo se está ejecutando demasiado tarde para hacer una diferencia en la primera iteración.

Dependiendo de su caso de uso, esto podría ser un gran problema o insignificante (si está leyendo miles de archivos, no tendrá importancia, si solo está leyendo uno, este es un problema).

Resolver tal problema no es fácil y no hay soluciones generales, aunque hay formas de manejar esto. Una prueba sencilla para ver si estamos en el camino correcto es ejecutar el código con la opción -Xcomp que obliga a HotSpot a compilar todos los métodos en la primera invocación. Y, de hecho, al hacerlo, hace que desaparezca la gran demora en la primera invocación:

# Warmup Iteration 1: 3.965 s/op # Warmup Iteration 2: 3.753 s/op

Solución posible

Ahora que tenemos una buena idea de cuál es el problema real (supongo que todos estos bloqueos no se combinan ni utilizan la implementación de bloqueos sesgados eficientes), la solución es bastante simple y directa: reducir el número de llamadas a funciones (así que sí podríamos haber llegado a esta solución sin todo lo anterior, pero siempre es bueno tener un buen control del problema y puede haber una solución que no implique cambiar mucho código).

El siguiente código se ejecuta consistentemente más rápido que cualquiera de los otros dos: puede jugar con el tamaño de la matriz pero es sorprendentemente poco importante (presumiblemente porque contrario a los otros métodos read(char[]) no tiene que adquirir un bloqueo así que el costo por llamada es más bajo para empezar).

private static final int BUFFER_SIZE = 256; private char[] arr = new char[BUFFER_SIZE]; @Benchmark public int readArrayTest() throws IOException, InterruptedException { clearFileCaches(); int result = 0; try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) { int charsRead; while ((charsRead = reader.read(arr)) != -1) { for (int i = 0; i < charsRead; i++) { result += arr[i]; } } } return result; }

Probablemente esto sea lo suficientemente bueno en cuanto al rendimiento, pero si desea mejorar el rendimiento aún más utilizando una asignación de archivos podría (no contaría con una mejora demasiado grande en un caso como este, pero si sabe que su texto siempre es ASCII , podría hacer algunas optimizaciones adicionales) para ayudar a mejorar el rendimiento.


No es sorprendente ver esta diferencia si lo piensas. Una prueba es iterar las líneas en un archivo de texto, mientras que la otra itera caracteres.

A menos que cada línea contenga un carácter, se espera que readLine() sea ​​mucho más rápido que el método read() (aunque como se señala en los comentarios anteriores, es discutible ya que un BufferedReader almacena la entrada, mientras que la lectura física podría no ser la única operación de toma de rendimiento)

Si realmente quiere probar la diferencia entre los 2, sugeriría una configuración donde itere sobre cada personaje en ambas pruebas. Por ejemplo, algo como:

void readTest(BufferedReader r) { int c; StringBuilder b = new StringBuilder(); while((c = r.read()) != -1) b.append((char)c); } void readLineTest(BufferedReader r) { String line; StringBuilder b = new StringBuilder(); while((line = b.readLine())!= null) for(int i = 0; i< line.length; i++) b.append(line.charAt(i)); }

Además de lo anterior, utilice una "herramienta de diagnóstico del rendimiento de Java" para comparar su código. Además, lea sobre cómo microbenchmark código de Java .


De acuerdo con la documentación:

Cada llamada al método read() hace una llamada costosa al sistema.

Sin embargo, cada llamada al método readLine() aún hace una costosa llamada al sistema para más bytes a la vez, por lo que hay menos llamadas.

Situación similar ocurre cuando hacemos el comando de update base de datos para cada registro que queremos actualizar, frente a una actualización por lotes, donde hacemos una llamada para todos los registros.