📜 ⬆️ ⬇️

Fast logging

In this article, I put the benchmarks of the most private calls to the loggers. I conducted all experiments on log4net and NLog, on Windows 10 x64 Intel with M.2 SSD.


To not read for a long time, the results table is immediately on top.


Raw results can be viewed on github . In the same repository code (to run, you need .Net 4.7.2 + Microsoft Visual Studio 2017+).


What, how and why - under the cut.


MethodMeanErrorStddevMedianRatioRank
KeepFileOpen = true, ConcurrentWrites = false, Async = true1,835.730 ns55.3980 ns163.3422 ns1,791.901 ns1.00one
KeepFileOpen = true, ConcurrentWrites = true, Async = true1,910.814 ns37.9116 ns90.1008 ns1,908.513 ns1.00one
KeepFileOpen = false, ConcurrentWrites = false, Async = true1,765.390 ns34.5893 ns33.9713 ns1,764.598 ns1.00one
KeepFileOpen = false, ConcurrentWrites = true, Async = true1,834.002 ns36.2599 ns56.4523 ns1,838.500 ns1.00one
KeepFileOpen = true, ConcurrentWrites = false, Async = false5,387.220 ns77.9523 ns69.1027 ns5,376.298 ns1.00one
KeepFileOpen = true, ConcurrentWrites = true, Async = false11,171.048 ns58.5253 ns54.7446 ns11,186.697 ns1.00one
KeepFileOpen = false, ConcurrentWrites = false, Async = false652,512.923 ns8,856.6000 ns8,284.4691 ns650,545.605 ns1.00one
KeepFileOpen = false, ConcurrentWrites = true, Async = false642,003.054 ns12,750.3183 ns31,515.6277 ns645,749.609 ns1.00one
CreateLog4NetFromString1,271.456 ns3.9287 ns3.4827 ns1,271.722 ns1.00one
CreateNLogFromString199.004 ns0.3101 ns0.2901 ns199.046 ns1.00one
CreateLog4NetLogger18,564.228 ns44.6344 ns41.7510 ns18,564.598 ns1.00one
CreateNLogTypeOfLogger140,220.404 ns188.8802 ns176.6787 ns140,235.303 ns1.00one
CreateNLogDynamicLogger115,329.549 ns243.0537 ns227.3526 ns115,361.597 ns1.00one
FileLoggingLog4NetNoParams7,076.251 ns41.5518 ns38.8676 ns7,075,394 ns1.00one
FileLoggingLog4NetSingleReferenceParam7,464.427 ns36.0445 ns33.7161 ns7,470.789 ns1.00one
FileLoggingLog4NetSingleValueParam7,684.635 ns49.2505 ns46.0690 ns7,693.219 ns1.00one
FileLoggingLog4NetMultipleReferencesParam8,207.387 ns79.5855 ns74.4443 ns8,220.897 ns1.00one
FileLoggingLog4NetMultipleValuesParam8,477.657 ns63.4105 ns59.3143 ns8,472.385 ns1.00one
FileLoggingNLogNetNoParams5,438.306 ns42.0170 ns37.2470 ns5,427.805 ns1.00one
FileLoggingNLogNetSingleReferenceParam5,734.572 ns46.0770 ns40.8461 ns5,729.974 ns1.00one
FileLoggingNLogNetSingleValueParam5,834.548 ns40.4125 ns35.8246 ns5,838.905 ns1.00one
FileLoggingNLogNetMultipleReferencesParam6,445.663 ns57.5870 ns53.8669 ns6,440.509 ns1.00one
FileLoggingNLogNetMultipleValuesParam6,784.489 ns43.9255 ns38.9388 ns6,782.898 ns1.00one
NoOpLog4NetNoParams11.063 ns0.0141 ns0.0125 ns11.065 ns1.00one
NoOpLog4NetSingleReferenceParam9.206 ns0.0321 ns0.0300 ns9.203 ns1.00one
NoOpLog4NetSingleValueParam11.423 ns0.0147 ns0.0131 ns11.421 ns1.00one
NoOpLog4NetMultipleReferencesParam44.472 ns0.0474 ns0.0396 ns44.475 ns1.00one
NoOpLog4NetMultipleValuesParam58.138 ns0.1598 ns0.1416 ns58.139 ns1.00one
NoOpNLogNetNoParams1.045 ns0.0037 ns0.0033 ns1.045 ns1.00one
NoOpNLogNetSingleReferenceParam1.994 ns0.0033 ns0.0028 ns1.994 ns1.00one
NoOpNLogNetSingleValueParam2.025 ns0.0044 ns0.0042 ns2.024 ns1.00one
NoOpNLogNetMultipleReferencesParam34.800 ns0.0374 ns0.0312 ns34.798 ns1.00one
NoOpNLogNetMultipleValuesParam47.509 ns0.1199 ns0.1063 ns47.511 ns1.00one

NoOpLogging


First, we estimate how much time we spend on calling the logging method, which ultimately leads to nothing. In most cases (in my experience) the verbose Debug is disabled on the combat servers, but no one removes calls.


First result:


MethodMeanErrorStddevMedian
NoOpLog4NetNoParams11.063 ns0.0141 ns0.0125 ns11.065 ns
NoOpLog4NetSingleReferenceParam9.206 ns0.0321 ns0.0300 ns9.203 ns
NoOpLog4NetSingleValueParam11.423 ns0.0147 ns0.0131 ns11.421 ns
NoOpLog4NetMultipleReferencesParam44.472 ns0.0474 ns0.0396 ns44.475 ns
NoOpLog4NetMultipleValuesParam58.138 ns0.1598 ns0.1416 ns58.139 ns
NoOpNLogNetNoParams1.045 ns0.0037 ns0.0033 ns1.045 ns
NoOpNLogNetSingleReferenceParam1.994 ns0.0033 ns0.0028 ns1.994 ns
NoOpNLogNetSingleValueParam2.025 ns0.0044 ns0.0042 ns2.024 ns
NoOpNLogNetMultipleReferencesParam34.800 ns0.0374 ns0.0312 ns34.798 ns
NoOpNLogNetMultipleValuesParam47.509 ns0.1199 ns0.1063 ns47.511 ns

And the code:


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

First, let's decide why these tests were chosen:



And now the analysis of the results:



Filelogging


Most programs (according to my observations) still log the results to a file, so for comparison we will select this operation. For simplicity, let's just take logger configurations when writing to a file without buffering, without additional locks, etc.


Results:


MethodMeanErrorStddevMedian
FileLoggingLog4NetNoParams7,076.251 ns41.5518 ns38.8676 ns7,075,394 ns
FileLoggingLog4NetSingleReferenceParam7,464.427 ns36.0445 ns33.7161 ns7,470.789 ns
FileLoggingLog4NetSingleValueParam7,684.635 ns49.2505 ns46.0690 ns7,693.219 ns
FileLoggingLog4NetMultipleReferencesParam8,207.387 ns79.5855 ns74.4443 ns8,220.897 ns
FileLoggingLog4NetMultipleValuesParam8,477.657 ns63.4105 ns59.3143 ns8,472.385 ns
FileLoggingNLogNetNoParams5,438.306 ns42.0170 ns37.2470 ns5,427.805 ns
FileLoggingNLogNetSingleReferenceParam5,734.572 ns46.0770 ns40.8461 ns5,729.974 ns
FileLoggingNLogNetSingleValueParam5,834.548 ns40.4125 ns35.8246 ns5,838.905 ns
FileLoggingNLogNetMultipleReferencesParam6,445.663 ns57.5870 ns53.8669 ns6,440.509 ns
FileLoggingNLogNetMultipleValuesParam6,784.489 ns43.9255 ns38.9388 ns6,782.898 ns

Code used:



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


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

As you can see, the configuration of the loggers is more or less similar, but according to the results:



NLog - different locking methods


MethodMeanErrorStddevMedian
KeepFileOpen = true, ConcurrentWrites = false, Async = false5,387.220 ns77.9523 ns69.1027 ns5,376.298 ns
KeepFileOpen = true, ConcurrentWrites = true, Async = false11,171.048 ns58.5253 ns54.7446 ns11,186.697 ns
KeepFileOpen = false, ConcurrentWrites = false, Async = false652,512.923 ns8,856.6000 ns8,284.4691 ns650,545.605 ns
KeepFileOpen = false, ConcurrentWrites = true, Async = false642,003.054 ns12,750.3183 ns31,515.6277 ns645,749.609 ns

Source:


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

If in place of XXXXX and YYYYY to put all possible combinations, we get a test from the table.


The results are pretty predictable:



Asynchronous logging and various locking methods


The NLog library is also able to perform all IO operations on another thread, immediately freeing the current one. Moreover, it does this competently, preserving the order of events, dropping all the data in blocks, and in each block an integer number of events (so as not to get truncated strings), and so on.


The results of different ways of non-blocking work:


MethodMeanErrorStddevMedian
KeepFileOpen = true, ConcurrentWrites = false, Async = true1,835.730 ns55.3980 ns163.3422 ns1,791.901 ns
KeepFileOpen = true, ConcurrentWrites = true, Async = true1,910.814 ns37.9116 ns90.1008 ns1,908.513 ns
KeepFileOpen = false, ConcurrentWrites = false, Async = true1,765.390 ns34.5893 ns33.9713 ns1,764.598 ns
KeepFileOpen = false, ConcurrentWrites = true, Async = true1,834.002 ns36.2599 ns56.4523 ns1,838.500 ns

Comparison of blocking and asynchronous approaches will be further, and here - only the latter.


AsyncTargetWrapper code:


 new AsyncTargetWrapper(fileTargetWithConcurrentWritesAndCloseFileAsync) { OverflowAction = AsyncTargetWrapperOverflowAction.Block, BatchSize = 4096, FullBatchSizeWriteLimit = 128 } 

As you can see, the settings of the wrapper are such that a direct reset to the file does not take a long time. Thus, a large buffer is accumulated, which means all resource-intensive operations like “open file” are performed once for the whole block. However, this algorithm requires additional memory (and quite a lot).


Findings:



Synchronous and asynchronous logging


Results:MethodMeanErrorStddevMedian
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

Findings:



Creating loggers


Results:MethodMeanErrorStddevMedian
CreateLog4NetFromString1,271.456 ns3.9287 ns3.4827 ns1,271.722 ns
CreateNLogFromString199.004 ns0.3101 ns0.2901 ns199.046 ns
CreateLog4NetLogger18,564.228 ns44.6344 ns41.7510 ns18,564.598 ns
CreateNLogTypeOfLogger140,220.404 ns188.8802 ns176.6787 ns140,235.303 ns
CreateNLogDynamicLogger115,329.549 ns243.0537 ns227.3526 ns115,361.597 ns

What tested:


 [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 }; } 

An important remark: unfortunately, it was difficult for me to make a reproducible benchmark that does not lead to Out Of Memory, but that would create different loggers (i.e., for different types, for different lines, and so on).


However, having studied the work of the libraries, I found that perhaps the most ponderous operations are performed to create the logger key (i.e., name definition, clearing of Generic arguments, and so on).
Moreover, in order to stabilize the benchmark for creating a logger for log4net, I had to perform not one operation, but 16 (that is, an array of 16 identical objects is returned). If you do not return anything, then .Net for me optimized execution (apparently, just not returning the result), which led to incorrect results.


And the conclusions:



Conclusion


How best to handle logs:



Conclusions for your code:



')

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


All Articles