Skip to content
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

Avoid string/StringBuilder/char[] allocation in LogValuesFormatter's ctor #44746

Merged
merged 1 commit into from
Nov 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ internal class LogValuesFormatter
private readonly string _format;
private readonly List<string> _valueNames = new List<string>();

// NOTE: If this assembly ever builds for netcoreapp, the below code should change to:
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
// - Be annotated as [SkipLocalsInit] to avoid zero'ing the stackalloc'd char span
// - Format _valueNames.Count directly into a span

public LogValuesFormatter(string format)
{
if (format == null)
Expand All @@ -29,35 +33,42 @@ public LogValuesFormatter(string format)

OriginalFormat = format;

var sb = new StringBuilder();
var vsb = new ValueStringBuilder(stackalloc char[256]);
int scanIndex = 0;
int endIndex = format.Length;

while (scanIndex < endIndex)
{
int openBraceIndex = FindBraceIndex(format, '{', scanIndex, endIndex);
if (scanIndex == 0 && openBraceIndex == endIndex)
{
// No holes found.
_format = format;
return;
}

int closeBraceIndex = FindBraceIndex(format, '}', openBraceIndex, endIndex);

if (closeBraceIndex == endIndex)
{
sb.Append(format, scanIndex, endIndex - scanIndex);
vsb.Append(format.AsSpan(scanIndex, endIndex - scanIndex));
scanIndex = endIndex;
}
else
{
// Format item syntax : { index[,alignment][ :formatString] }.
int formatDelimiterIndex = FindIndexOfAny(format, FormatDelimiters, openBraceIndex, closeBraceIndex);

sb.Append(format, scanIndex, openBraceIndex - scanIndex + 1);
sb.Append(_valueNames.Count.ToString(CultureInfo.InvariantCulture));
vsb.Append(format.AsSpan(scanIndex, openBraceIndex - scanIndex + 1));
vsb.Append(_valueNames.Count.ToString());
_valueNames.Add(format.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth delaying this Substring alloc?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delaying it to when?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delaying it until the string is actually used.

https://source.dot.net/#Microsoft.Extensions.Logging.Abstractions/LogValuesFormatter.cs,a31f3c3b2cea5615,references

I am not super familiar with this area, but if we only captured the Ranges here, and returned the _valueNames as ReadOnlyMemory<char>, maybe we could save alloc'ing these as strings all together?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could save alloc'ing these as strings all together?

If this is likely, then yeah, we should consider that. But I have no idea how common that is (and if it's at all common, it'll end up being worse as we'll end up having stored the ranges and then needing to also store / replace that with the constructed strings).

@davidfowl?

Let's address it separately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would stringy those up front.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meaning as-is, right? Does that mean these names are generally used?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. We have another source generator thing that will replace this.

Copy link
Member

@eerhardt eerhardt Nov 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was roughly what I was thinking: eerhardt@aa33457. At first I thought I could eliminate the substring allocs all together.

However, it appears that there is a hidden expectation that the scope objects implement IReadOnlyList<KeyValuePair<string, object>>, which means this would be a breaking change. Or we could make the LogValues structs implement both IReadOnlyList<KeyValuePair<string, object>> and IReadOnlyList<KeyValuePair<ReadOnlyMemory<char>, object>>, and have the Console logger (and any other loggers) check for both.

sb.Append(format, formatDelimiterIndex, closeBraceIndex - formatDelimiterIndex + 1);
vsb.Append(format.AsSpan(formatDelimiterIndex, closeBraceIndex - formatDelimiterIndex + 1));

scanIndex = closeBraceIndex + 1;
}
}

_format = sb.ToString();
_format = vsb.ToString();
}

public string OriginalFormat { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand All @@ -12,6 +13,13 @@
Link="Common\src\Extensions\Logging\NullExternalScopeProvider.cs" />
<Compile Include="$(CommonPath)Extensions\Logging\NullScope.cs"
Link="Common\src\Extensions\Logging\NullScope.cs" />
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.cs"
Link="Common\System\Text\ValueStringBuilder.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Buffers" Version="$(SystemBuffersVersion)" />
<PackageReference Include="System.Memory" Version="$(SystemMemoryVersion)" />
</ItemGroup>

</Project>