¿printf seguirá teniendo un costo incluso si redirijo la salida a/dev/null?

11 minutos de lectura

avatar de usuario
Miguel

Tenemos un daemon que contiene muchos mensajes de impresión. Dado que estamos trabajando en un dispositivo integrado con una CPU débil y otro hardware limitado, queremos minimizar cualquier tipo de costo (IO, CPU, etc.) de los mensajes de impresión en nuestra versión final. (Los usuarios no tienen una consola)

Mi compañero de equipo y yo tenemos un desacuerdo. Cree que podemos redirigir todo a /dev/null. No te costará ningún IO por lo que las afectaciones serán mínimas. Pero creo que aún costará CPU y es mejor que definamos una macro para printf para que podamos reescribir “printf” (tal vez solo regrese).

Así que necesito algunas opiniones sobre quién tiene razón. ¿Linux será lo suficientemente inteligente como para optimizar printf? Realmente lo dudo.

  • Cuidado con los efectos secundarios: printf("%d", x=a+b); Si redirige a /dev/null ocurrirán efectos secundarios; si reescribes como hacer nada macro, los efectos secundarios se perderán

    – pmg

    15 de enero de 2019 a las 9:42


  • proporcionando un myprintf(...) { return; } es probablemente lo que quieres. Luego puede tener una macro para el reenvío de printf a ese método, conservando los efectos secundarios pero sin formatear ninguna cadena o llamar a escribir

    – msrd0

    15 de enero de 2019 a las 10:10

  • @pmg: Efectos secundarios en un printf declaración son malas. En la revisión del código, definitivamente plantearía un problema al respecto.

    – MSalters

    15 de enero de 2019 a las 14:37

  • Daría un paso atrás. En un demonio de Linux hay opciones mucho mejores que printf… Considere, por ejemplo, usar syslog en su lugar, si configura el nivel de registro al inicio (idealmente desde una variable de entorno) y puede dirigir los registros a un archivo o a otra máquina a través del red trivialmente, y el nivel de registro le permite desactivar el registro de cosas que no le interesan a un costo relativamente bajo en tiempo de ejecución. Aún mejor, si hace algo como atrapar algunas señales, puede cambiar el nivel de registro en tiempo de ejecución sin detenerse, y mucho menos tener que volver a compilar el daemon.

    – Dan Mills

    15 de enero de 2019 a las 19:33

  • Parece que necesita un marco de registro adecuado. Como mínimo, uno que admita la evaluación perezosa del mensaje.

    – Alejandro

    15 de enero de 2019 a las 23:48

avatar de usuario
iBug

bastante

Cuando redirige la salida estándar del programa a /dev/nullcualquier llamada a printf(3) aún evaluará todos los argumentos, y el proceso de formateo de cadenas aún tendrá lugar antes de llamar write(2), que escribe la cadena con formato completo en la salida estándar del proceso. Es en el nivel del núcleo donde los datos no se escriben en el disco, sino que el controlador asociado con el dispositivo especial los descarta. /dev/null.

Entonces, en el mejor de los casos, no pasará por alto ni evadirá la sobrecarga de evaluar los argumentos y pasárselos a printfel trabajo de formato de cadena detrás printfy al menos una llamada al sistema para escribir los datos, simplemente redirigiendo stdout a /dev/null. Bueno, esa es una verdadera diferencia en Linux. La implementación solo devuelve la cantidad de bytes que desea escribir (especificado por el tercer argumento de su llamada a write(2)) e ignora todo lo demás (ver esta respuesta). Según la cantidad de datos que esté escribiendo y la velocidad del dispositivo de destino (disco o terminal), la diferencia en el rendimiento puede variar mucho. En los sistemas integrados, en términos generales, cortar la escritura del disco redirigiendo a /dev/null puede ahorrar bastantes recursos del sistema para una cantidad no trivial de datos escritos.

Aunque en teoría, el programa podría detectar /dev/null y realizar algunas optimizaciones dentro de las restricciones de los estándares que cumplen (ISO C y POSIX), según la comprensión general de las implementaciones comunes, prácticamente no lo hacen (es decir, no tengo conocimiento de que ningún sistema Unix o Linux lo haga).

El estándar POSIX exige escribir en la salida estándar para cualquier llamada a printf(3)por lo que no cumple con los estándares suprimir la llamada a write(2) dependiendo de los descriptores de archivo asociados. Para obtener más detalles sobre los requisitos de POSIX, puede leer la respuesta de Damon. Ah, y una nota rápida: todas las distribuciones de Linux son prácticamente compatibles con POSIX, a pesar de no ser certificado ser tan.

Tenga en cuenta que si reemplaza printf completamente, algunos efectos secundarios pueden salir mal, por ejemplo printf("%d%n", a++, &b). Si realmente necesita suprimir la salida según el entorno de ejecución del programa, considere establecer un indicador global y termine printf para verificar el indicador antes de imprimir; no ralentizará el programa hasta el punto en que la pérdida de rendimiento sea visible. , como una verificación de condición única es mucho más rápido que llamar printf y haciendo todo el formato de cadena.

  • Las respuestas como esta deben indicar que se basan en la comprensión general de las implementaciones comunes y no en la documentación específica. En teoría, no hay motivo para que una implementación de C no inspeccione stdoutsepa que es /dev/null y suprima printf llamadas que no contienen %n y cuyo valor de retorno no se utiliza. Realmente no podemos afirmar que nadie haya hecho esto, y los estudiantes deben aprender la procedencia de la información, ya que una parte importante de la ingeniería es saber cómo se sabe algo (se especifica en un estándar, se supone que es demostrable, etc.) ).

    –Eric Postpischil

    15 de enero de 2019 a las 11:34

  • @EricPostpischil ¡Gracias por eso! Información muy valiosa.

    – iBug

    15 de enero de 2019 a las 11:39

  • @EricPostpischil Consulte la respuesta de Damon como referencia del estándar POSIX: suprimir un write(2) La llamada para escribir stdout está prohibida por el estándar.

    – iBug

    15 de enero de 2019 a las 13:29

  • AFAIK, la salida de la consola es bastante lenta, por lo que el tiempo ahorrado al escribir en /dev/null podría ser significativo. Sin embargo, sería menos efectivo si escribiera para archivar.

    – SJuan76

    15 de enero de 2019 a las 15:12

  • @ SJuan76 Escribir en una terminal no es necesariamente lento, mostrando lo es (es decir, depende de la terminal).

    – iBug

    15 de enero de 2019 a las 15:13


los printf función voluntad escribir a stdout. No se ajusta a optimizar para /dev/null. Por lo tanto, tendrá la sobrecarga de analizar la cadena de formato y evaluar los argumentos necesarios, y tendrá al menos una llamada al sistema, además de copiar un búfer en el espacio de direcciones del kernel (que, en comparación con el costo de la llamada al sistema, es insignificante) .

Esta respuesta se basa en la documentación específica de POSIX.

Interfaces del sistema

dprintf, fprintf, printf, snprintf, sprintf – salida con formato de impresión

La función fprintf() colocará la salida en el flujo de salida nombrado. La función printf() colocará la salida en el flujo de salida estándar stdout. La función sprintf() colocará la salida seguida del byte nulo, ‘\0’, en bytes consecutivos comenzando con *s; es responsabilidad del usuario asegurarse de que haya suficiente espacio disponible.

Definiciones básicas

deberá
Para una implementación que cumple con POSIX.1-2017, describe una característica o comportamiento que es obligatorio. Una aplicación puede confiar en la existencia de la función o el comportamiento.

  • El núcleo es responsable de copiar el búfer al espacio de direcciones del núcleo y el controlador nulo probablemente omite ese paso (en Linux, definitivamente lo hace, ni siquiera verifica que sea una dirección válida)

    – Aleatorio832

    15 de enero de 2019 a las 15:13

  • Tal vez sea un poco más claro, que no todas las llamadas a printf darán como resultado una llamada al sistema; en este momento, esto es un poco ambiguo, de lo contrario, es una gran respuesta.

    – Vu

    15 de enero de 2019 a las 19:46

  • -1, esto ignora la regla como si (que se aplica a la implementación completa, no solo el compilador). Si la implementación en su conjunto puede demostrar que no hay diferencia en el comportamiento observable, puede realizar cualquier optimización que desee, incluso después de que se haya completado la compilación. Ahora, si estuviéramos hablando de un lenguaje que no sea C, es posible que tengas razón.

    –Kevin

    15 de enero de 2019 a las 19:47

  • @Kevin hace la regla del como-si de la C norma aplicable a la POSIX estándar también?

    – Angew ya no está orgulloso de SO

    16 de enero de 2019 a las 12:31

  • @Kevin Dado que las llamadas a las funciones de E/S son un comportamiento observable en sí mismas, no está restringida la optimización. Si el estándar dijera que solo la E/S es un efecto observable, entonces sería posible eliminar printf a /dev/null.

    – jinawee

    16 de enero de 2019 a las 16:34

avatar de usuario
Un tipo programador

los printf la función escribe en stdout. Si el descriptor de archivo conectado a stdout se redirige a /dev/null entonces no se escribirá ninguna salida en ninguna parte (pero aún se escribirá), pero la llamada a printf sí mismo y el formateo que hace seguirán ocurriendo.

  • @OP: Adición: puede reducir aún más el costo de printf() mediante la creación de un nuevo controlador que proporciona una nueva FILE * (dependiendo de si su plataforma lo admite). En este caso, puede crear un sumidero de datos que descarte los datos. El costo de formatear, etc. aún permanece, pero la llamada del sistema operativo para escribir en /dev/null se va.

    – glglgl

    15 de enero de 2019 a las 9:48

  • Las respuestas como esta deben indicar que se basan en la comprensión general de las implementaciones comunes y no en la documentación específica. En teoría, no hay motivo para que una implementación de C no inspeccione stdoutsepa que es /dev/null y suprima printf llamadas que no contienen %n y cuyo valor de retorno no se utiliza. Realmente no podemos afirmar que nadie haya hecho esto, y los estudiantes deben aprender la procedencia de la información, ya que una parte importante de la ingeniería es saber cómo se sabe algo (se especifica en un estándar, se supone que es demostrable, etc.) ).

    –Eric Postpischil

    15 de enero de 2019 a las 11:32

avatar de usuario
MAXdB

Escriba el suyo propio que envuelva printf() usando la fuente printf() como guía, y regresando inmediatamente si se establece un indicador de noprint. La desventaja de esto es que cuando realmente se imprime consumirá más recursos debido a tener que analizar la cadena de formato dos veces. Pero utiliza recursos insignificantes cuando no se imprime. No se puede simplemente reemplazar printf() porque las llamadas subyacentes dentro de printf() pueden cambiar con una versión más nueva de la biblioteca stdio.

void printf2(const char *formatstring, …);

En términos generales, se permite que una implementación realice tales optimizaciones si no afectan los resultados observables (funcionales) del programa. En el caso de printf()eso significaría que si el programa no usa el valor de retorno, y si no hay %n conversiones, entonces la implementación no podría hacer nada.

En la práctica, no conozco ninguna implementación en Linux que actualmente (principios de 2019) realice tal optimización: los compiladores y las bibliotecas con las que estoy familiarizado formatearán la salida y escribirán el resultado en el dispositivo nulo, confiando en el kernel ‘ para ignorarlo.

Es posible que desee escribir una función de reenvío propia si realmente necesita ahorrar el costo de formatear cuando no se usa la salida; querrá que regrese voidy debe verificar la cadena de formato para %n. (Puedes usar snprintf con un NULL y 0 amortiguador si necesita esos efectos secundarios, pero es poco probable que los ahorros paguen el esfuerzo invertido).

  • Dado que el comportamiento observable de la máquina abstracta incluye llamadas a las funciones de E/S de la biblioteca, seguramente printf por lo menos habría que llamarlo no? Aunque no estoy seguro de cómo dos llamadas diferentes a una función de biblioteca se consideran diferentes o no…

    – jinawee

    16 de enero de 2019 a las 16:29


en escritura C 0; se ejecuta y nada, que es similar a ;.

significa que puedes escribir una macro como

#if DEBUG
#define devlognix(frmt,...) fprintf(stderr,(frmt).UTF8String,##__VA_ARGS__)
#else
#define nadanix 0
#define devlognix(frmt,...) nadanix
#endif
#define XYZKitLogError(frmt, ...) devlognix(frmt)

dónde XYZKitLogError sería su comando Log. o incluso

#define nadanix ;

que eliminará todas las llamadas de registro en tiempo de compilación y las reemplazará con 0; o ; por lo que se analiza.

conseguirás Variable sin usar advertencias, pero hace lo que quiere y este efecto secundario puede incluso ser útil porque le informa sobre el cálculo que no es necesario en su versión.

.UTF8String es un método de Objective-C que convierte NSStrings en const char*; no es necesario.

  • Dado que el comportamiento observable de la máquina abstracta incluye llamadas a las funciones de E/S de la biblioteca, seguramente printf por lo menos habría que llamarlo no? Aunque no estoy seguro de cómo dos llamadas diferentes a una función de biblioteca se consideran diferentes o no…

    – jinawee

    16 de enero de 2019 a las 16:29


¿Ha sido útil esta solución?