Create a gist now

Instantly share code, notes, and snippets.

Roslyn Adventures: Optimizing StringBuilder string interpolation
new StringBuilder().Append(string.Format("foo {0}", 1));
new StringBuilder().Append($"foo {1}");
IL_0001: newobj System.Text.StringBuilder..ctor
IL_0006: ldstr "foo {0}"
IL_000B: ldc.i4.1
IL_000C: box System.Int32
IL_0011: call System.String.Format
IL_0016: call System.Text.StringBuilder.Append
class Foo
{
public static void Bar(FormattableString str)
{
Console.WriteLine("FORMAT: " + str.Format, str.GetArguments());
}
public static void Bar(string str)
{
Console.WriteLine("STRING: " + str);
}
public static void Main()
{
Bar($"a test {42}");
}
}
new StringBuilder().AppendFormat("foo {0}", 1);
IL_0001: newobj System.Text.StringBuilder..ctor
IL_0006: ldstr "foo {0}"
IL_000B: ldc.i4.1
IL_000C: box System.Int32
IL_0011: call System.Text.StringBuilder.AppendFormat
CSharpCompilation compilation = ...;
foreach (var syntaxTree in compilation.SyntaxTrees)
{
var model = compilation.GetSemanticModel (syntaxTree);
var rewriter = new StringBuilderInterpolationOptimizer(model);
var rootNode = syntaxTree.GetRoot();
var rewritten = rewriter.Visit(rootNode);
if (rootNode != rewritten)
{
compilation = compilation.ReplaceSyntaxTree(
syntaxTree,
syntaxTree.WithRootAndOptions(rewritten, syntaxTree.Options));
}
}
class StringBuilderInterpolationOptimizer : CSharpSyntaxRewriter
{
private readonly SemanticModel _model;
public StringBuilderInterpolationOptimizer(SemanticModel model)
{
_model = model;
}
// we use the semantic model to get the type information of the method being called
private static bool CanRewriteSymbol(SymbolInfo symbolInfo, out bool appendNewLine)
{
appendNewLine = false;
IMethodSymbol methodSymbol = symbolInfo.Symbol as IMethodSymbol;
if (methodSymbol == null) return false;
switch (methodSymbol.Name)
{
case "AppendLine":
case "Append":
if (methodSymbol.ContainingType.ToString() == "System.Text.StringBuilder")
{
appendNewLine = methodSymbol.Name == "AppendLine";
return true;
}
break;
}
return false;
}
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
{
var memberAccess = node.Expression as MemberAccessExpressionSyntax;
if (memberAccess != null && node.ArgumentList.Arguments.Count == 1)
{
// check if the single method argument is an interpolated string
var interpolatedStringSyntax = node.ArgumentList.Arguments[0].Expression as olatedStringExpressionSyntax;
if (interpolatedStringSyntax != null)
{
bool appendNewLine; // this distinguishes Append and AppendLine calls
if (CanRewriteSymbol(_model.GetSymbolInfo(memberAccess), out appendNewLine))
{
var formatCount = 0;
var formatString = new StringBuilder();
var formatArgs = new List<ArgumentSyntax>();
// build the format string
foreach (var content in interpolatedStringSyntax.Contents)
{
switch (content.Kind())
{
case SyntaxKind.InterpolatedStringText:
var text = (InterpolatedStringTextSyntax)content;
formatString.Append(text.TextToken.Text);
break;
case SyntaxKind.Interpolation:
var interpolation = (InterpolationSyntax)content;
formatString.Append("{");
formatString.Append(formatCount++);
formatString.Append(interpolation.AlignmentClause);
formatString.Append(interpolation.FormatClause);
formatString.Append("}");
// the interpolations become arguments for the AppendFormat call
formatArgs.Add(SyntaxFactory.Argument(interpolation.Expression));
break;
}
}
if (appendNewLine)
{
formatString.AppendLine();
}
// the first parameter is the format string
formatArgs.Insert(0,
SyntaxFactory.Argument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(formatString.ToString()))));
return node
.WithExpression(memberAccess.WithName(SyntaxFactory.IdentifierName("AppendFormat")))
.WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(formatParams)));
}
}
}
return base.VisitInvocationExpression(node);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment