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

Implement ISpanFormattable to improve string interpolation performance #244

Open
sonnemaf opened this issue May 31, 2022 · 4 comments
Open

Comments

@sonnemaf
Copy link

Are you planning to implement the ISpanFormattable interface on the structs? This can improve the performance of string interpolation. It only works in .NET 6 so it requires some conditional compilation.

@Corniel
Copy link
Collaborator

Corniel commented May 31, 2022

I thought of it. The big downside I see, is that it will result in the formatting code being duplicated. I'm not sure yet if that is worth the small gain in performance. Do you have experience with it yourself?

@sonnemaf
Copy link
Author

I did some comparisons and saw a 50% performance improvement. But it is a lot of work to write the TryFormat method correctly.

readonly record struct Point {

    public readonly int X, Y;

    public Point(int x, int y) => (X, Y) = (x, y);

    public override string ToString() => $"({X},{Y})";
}
readonly record struct Point : ISpanFormattable {
    public readonly int X, Y;
    public Point(int x, int y) => (X, Y) = (x, y);

    public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) {
        destination[0] = '(';
        charsWritten = 1;
        destination = destination.Slice(1);
        if (!X.TryFormat(destination, out int tmpCharsWritten, format, provider)) {
            return false;
        }
        charsWritten += tmpCharsWritten;
        destination = destination.Slice(tmpCharsWritten);
        if (destination.Length < 3) { // not enough room to write the , Y and )
            return false;
        }
        destination[0] = ',';
        destination = destination.Slice(1);
        charsWritten++;
        if (!Y.TryFormat(destination, out tmpCharsWritten, format, provider)) {
            return false;
        }
        if (destination.Length < tmpCharsWritten + 1) { // not enough room to write the )
            return false;
        }
        destination[tmpCharsWritten] = ')';
        charsWritten += tmpCharsWritten + 1;
        return true;
    }

    public override string ToString() => $"({X},{Y})";

    public string ToString(string? format, IFormatProvider? formatProvider) => $"({X.ToString(format, formatProvider)},{Y.ToString(format, formatProvider)})";
}

You can use a simple approach using the MemoryExtensions.TryWrite(). This will give you a performance improvement of around 30%. This TryWrite uses the string interpolation syntax.

var p = new Point(3, 4);
Console.WriteLine($"Point {p}");

readonly record struct Point : ISpanFormattable {

    public readonly int X, Y;

    public Point(int x, int y) => (X, Y) = (x, y);

    public bool TryFormat(Span<char> destination, out int charsWritten,
                          ReadOnlySpan<char> format, IFormatProvider? provider) =>
        destination.TryWrite($"({X},{Y})", out charsWritten);

    public override string ToString() => $"({X},{Y})";

    public string ToString(string? format, IFormatProvider? formatProvider) =>
        $"({X.ToString(format, formatProvider)},{Y.ToString(format, formatProvider)})";

}

@Corniel
Copy link
Collaborator

Corniel commented May 31, 2022

We need 26 of them, and most of them are way more complex than this Point implementation. :(

@sonnemaf
Copy link
Author

Maybe you can put it on the backlog

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants