Registro rápido

En este artículo he colocado puntos de referencia de las llamadas más privadas de los madereros. Realicé todos los experimentos en log4net y NLog, en Intel Windows 10 x64 con M.2 SSD.


Los resultados sin procesar se pueden ver en GitHub . El código está en el mismo repositorio (para ejecutar, necesitará .Net 4.7.2 + Microsoft Visual Studio 2017+).


Qué, cómo y por qué, debajo del corte.


Para no leer durante mucho tiempo, la tabla de resultados:


MétodoMediaErrorStddev
KeepFileOpen = true, ConcurrentWrites = false, Async = true1.144,677 ns26.3805 ns77.7835 ns
KeepFileOpen = true, ConcurrentWrites = true, Async = true1,106.691 ns31.4041 ns87.5421 ns
KeepFileOpen = false, ConcurrentWrites = false, Async = true4,804.426 ns110.3406 ns103.2126 ns
KeepFileOpen = false, ConcurrentWrites = true, Async = true5,303.602 ns104.3022 ns102.4387 ns
KeepFileOpen = true, ConcurrentWrites = false, Async = false5,642.301 ns73,2291 ns68.4986 ns
KeepFileOpen = true, ConcurrentWrites = true, Async = false11,834.892 ns82.7578 ns77,4117 ns
KeepFileOpen = false, ConcurrentWrites = false, Async = false731,250.539 ns14,612.0117 ns27,444.8998 ns
KeepFileOpen = false, ConcurrentWrites = true, Async = false730,271.927 ns11,330.0172 ns10,598.1051 ns
CreateLog4NetFromString1,470.662 ns19.9492 ns18.6605 ns
CreateNLogFromString228.774 ns2.1315 ns1.8895 ns
CreateLog4NetLogger21,046.294 ns284.1171 ns265.7633 ns
CreateNLogTypeOfLogger164,487.931 ns3,240.4372 ns3,031.1070 ns
CreateNLogDynamicLogger134,459.092 ns1,882.8663 ns1,761.2344 ns
FileLoggingLog4NetNoParams8.251.032 ns109.3075 ns102.2463 ns
FileLoggingLog4NetSingleReferenceParam8.260.452 ns145.9028 ns136.4776 ns
FileLoggingLog4NetSingleValueParam8.378.693 ns121.3003 ns113.4643 ns
FileLoggingLog4NetMultipleReferencesParam9.133.136 ns89.7420 ns79.5539 ns
FileLoggingLog4NetMultipleValuesParam9.393.989 ns166.0347 ns155.3089 ns
FileLoggingNLogNetNoParams6.061.837 ns69.5666 ns65.0726 ns
FileLoggingNLogNetSingleReferenceParam6.458.201 ns94.5617 ns88.4530 ns
FileLoggingNLogNetSingleValueParam6.460.859 ns95.5435 ns84,6969 ns
FileLoggingNLogNetMultipleReferencesParam7.236.886 ns89.7334 ns83,9367 ns
FileLoggingNLogNetMultipleValuesParam7.524.876 ns82.8979 ns77,5427 ns
NoOpLog4NetNoParams12.684 ns0.0795 ns0,0743 ns
NoOpLog4NetSingleReferenceParam10.506 ns0,0571 ns0,0506 ns
NoOpLog4NetSingleValueParam12.608 ns0.1012 ns0.0946 ns
NoOpLog4NetMultipleReferencesParam48.858 ns0.3988 ns0.3730 ns
NoOpLog4NetMultipleValuesParam69,463 ns0.9444 ns0.8834 ns
NoOpNLogNetNoParams2.073 ns0,0253 ns0.0225 ns
NoOpNLogNetSingleReferenceParam2.625 ns0,0364 ns0.0340 ns
NoOpNLogNetSingleValueParam2.281 ns0,0222 ns0.0208 ns
NoOpNLogNetMultipleReferencesParam41.525 ns0.4481 ns0.4191 ns
NoOpNLogNetMultipleValuesParam57.622 ns0.5341 ns0.4996 ns

NoOpLogging


Primero, calculemos cuánto tiempo pasamos llamando a un método para iniciar sesión, que al final no conducirá a nada. En la mayoría de los casos (en mi experiencia), la depuración detallada está deshabilitada en los servidores de batalla, pero nadie elimina las llamadas.


Primero, el resultado:


MétodoMediaErrorStddev
NoOpLog4NetNoParams12.684 ns0.0795 ns0,0743 ns
NoOpLog4NetSingleReferenceParam10.506 ns0,0571 ns0,0506 ns
NoOpLog4NetSingleValueParam12.608 ns0.1012 ns0.0946 ns
NoOpLog4NetMultipleReferencesParam48.858 ns0.3988 ns0.3730 ns
NoOpLog4NetMultipleValuesParam69,463 ns0.9444 ns0.8834 ns
NoOpNLogNetNoParams2.073 ns0,0253 ns0.0225 ns
NoOpNLogNetSingleReferenceParam2.625 ns0,0364 ns0.0340 ns
NoOpNLogNetSingleValueParam2.281 ns0,0222 ns0.0208 ns
NoOpNLogNetMultipleReferencesParam41.525 ns0.4481 ns0.4191 ns
NoOpNLogNetMultipleValuesParam57.622 ns0.5341 ns0.4996 ns

Y el código:


void Log4NetNoParams() => _log4Net.Debug("test"); void Log4NetSingleReferenceParam() => _log4Net.DebugFormat("test {0}", _stringArgument); void Log4NetSingleValueParam() => _log4Net.DebugFormat("test {0}", _intArgument); void Log4NetMultipleReferencesParam() => _log4Net.DebugFormat( "test {0} {1} {2} {3} {4} {5} {6} {7} {8}", _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument); void Log4NetMultipleValuesParam() => _log4Net.DebugFormat( "test {0} {1} {2} {3} {4} {5} {6} {7} {8}", _intArgument, _intArgument, _intArgument, _intArgument, _intArgument, _intArgument, _intArgument, _intArgument, _intArgument); void NLogNetNoParams() => _nlog.Debug("test"); void NLogNetSingleReferenceParam() => _nlog.Debug("test {0}", _stringArgument); void NLogNetSingleValueParam() => _nlog.Debug("test {0}", _intArgument); void NLogNetMultipleReferencesParam() => _nlog.Debug( "test {0} {1} {2} {3} {4} {5} {6} {7} {8}", _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument, _stringArgument); void NLogNetMultipleValuesParam() => _nlog.Debug( "test {0} {1} {2} {3} {4} {5} {6} {7} {8}", _intArgument, _intArgument, _intArgument, _intArgument, _intArgument, _intArgument, _intArgument, _intArgument, _intArgument); 

Primero, determinemos por qué se eligieron tales pruebas:


  • Los experimentos se realizaron en las bibliotecas más populares.
  • NLog y log4net tienen diferentes firmas de función para una pequeña cantidad de argumentos:


    • log4net:

     void DebugFormat(string format, object arg0) 

    • Nlog:

     void Debug(string message, string argument) void Debug<TArgument>(string message, TArgument argument) 

    • Teoría: cuando se transfiere un tipo significativo a log4net, debe producirse un boxeo, que simplemente pasa el tiempo del procesador y no conduce a nada. En el caso de NLog, no existe tal comportamiento, por lo que este último debería funcionar más rápido.

  • Las firmas para una gran cantidad de argumentos en las bibliotecas son aproximadamente las mismas, por lo que me gustaría saber:
    • ¿Cuánto más eficiente es invocar métodos con un pequeño número de parámetros?
    • ¿Hay alguna diferencia en la velocidad de llamar al método "Está ... habilitado" entre las dos bibliotecas

Y ahora el análisis de los resultados:


  • Debido al uso de argumentos genéricos en NLog, funciona más rápido para el caso cuando el registro directo no es necesario. Es decir, en el caso de que en su programa de depuración el nivel esté habilitado solo en el sistema de prueba, solo cambiar la biblioteca puede acelerar el software (y mejorar la vida de los usuarios).
  • Si tiene el registro desactivado y desea llamar a un método con una gran cantidad de argumentos, es más eficiente dividirlo en dos. Debido a esto, las llamadas a métodos anteriores funcionarán diez veces más rápido.
  • Cuando escribe una función que puede tomar cualquier objeto, a menudo es más efectivo confundirse y hacer una función genérica. Debido a una optimización tan simple, el código funcionará más rápido (esto se ve claramente en la diferencia de tiempo entre las llamadas a Log4NetSingleReferenceParam y Log4NetSingleValueParam )

Registro de archivos


La mayoría de los programas (de acuerdo con mis observaciones) todavía registran los resultados en un archivo, por lo que para la comparación elegimos esta operación. Para simplificar, simplemente tomamos la configuración de los registradores cuando un archivo se escribe en el archivo sin almacenamiento en búfer, sin bloqueos adicionales, etc.


Resultados:


MétodoMediaErrorStddev
FileLoggingLog4NetNoParams8.251.032 ns109.3075 ns102.2463 ns
FileLoggingLog4NetSingleReferenceParam8.260.452 ns145.9028 ns136.4776 ns
FileLoggingLog4NetSingleValueParam8.378.693 ns121.3003 ns113.4643 ns
FileLoggingLog4NetMultipleReferencesParam9.133.136 ns89.7420 ns79.5539 ns
FileLoggingLog4NetMultipleValuesParam9.393.989 ns166.0347 ns155.3089 ns
FileLoggingNLogNetNoParams6.061.837 ns69.5666 ns65.0726 ns
FileLoggingNLogNetSingleReferenceParam6.458.201 ns94.5617 ns88.4530 ns
FileLoggingNLogNetSingleValueParam6.460.859 ns95.5435 ns84,6969 ns
FileLoggingNLogNetMultipleReferencesParam7.236.886 ns89.7334 ns83,9367 ns
FileLoggingNLogNetMultipleValuesParam7.524.876 ns82.8979 ns77,5427 ns

Código utilizado:


  • log4net:

 var roller = new RollingFileAppender(); roller.ImmediateFlush = true; roller.RollingStyle = RollingFileAppender.RollingMode.Once; roller.MaxFileSize = 128 * 1000 * 1000; 

  • Nlog:

 new FileTarget($"target_{_logIndex++}") { ArchiveAboveSize = 128 * 1000 * 1000, MaxArchiveFiles = 16, AutoFlush = true, ConcurrentWrites = false, KeepFileOpen = false }; 

Como puede ver, la configuración de los registradores es más o menos similar y según los resultados:


  • NLog es un poco más rápido que log4net, en algún lugar alrededor del 15%.
  • Según las pruebas, resultó que es más eficiente registrar menos parámetros. Sin embargo, no se debe olvidar que con un mayor número de parámetros, la cadena resultante también se expandió. Por lo tanto, la tabla solo compara correctamente NLog con log4net.

NLog - diferentes tipos de cerraduras


MétodoMediaErrorStddev
KeepFileOpen = true, ConcurrentWrites = false, Async = false5,642.301 ns73,2291 ns68.4986 ns
KeepFileOpen = true, ConcurrentWrites = true, Async = false11,834.892 ns82.7578 ns77,4117 ns
KeepFileOpen = false, ConcurrentWrites = false, Async = false731,250.539 ns14,612.0117 ns27,444.8998 ns
KeepFileOpen = false, ConcurrentWrites = true, Async = false730,271.927 ns11,330.0172 ns10,598.1051 ns

Código fuente:


 new FileTarget($"target_{_logIndex++}") { ArchiveAboveSize = 128 * 1000 * 1000, MaxArchiveFiles = 16, AutoFlush = true, ConcurrentWrites = XXXXX, KeepFileOpen = YYYYY }; 

Si ponemos todas las combinaciones posibles en lugar de XXXXX y AAAA, obtenemos la prueba de la tabla.


Los resultados son bastante predecibles:


  • Si habilita ConcurrentWrites, entonces el sistema tomará constantemente y le dará Mutex, que no es gratuito. Pero, como vemos, escribir una línea en un archivo es aproximadamente equivalente a un bloqueo del sistema.
  • Cerrar y abrir un archivo, como vemos, afecta aún más el rendimiento del sistema. En los ejemplos con KeepFileOpen=true para cada operación de registro, creamos un archivo (junto con Handle), escribimos en el disco, llamado Flush, devolvimos Handle y también realizamos muchas operaciones de capó del motor. Como resultado, la velocidad cae cientos de veces.

Registro asincrónico y diferentes métodos de bloqueo.


La biblioteca NLog también puede realizar todas las operaciones de E / S en otro subproceso, liberando inmediatamente el actual. Y lo hace de manera competente, preservando el orden de los eventos, colocando todos los datos en bloques, y en cada bloque un número entero es un número de evento (para que no se obtengan líneas recortadas), y así sucesivamente.


Los resultados de diferentes métodos sin bloqueo:


MétodoMediaErrorStddev
KeepFileOpen = true, ConcurrentWrites = false, Async = true1.144,677 ns26.3805 ns77.7835 ns
KeepFileOpen = true, ConcurrentWrites = true, Async = true1,106.691 ns31.4041 ns87.5421 ns
KeepFileOpen = false, ConcurrentWrites = false, Async = true4,804.426 ns110.3406 ns103.2126 ns
KeepFileOpen = false, ConcurrentWrites = true, Async = true5,303.602 ns104.3022 ns102.4387 ns

La comparación de los enfoques de bloqueo y asincrónicos será más profunda, pero aquí, solo la última.


Código AsyncTargetWrapper :


 new AsyncTargetWrapper(fileTargetWithConcurrentWritesAndCloseFileAsync) { OverflowAction = AsyncTargetWrapperOverflowAction.Block, QueueLimit = 10000 } 

Como puede ver, la configuración del contenedor es tal que un volcado directo al archivo no lleva bastante tiempo. Por lo tanto, se acumula un búfer grande, lo que significa que todas las operaciones intensivas en recursos, como "abrir archivo", se realizan una vez para todo el bloque. Sin embargo, dicho algoritmo requiere memoria adicional (y mucha).


Conclusiones:


  • Si se utiliza una salida asincrónica, no importa qué tipo de configuración de salida se use con el archivo. Puede abrir y cerrar el archivo cada vez, con un búfer grande será casi imperceptible.
  • Todas las mediciones son verdaderas solo para el caso en que los datos se descargan en el disco a aproximadamente la misma velocidad que el llenado de las memorias intermedias (lo hice debido al sistema de archivos rápido + pausas naturales entre mediciones).

Registro síncrono y asíncrono


Resultados:MétodoMediaErrorStddevMediana
KeepFileOpen = true, ConcurrentWrites = false, Async = true1,835.730 ns55,3980 ns163,3422 ns1,791.901 ns
FileLoggingLog4NetNoParams7,076.251 ns41.5518 ns38.8676 ns7,075.394 ns
FileLoggingNLogNetNoParams5.438.306 ns42.0170 ns37,2470 ns5.427.805 ns
NoOpLog4NetNoParams11.063 ns0,0141 ns0,0125 ns11.065 ns
NoOpNLogNetNoParams1.045 ns0.0037 ns0.0033 ns1.045 ns

Conclusiones:


  • A pesar del disco rápido (en mi caso, SSD M.2), escribir en un archivo en otra secuencia acelera el trabajo varias veces. Si su aplicación escribe en discos HDD e incluso se ejecuta en una máquina virtual, la ganancia será aún mayor.
  • Sin embargo, a pesar de la operación incluso rápida del código asincrónico, la falta de registro proporciona una ganancia aún mayor (aunque ligeramente diferente, dependiendo de la biblioteca).

Crear registradores


Resultados:


MétodoMediaErrorStddev
CreateLog4NetFromString1,470.662 ns19.9492 ns18.6605 ns
CreateNLogFromString228.774 ns2.1315 ns1.8895 ns
CreateLog4NetLogger21,046.294 ns284.1171 ns265.7633 ns
CreateNLogTypeOfLogger164,487.931 ns3,240.4372 ns3,031.1070 ns
CreateNLogDynamicLogger134,459.092 ns1,882.8663 ns1,761.2344 ns

Lo que fue probado:


 [Benchmark] public object CreateLog4NetFromString() { return LogManager.GetLogger("my-logger_" + (Interlocked.Increment(ref _log4NetStringLogIndex) % 1000)); } [Benchmark] public object CreateNLogFromString() { return NLog.LogManager.GetLogger("my-logger_" + (Interlocked.Increment(ref _nLogStringLogIndex) % 1000)); } [Benchmark] public object CreateLog4NetLogger() { return new [] { LogManager.GetLogger(typeof(BaseTest)), // x16 times }; } [Benchmark] public object CreateNLogTypeOfLogger() { return new[] { NLog.LogManager.GetCurrentClassLogger(typeof(BaseTest)), // x16 times }; } [Benchmark] public object CreateNLogDynamicLogger() { return new[] { NLog.LogManager.GetCurrentClassLogger(), // x16 times }; } 

Comentario importante: desafortunadamente, fue difícil para mí hacer un punto de referencia reproducible que no condujera a la falta de memoria, pero que crearía diferentes registradores (es decir, para diferentes tipos, para diferentes líneas, etc.).


Sin embargo, después de estudiar el trabajo de las bibliotecas, descubrí que se realizan casi las operaciones más difíciles para crear una clave de registro (es decir, determinar un nombre, borrar argumentos genéricos, etc.).
Además, para estabilizar el punto de referencia para crear un registrador para log4net, fue necesario realizar no una operación, sino 16 (es decir, se devuelve una matriz de 16 objetos idénticos). Si no devuelve nada, .Net optimizó la ejecución para mí (aparentemente, simplemente no devolvió el resultado), lo que condujo a resultados incorrectos.


Y conclusiones:


  • Los registradores se crean más rápidamente a partir de cadenas (NLog es más rápido nuevamente, sin embargo, la diferencia entre las bibliotecas es pequeña, teniendo en cuenta que los registradores se crean no solo así, sino para el trabajo posterior con ellos).
  • log4net es más rápido que NLog al inicializar un proyecto. Quizás esto se deba al almacenamiento en caché adicional en el lado NLog, que ayuda a acelerar las llamadas directas a Debug , Info , etc. De hecho, cada ILogger conoce la respuesta a sí mismo: si debe llamar a los siguientes métodos o no (y esto requiere al menos algún tipo de enlace a la configuración general). Debido a este esquema de trabajo, Out of Memory fue utilizado por mí en la mayoría de las pruebas (si uso diferentes líneas, etc.).
  • LogManager.GetCurrentClassLogger() es incluso más lento que LogManager.GetLogget(typeof(XXX)) . Esto es lógico, incluso los desarrolladores de NLog no recomiendan llamar al primer método en un bucle.
  • Y lo más importante: la velocidad de todos estos métodos a menudo afecta solo el inicio en frío de la aplicación cuando private static readonly ILogger Log = LogManager.GetCurrentClassLogger() campos de la forma private static readonly ILogger Log = LogManager.GetCurrentClassLogger() . Es decir, no afecta directamente el rendimiento del sistema.

Conclusión


¿Cuál es la mejor manera de manejar los registros?


  • Si es posible no iniciar sesión en absoluto, este será el más rápido (lo cual es obvio hasta ahora).
  • Si el proyecto tiene muchas llamadas de registrador que no vuelcan datos a un archivo (a la consola, etc.), entonces NLog es más rápido. Además, asigna menos objetos en el montón.
  • Si aún necesita escribir en un archivo, NLog funciona de forma asíncrona con la mayor rapidez. Sí, consume más memoria (en comparación con NLog en modo síncrono, ya que de acuerdo con mis mediciones anteriores, log4net ni siquiera intenta reutilizar las matrices y Stream 's). Sin embargo, el programa podrá ejecutarse más rápido.
  • Crear un registrador no es una operación gratuita, por lo que a menudo es mejor crearlo con un campo estático. Esto no se aplica a la creación de una cadena, es decir, algo así como LogManager.GetLogger("123") . Estas llamadas funcionan más rápido, lo que significa que se puede crear un registrador para grandes instancias de objetos (por ejemplo, "un registrador para el contexto de la consulta").
  • Si desea generar muchos parámetros en el registro, pero en la mayoría de los casos no habrá volcado directo de datos en el archivo, entonces es mejor hacer varias llamadas. Por lo tanto, NLog no creará objetos adicionales en el montón si no se necesitan allí.

Conclusiones para su código:


  • Si su método acepta un objeto arbitrario (es decir, un object ) y en la mayoría de los casos no hace nada (lo cual es cierto para los contratos / validadores), entonces es más correcto ajustar las llamadas en una forma genérica (es decir, hacer métodos de la forma Something<TArg>(TArg arg) ). Esto funcionará realmente más rápido.
  • Si en su código se permite un restablecimiento de datos de archivo y al mismo tiempo funciona con otra cosa, es mejor confundirse y respaldar esto. Sí, parece obvio que la ejecución paralela puede acelerar el trabajo, sin embargo, en el caso de las operaciones de E / S, este enfoque también brinda un aumento de rendimiento adicional en máquinas con discos lentos.

Source: https://habr.com/ru/post/456488/


All Articles