-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reduce heap allocation of the default log message formatting logic in the FileLogger.Log #74
Conversation
@@ -81,23 +82,11 @@ Version 1.0.4 changes: | |||
<SignAssembly>false</SignAssembly> | |||
<AssemblyOriginatorKeyFile>NReco.Logging.File.snk</AssemblyOriginatorKeyFile> | |||
</PropertyGroup> | |||
|
|||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these packages in their version 8 are compiled for netstandard2.0
, net6.0
and net8.0
, so there's no need to create branches for separate frameworks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return true; | ||
} | ||
|
||
internal static void WriteDigits(this uint value, Span<char> destination, int count) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
namespace NReco.Logging.File.Extensions { | ||
public static class DateTimeExtensions { | ||
public static bool TryFormatO(this DateTime dateTime, Span<char> destination, out int charsWritten) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Writing intesive logs via "Microsoft.Extensions.Logging" infrastructure is not a typical use case I guess (as ILogger interface is not designed for 'low-allocation' usage), however more optimal code is not a drawback for sure. We definitely need to add more tests to ensure that optimized formatting 100% works as original one. Maybe it would be better to introduce a special FileLoggerOptions property to enable this optimization (and use original non-optimized default logs entry formatter by default)? This 100% can guarantee that this change will not affect existing lib users. I can add tests by myself, this is not a problem. I just need a bit of free time for that. |
True. Although, there are certain efforts to improve logging performance :)
Thank you!
Makes sense. But, if tested thoroughly enough, I believe it also makes sense to switch to the optimized approach right away.
So, up to you to decide :) |
…ameworks greater than netstandard2.0
…ect to cover netstandard2.0
Off-topic: there's one flaky test - |
This mode was added here by a contributor: #66 I've added #75 to fix this. |
@aannenko Could you explain a motivation to remove references to 6.0.* for net6? |
These packages are both shipped with a .NET runtime and exist as separate NuGet packages. Version 8 of the ones from NuGet, including Some more thoughts. Are there really benefits in locking a package version to a framework version?
I also had a look at how a couple of popular NuGet packages do this: Serilog. Uses 8.0.1 for all its targets. MiniValidation. Targets net6.0, does not target net8.0 as of now, references the latest package versions anyway. |
Policy for dependencies on other libs versions is a good topic for discussion. I don't think that "the best" policy ever exists -- more adequate answer, I guess, "it depends". For "NReco.Logging.File", I think not using the latest version for old (legacy) targets makes sense. Regarding
If project has a reference to newer version of "Microsoft.Extensions.Logging", dependency on the lower version in NReco.Logging.File cannot force a downgrade. I believe that when someone decides to use "NReco.Logging.File" this dependency should not cause upgrades of another dependencies implicitely, this is a safe behaviour that developers may expect. I agree that it is ok to reference 8.0.* for the netstandard2.0 target, but keep 6.0.* for legacy net6 (and 8.0.* for net8, when next net10 LTS becomes available). |
I found a bit of time to review this PR, I have the following comments/propositions:
I can make these changes by myself, no need to change the PR. |
Ok, totally! Feel free to add changes to this PR and close it as you see fit :) As for making the extension classes [assembly: InternalsVisibleTo("NReco.Logging.Tests")] Or you can extract these extensions into a separate project that targets only netstandard2.0 and reference it only by the netstandard2.0 target of
I yield :) |
As for backward compatibility. We did not really change any APIs, source and even binary compatibility are preserved, and from the user's standpoint the behavior is the same - and this can be checked manually and with tests. So we have full backward compatibility. Another concern is if the changes introduce new potential points of failure - which, again, can be addressed by covering the changes with tests, especially when the most significant changes are concentrated in one method of one class. This can be a good enough guarantee that nothing breaks. If I were to decide, I would try switching to the optimized implementation and avoid exposing additional options in the app's settings. In such case all users would just receive a free perf boost without changing anything as they upgrade the package. I myself, as a user of this package, very much like that it has minimalistic settings and sensible defaults. If I were at some point to find the information about a new configuration option that decreases heap allocation, I would certainly try it. So now I have +1 option configured for this package. What's next? Should I memorize that I need this option in every project where I use this logger? Will the code pertaining this option eventually replace the original? If so, what should I do with this option then? Will I still need it / will it still work? Though, as I said earlier, you are the main package maintainer so it's up to you to decide. |
… the FileLogger.Log (#74) -- extract all optimized code to StringLogEntryFormatter class, optimize GetFormattedLength, added more tests
@aannenko I reorganized the code and I decided to use your optimized implementation by default as you proposed, see 2396077:
These changes are already shipped in 1.2.2 |
@VitaliyMF this is awesome, thank you! |
The purpose of this PR is to reduce allocations done by
FileLogger.Log()
at run time.Details
Profiling
Here's a simple
Program.cs
that logs 10000 messages:We can profile this program in Visual Studio with ".NET Object Allocation Tracking" enabled.
Here's how the profiling results look before the changes from this PR.
Note that for 10000 messages the code is allocating 40000 char arrays, ~30000 strings, 40000
StringBuilder
s and boxes 10000EventId
s, all of those allocations consume memory which leads to more frequent GCs.In contrast, here are the results with these changes.
No char arrays, no
StringBuilder
s, we no longer boxEventId
s and allocate one less string per log message.Benchmark
BenchmarkDotNet results:
Expected outcome
These changes may prove useful in memory-constrained environments like docker containers, low-power devices (e.g. Raspberry), etc., as well as in high-thoughput scenarios where GC pressure is constantly high.