¿Qué tan malo es llamar a println () a menudo que concatenar cadenas juntas y llamarlo una vez?

23

Sé que la salida a la consola es una operación costosa. En aras de la legibilidad del código, a veces es bueno llamar a una función para generar texto dos veces, en lugar de tener una larga cadena de texto como argumento.

Por ejemplo, ¿cuánto menos eficiente es tener

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

vs.

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

En el ejemplo, la diferencia es solo una llamada, println()pero ¿y si es más?

En una nota relacionada, las declaraciones relacionadas con la impresión de texto pueden parecer extrañas mientras se visualiza el código fuente si el texto a imprimir es largo. Suponiendo que el texto en sí no se puede acortar, ¿qué se puede hacer? ¿Debería ser este un caso en el que println()se realicen múltiples llamadas? Una vez alguien me dijo que una línea de código no debería tener más de 80 caracteres (IIRC), entonces, ¿qué harías con

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.");

¿Es lo mismo para lenguajes como C / C ++ ya que cada vez que los datos se escriben en un flujo de salida se debe realizar una llamada al sistema y el proceso debe pasar al modo kernel (que es muy costoso)?

Celeritas
fuente
Aunque este es un código muy pequeño, debo decir que me he estado preguntando lo mismo. Sería bueno determinar la respuesta a esto de una vez por todas
Simon Forsberg
@ SimonAndréForsberg No estoy seguro de si es aplicable a Java porque se ejecuta en una máquina virtual, pero en lenguajes de nivel inferior como C / C ++ me imagino que sería costoso ya que cada vez que algo escribe en un flujo de salida, una llamada al sistema debe hacerse.
También debe considerar esto: stackoverflow.com/questions/21947452/…
hjk
1
Tengo que decir que no veo el punto aquí. Al interactuar con un usuario a través de la terminal, no puedo imaginar ningún problema de rendimiento porque generalmente no hay mucho para imprimir. Y las aplicaciones con una GUI o una aplicación web deben escribir en un archivo de registro (generalmente usando un marco).
Andy
1
Si dices buenos días, lo haces una o dos veces al día. La optimización no es una preocupación. Si se trata de algo más, necesita un perfil para saber si es un problema. El código que trabajo en el registro ralentiza el código a inutilizable a menos que construya un búfer de varias líneas y voltee el texto en una sola llamada.
mattnz

Respuestas:

29

Hay dos 'fuerzas' aquí, en tensión: Rendimiento vs. Legibilidad.

Sin embargo, abordemos primero el tercer problema, líneas largas:

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.");

La mejor manera de implementar esto y mantener la lectura es utilizar la concatenación de cadenas:

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.");

La concatenación de cadena constante ocurrirá en el momento de la compilación y no tendrá ningún efecto en el rendimiento. Las líneas son legibles y puedes seguir adelante.

Ahora, sobre el:

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

vs.

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

La segunda opción es significativamente más rápida. Sugeriré alrededor de 2X tan rápido ... ¿por qué?

Debido a que el 90% (con un amplio margen de error) del trabajo no está relacionado con el volcado de los caracteres a la salida, sino que es una sobrecarga necesaria para asegurar la salida para escribir en ella.

Sincronización

System.outes un PrintStream. Todas las implementaciones de Java que conozco, sincronizan internamente el PrintStream: ¡ Vea el código en GrepCode! .

¿Qué significa esto para tu código?

Significa que cada vez que llama System.out.println(...)está sincronizando su modelo de memoria, está verificando y esperando un bloqueo. Cualquier otro hilo que llame a System.out también estará bloqueado.

En las aplicaciones de un solo subproceso, el impacto de a System.out.println()menudo está limitado por el rendimiento de E / S de su sistema, qué tan rápido puede escribir en un archivo. En aplicaciones multiproceso, el bloqueo puede ser más problemático que el IO.

Enrojecimiento

Cada impresión se enjuaga . Esto hace que los búferes se borren y desencadena una escritura de nivel de consola en los búferes. La cantidad de esfuerzo realizada aquí depende de la implementación, pero, en general, se entiende que el rendimiento del vaciado solo está relacionado en una pequeña parte con el tamaño del búfer que se está vaciando. Hay una sobrecarga significativa relacionada con el vaciado, donde los búferes de memoria están marcados como sucios, la máquina virtual está realizando E / S, y así sucesivamente. Incurrir esa sobrecarga una vez, en lugar de dos, es una optimización obvia.

Algunos numeros

Realicé la siguiente pequeña prueba:

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);

    }


}

El código es relativamente simple, imprime repetidamente una cadena corta o larga para generar. La cadena larga tiene múltiples líneas nuevas. Mide cuánto tiempo lleva imprimir 1000 iteraciones de cada una.

Si lo ejecuto en el símbolo del sistema de Unix (Linux), y redirijo el STDOUTa /dev/nulle imprimo los resultados reales STDERR, puedo hacer lo siguiente:

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

La salida (en errlog) se ve así:

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

¿Qué significa esto? Permítanme repetir la última 'estrofa':

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

Significa que, para todos los efectos, aunque la línea 'larga' es aproximadamente 5 veces más larga y contiene varias líneas nuevas, la salida corta tarda casi tanto como la línea corta.

El número de caracteres por segundo a largo plazo es 5 veces mayor, y el tiempo transcurrido es casi el mismo .....

En otras palabras, su rendimiento escala en relación con el número de impresiones que tiene, no con lo que imprimen.

Actualización: ¿Qué sucede si redirige a un archivo, en lugar de a / 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

Es mucho más lento, pero las proporciones son casi las mismas ...

rolfl
fuente
Se agregaron algunos números de rendimiento.
rolfl
También debe considerar el problema que "\n"puede no ser el terminador de línea correcto. printlnautomáticamente terminará la línea con los caracteres correctos, pero pegar un \na su cadena directamente puede causar problemas. Si desea hacerlo bien, es posible que deba usar el formato de cadena o la line.separatorpropiedad del sistema . printlnEs mucho más limpio.
user2357112 es compatible con Monica
3
Todo esto es un gran análisis, por lo que es seguro +1, pero diría que una vez que se haya comprometido con la salida de la consola, estas pequeñas diferencias de rendimiento se saltan por la ventana. Si el algoritmo de su programa se ejecuta más rápido que la salida de los resultados (en este pequeño nivel de salida), puede imprimir cada carácter uno por uno y no notar la diferencia.
David Harkness
Creo que esta es una diferencia entre Java y C / C ++ que la salida está sincronizada. Digo esto porque recuerdo haber escrito un programa multiproceso y tener problemas con la salida confusa si diferentes hilos intentan escribir para escribir en la consola. ¿Alguien puede verificar esto?
66
También es importante recordar que nada de esa velocidad importa en absoluto cuando se coloca al lado de la función que espera la entrada del usuario.
vmrob 01 de
2

No creo que tener un montón de printlns sea un problema de diseño en absoluto. La forma en que lo veo es que esto se puede hacer claramente con el analizador de código estático si realmente es un problema.

Pero no es un problema porque la mayoría de las personas no hacen IOs como este. Cuando realmente necesitan hacer muchas E / S, usan las almacenadas en búfer (BufferedReader, BufferedWriter, etc.) cuando la entrada está almacenada, verá que el rendimiento es lo suficientemente similar, que no necesita preocuparse por tener un manojo de printlno pocos println.

Entonces para responder la pregunta original. Yo diría que no está mal si usas printlnpara imprimir algunas cosas como lo haría la mayoría de la gente println.

InformadoA
fuente
1

En lenguajes de nivel superior como C y C ++, esto es menos problemático que en Java.

En primer lugar, C y C ++ definen la concatenación de cadenas en tiempo de compilación, por lo que puede hacer algo como:

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.";

En tal caso, concatenar la cadena no es solo una optimización que puede basarse, generalmente (etc.) depende del compilador que realice. Más bien, es requerido directamente por los estándares C y C ++ (fase 6 de la traducción: "Los tokens literales de cadena adyacentes están concatenados").

Aunque es a expensas de una pequeña complejidad adicional en el compilador y la implementación, C y C ++ hacen un poco más para ocultar la complejidad de producir resultados de manera eficiente para el programador. Java es mucho más parecido al lenguaje ensamblador: cada llamada a se System.out.printlntraduce mucho más directamente a una llamada a la operación subyacente para escribir los datos en la consola. Si desea que el almacenamiento en búfer mejore la eficiencia, debe proporcionarlo por separado.

Esto significa, por ejemplo, que en C ++, reescribiendo el ejemplo anterior, a algo como esto:

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.";

... normalmente 1 casi no tienen efecto sobre la eficiencia. Cada uso de coutsimplemente depositaría datos en un búfer. Ese búfer se volcaría a la secuencia subyacente cuando se llenara el búfer, o el código intentara leer la entrada del uso (como con std::cin).

iostreams también tienen una sync_with_stdiopropiedad que determina si la salida de iostreams está sincronizada con la entrada de estilo C (por ejemplo, getchar) De forma predeterminada, sync_with_stdiose establece en verdadero, por lo que si, por ejemplo, escribe en std::cout, luego lee a través de getchar, los datos en los que escribió coutse enjuagarán cuando getcharse llame. Puede establecerlo sync_with_stdioen falso para deshabilitarlo (generalmente para mejorar el rendimiento).

sync_with_stdioTambién controla un grado de sincronización entre hilos. Si la sincronización está activada (el valor predeterminado), escribir en un iostream desde varios subprocesos puede provocar que los datos de los subprocesos se intercalen, pero evita cualquier condición de carrera. IOW, su programa se ejecutará y producirá salida, pero si más de un hilo escribe en una secuencia a la vez, la mezcla arbitraria de los datos de los diferentes hilos generalmente hará que la salida sea bastante inútil.

Si desactiva la sincronización, entonces sincronizar el acceso desde múltiples hilos también se convierte en su responsabilidad. Las escrituras concurrentes de múltiples hilos pueden / conducirán a una carrera de datos, lo que significa que el código tiene un comportamiento indefinido.

Resumen

C ++ por defecto es un intento de equilibrar la velocidad con la seguridad. El resultado es bastante exitoso para código de subproceso único, pero no tanto para el código de subproceso múltiple. El código multiproceso generalmente necesita garantizar que solo un hilo escriba en una secuencia a la vez para producir una salida útil.


1. Es posible desactivar el almacenamiento en búfer para una transmisión, pero en realidad hacerlo es bastante inusual, y cuando / si alguien lo hace, probablemente sea por una razón bastante específica, como garantizar que toda la salida se capture de inmediato a pesar del efecto en el rendimiento . En cualquier caso, esto solo sucede si el código lo hace explícitamente.

Jerry Coffin
fuente
13
" En lenguajes de nivel superior como C y C ++, esto es menos problemático que en Java " . ¿Qué? C y C ++ son lenguajes de nivel inferior que Java. Además, olvidó los terminadores de línea.
user2357112 es compatible con Monica
1
A lo largo, señalo que la base objetiva para Java es el lenguaje de nivel inferior. No estoy seguro de qué terminadores de línea estás hablando.
Jerry Coffin
2
Java también realiza la concatenación en tiempo de compilación. Por ejemplo, "2^31 - 1 = " + Integer.MAX_VALUEse almacena como una sola cadena interna (JLS Sec 3.10.5 y 15.28 ).
200_success
2
@ 200_success: Java haciendo la concatenación de cadenas en tiempo de compilación parece reducirse a §15.18.1: "El objeto String se acaba de crear (§12.5) a menos que la expresión sea una expresión constante en tiempo de compilación (§15.28)". Esto parece permitir pero no requiere que la concatenación se realice en tiempo de compilación. Es decir, el resultado debe crearse nuevamente si las entradas no son constantes de tiempo de compilación, pero no se exige en ninguna dirección si son constantes de tiempo de compilación. Para requerir la concatenación en tiempo de compilación, tendría que leer su "si" (implícito) como si realmente significara "si y solo si".
Jerry Coffin
2
@Phoshi: Probar con recursos ni siquiera es vagamente similar a RAII. RAII permite que la clase administre los recursos, pero probar con recursos requiere que el código del cliente administre los recursos. Las características (abstracciones, más exactamente) que tiene una y la otra carece son completamente relevantes; de hecho, son exactamente lo que hace que un idioma tenga un nivel más alto que otro.
Jerry Coffin
1

Si bien el rendimiento no es realmente un problema aquí, la mala legibilidad de un montón de printlndeclaraciones apunta a un aspecto de diseño que falta.

¿Por qué escribimos una secuencia de muchas printlndeclaraciones? Si fuera solo un bloque de texto fijo, como un --helptexto en un comando de consola, sería mucho mejor tenerlo como un recurso separado y leerlo y escribirlo en la pantalla a pedido.

Pero generalmente es una mezcla de partes dinámicas y estáticas. Digamos que tenemos algunos datos de pedidos desnudos, por un lado, y algunas partes fijas de texto estático, por otro lado, y estas cosas tienen que mezclarse para formar una hoja de confirmación de pedido. Una vez más, también en este caso, es mejor tener un archivo de texto de recursos separado: El recurso sería una plantilla, que contiene algún tipo de símbolos (marcadores de posición), que se reemplazan en tiempo de ejecución por los datos del pedido real.

La separación del lenguaje de programación del lenguaje natural tiene muchas ventajas, entre ellas está la internacionalización: es posible que deba traducir el texto si desea convertirse en multilingüe con su software. Además, ¿por qué debería ser necesario un paso de compilación si solo desea tener una corrección textual, por ejemplo, corregir algunos errores ortográficos?

rplantiko
fuente