提交 70d2dee7 编写于 作者: S Stephen Toub

Avoid unnecessary boxing and string allocs with String.Concat

When string concatenation encounters non-strings (e.g. path + '/', name + someInt32, etc.), it calls overloads of String.Concat that accept objects. These overloads then just call ToString on each of the objects, mapping to the C# spec which states that "any non-string argument is converted to its string representation by invoking the virtual ToString method inherited from type object." When any of the individual items being concatenated is a value type, that object first gets boxed to be passed to String.Concat as an object, only to then have ToString called on the boxed object.

This commit changes the local rewriter for string concatenation to test whether an argument is of an appropriate type, and if it is, to call ToString on it directly, rather than first boxing it. This can then affect which overload of Concat is used, as the type of the argument has changed to be String. The primary benefit of this is saving the allocation per value-type item. There are some secondary benefits, as well; for example, as there is a four-string overload of Concat but no four-object overload of Concat, if this optimization is able to force all of the items to be strings, the four-string overload can be used rather than allocating an object array for the inputs and then another string array for the resulting strings (inside of Concat).

This commit also includes an additional optimization specific to const chars; rather than doing a ToString call at run time, we can do it at compile-time and emit a literal string instead of a literal char, saving both the boxing and the string allocation at run time.
上级 2321b296
......@@ -45,6 +45,10 @@ private BoundExpression RewriteStringConcatenation(CSharpSyntaxNode syntax, Bina
return RewriteStringConcatInExpressionLambda(syntax, operatorKind, loweredLeft, loweredRight, type);
}
// avoid run time boxing and ToString operations if we can reasonably convert to a string at compile time
loweredLeft = ConvertConcatExprToStringIfPossible(syntax, loweredLeft);
loweredRight = ConvertConcatExprToStringIfPossible(syntax, loweredRight);
// try fold two args without flattening.
var folded = TryFoldTwoConcatOperands(syntax, loweredLeft, loweredRight);
if (folded != null)
......@@ -346,5 +350,83 @@ private BoundExpression RewriteStringConcatInExpressionLambda(CSharpSyntaxNode s
return new BoundBinaryOperator(syntax, operatorKind, loweredLeft, loweredRight, default(ConstantValue), method, default(LookupResultKind), type);
}
/// <summary>
/// Checks whether the expression represents a boxing conversion of a special value type.
/// If it does, it tries to return a string-based representation instead in order
/// to avoid allocations. If it can't, the original expression is returned.
/// </summary>
private BoundExpression ConvertConcatExprToStringIfPossible(CSharpSyntaxNode syntax, BoundExpression expr)
{
if (expr.Kind == BoundKind.Conversion)
{
BoundConversion conv = (BoundConversion)expr;
if (conv.ConversionKind == ConversionKind.Boxing)
{
BoundExpression operand = conv.Operand;
if (operand != null)
{
// Is the expression a literal char? If so, we can
// simply make it a literal string instead and avoid any
// allocations for converting the char to a string at run time.
if (operand.Kind == BoundKind.Literal)
{
ConstantValue cv = ((BoundLiteral)operand).ConstantValue;
if (cv != null && cv.SpecialType == SpecialType.System_Char)
{
return _factory.StringLiteral(cv.CharValue.ToString());
}
}
// Can the expression be optimized with a ToString call?
// If so, we can synthesize a ToString call to avoid boxing.
if (ConcatExprCanBeOptimizedWithToString(operand.Type))
{
var toString = GetSpecialTypeMethod(syntax, SpecialMember.System_Object__ToString);
return BoundCall.Synthesized(syntax, operand, toString);
}
}
}
}
// Optimization not possible; just return the original expression.
return expr;
}
/// <summary>
/// Gets whether the type of an argument used in string concatenation can
/// be optimized by first calling ToString on it before passing the argument
/// to the String.Concat function.
/// </summary>
/// <param name="symbol">The type symbol of the argument.</param>
/// <returns>
/// true if ToString may be used; false if using ToString could lead to observable differences in behavior.
/// </returns>
private static bool ConcatExprCanBeOptimizedWithToString(TypeSymbol symbol)
{
// There are several constraints applied here in support of backwards compatibility:
// - This optimization potentially changes the order in which ToString is called
// on the arguments. That's a a compatibility issue if one argument's ToString
// depends on state mutated by another, such as current culture.
// - For value types, this optimization causes ToString to be called on the original
// value rather than on a boxed copy. That means a mutating ToString implementation
// could change the original rather than the copy.
// For these reasons, this optimization is currently restricted to primitives
// known to have a non-mutating ToString implementation that is independent
// of externally mutable state. Common value types such as Int32 and Double
// do not meet this bar.
switch (symbol.SpecialType)
{
case SpecialType.System_Boolean:
case SpecialType.System_Char:
case SpecialType.System_IntPtr:
case SpecialType.System_UIntPtr:
return true;
default:
return false;
}
}
}
}
......@@ -845,5 +845,255 @@ static void Main()
CompileAndVerify(source, expectedOutput: "(0)(0)(0)(1)(2)(0)(0)(0)(1)(2)");
}
[Fact]
public void ConcatWithSpecialValueTypes()
{
var source = @"
using System;
public class Test
{
static void Main()
{
const char a = 'a', b = 'b';
char c = 'c', d = 'd';
Console.WriteLine(a + ""1"");
Console.WriteLine(""2"" + b);
Console.WriteLine(c + ""3"");
Console.WriteLine(""4"" + d);
Console.WriteLine(true + ""5"" + c);
Console.WriteLine(""6"" + d + (IntPtr)7);
Console.WriteLine(""8"" + (UIntPtr)9 + false);
Console.WriteLine(c + ""10"" + d + ""11"");
Console.WriteLine(""12"" + c + ""13"" + d);
Console.WriteLine(a + ""14"" + b + ""15"" + a + ""16"");
Console.WriteLine(c + ""17"" + d + ""18"" + c + ""19"");
Console.WriteLine(""20"" + 21 + c + d + c + d);
Console.WriteLine(""22"" + c + ""23"" + d + c + d);
}
}
";
var comp = CompileAndVerify(source, expectedOutput: @"a1
2b
c3
4d
True5c
6d7
89False
c10d11
12c13d
a14b15a16
c17d18c19
2021cdcd
22c23dcd");
comp.VerifyDiagnostics();
comp.VerifyIL("Test.Main", @"
{
// Code size 611 (0x263)
.maxstack 4
.locals init (char V_0, //c
char V_1, //d
bool V_2,
System.IntPtr V_3,
System.UIntPtr V_4)
IL_0000: ldc.i4.s 99
IL_0002: stloc.0
IL_0003: ldc.i4.s 100
IL_0005: stloc.1
IL_0006: ldstr ""a1""
IL_000b: call ""void System.Console.WriteLine(string)""
IL_0010: ldstr ""2b""
IL_0015: call ""void System.Console.WriteLine(string)""
IL_001a: ldloca.s V_0
IL_001c: constrained. ""char""
IL_0022: callvirt ""string object.ToString()""
IL_0027: ldstr ""3""
IL_002c: call ""string string.Concat(string, string)""
IL_0031: call ""void System.Console.WriteLine(string)""
IL_0036: ldstr ""4""
IL_003b: ldloca.s V_1
IL_003d: constrained. ""char""
IL_0043: callvirt ""string object.ToString()""
IL_0048: call ""string string.Concat(string, string)""
IL_004d: call ""void System.Console.WriteLine(string)""
IL_0052: ldc.i4.1
IL_0053: stloc.2
IL_0054: ldloca.s V_2
IL_0056: constrained. ""bool""
IL_005c: callvirt ""string object.ToString()""
IL_0061: ldstr ""5""
IL_0066: ldloca.s V_0
IL_0068: constrained. ""char""
IL_006e: callvirt ""string object.ToString()""
IL_0073: call ""string string.Concat(string, string, string)""
IL_0078: call ""void System.Console.WriteLine(string)""
IL_007d: ldstr ""6""
IL_0082: ldloca.s V_1
IL_0084: constrained. ""char""
IL_008a: callvirt ""string object.ToString()""
IL_008f: ldc.i4.7
IL_0090: call ""System.IntPtr System.IntPtr.op_Explicit(int)""
IL_0095: stloc.3
IL_0096: ldloca.s V_3
IL_0098: constrained. ""System.IntPtr""
IL_009e: callvirt ""string object.ToString()""
IL_00a3: call ""string string.Concat(string, string, string)""
IL_00a8: call ""void System.Console.WriteLine(string)""
IL_00ad: ldstr ""8""
IL_00b2: ldc.i4.s 9
IL_00b4: conv.i8
IL_00b5: call ""System.UIntPtr System.UIntPtr.op_Explicit(ulong)""
IL_00ba: stloc.s V_4
IL_00bc: ldloca.s V_4
IL_00be: constrained. ""System.UIntPtr""
IL_00c4: callvirt ""string object.ToString()""
IL_00c9: ldc.i4.0
IL_00ca: stloc.2
IL_00cb: ldloca.s V_2
IL_00cd: constrained. ""bool""
IL_00d3: callvirt ""string object.ToString()""
IL_00d8: call ""string string.Concat(string, string, string)""
IL_00dd: call ""void System.Console.WriteLine(string)""
IL_00e2: ldloca.s V_0
IL_00e4: constrained. ""char""
IL_00ea: callvirt ""string object.ToString()""
IL_00ef: ldstr ""10""
IL_00f4: ldloca.s V_1
IL_00f6: constrained. ""char""
IL_00fc: callvirt ""string object.ToString()""
IL_0101: ldstr ""11""
IL_0106: call ""string string.Concat(string, string, string, string)""
IL_010b: call ""void System.Console.WriteLine(string)""
IL_0110: ldstr ""12""
IL_0115: ldloca.s V_0
IL_0117: constrained. ""char""
IL_011d: callvirt ""string object.ToString()""
IL_0122: ldstr ""13""
IL_0127: ldloca.s V_1
IL_0129: constrained. ""char""
IL_012f: callvirt ""string object.ToString()""
IL_0134: call ""string string.Concat(string, string, string, string)""
IL_0139: call ""void System.Console.WriteLine(string)""
IL_013e: ldstr ""a14b15a16""
IL_0143: call ""void System.Console.WriteLine(string)""
IL_0148: ldc.i4.6
IL_0149: newarr ""string""
IL_014e: dup
IL_014f: ldc.i4.0
IL_0150: ldloca.s V_0
IL_0152: constrained. ""char""
IL_0158: callvirt ""string object.ToString()""
IL_015d: stelem.ref
IL_015e: dup
IL_015f: ldc.i4.1
IL_0160: ldstr ""17""
IL_0165: stelem.ref
IL_0166: dup
IL_0167: ldc.i4.2
IL_0168: ldloca.s V_1
IL_016a: constrained. ""char""
IL_0170: callvirt ""string object.ToString()""
IL_0175: stelem.ref
IL_0176: dup
IL_0177: ldc.i4.3
IL_0178: ldstr ""18""
IL_017d: stelem.ref
IL_017e: dup
IL_017f: ldc.i4.4
IL_0180: ldloca.s V_0
IL_0182: constrained. ""char""
IL_0188: callvirt ""string object.ToString()""
IL_018d: stelem.ref
IL_018e: dup
IL_018f: ldc.i4.5
IL_0190: ldstr ""19""
IL_0195: stelem.ref
IL_0196: call ""string string.Concat(params string[])""
IL_019b: call ""void System.Console.WriteLine(string)""
IL_01a0: ldc.i4.6
IL_01a1: newarr ""object""
IL_01a6: dup
IL_01a7: ldc.i4.0
IL_01a8: ldstr ""20""
IL_01ad: stelem.ref
IL_01ae: dup
IL_01af: ldc.i4.1
IL_01b0: ldc.i4.s 21
IL_01b2: box ""int""
IL_01b7: stelem.ref
IL_01b8: dup
IL_01b9: ldc.i4.2
IL_01ba: ldloca.s V_0
IL_01bc: constrained. ""char""
IL_01c2: callvirt ""string object.ToString()""
IL_01c7: stelem.ref
IL_01c8: dup
IL_01c9: ldc.i4.3
IL_01ca: ldloca.s V_1
IL_01cc: constrained. ""char""
IL_01d2: callvirt ""string object.ToString()""
IL_01d7: stelem.ref
IL_01d8: dup
IL_01d9: ldc.i4.4
IL_01da: ldloca.s V_0
IL_01dc: constrained. ""char""
IL_01e2: callvirt ""string object.ToString()""
IL_01e7: stelem.ref
IL_01e8: dup
IL_01e9: ldc.i4.5
IL_01ea: ldloca.s V_1
IL_01ec: constrained. ""char""
IL_01f2: callvirt ""string object.ToString()""
IL_01f7: stelem.ref
IL_01f8: call ""string string.Concat(params object[])""
IL_01fd: call ""void System.Console.WriteLine(string)""
IL_0202: ldc.i4.6
IL_0203: newarr ""string""
IL_0208: dup
IL_0209: ldc.i4.0
IL_020a: ldstr ""22""
IL_020f: stelem.ref
IL_0210: dup
IL_0211: ldc.i4.1
IL_0212: ldloca.s V_0
IL_0214: constrained. ""char""
IL_021a: callvirt ""string object.ToString()""
IL_021f: stelem.ref
IL_0220: dup
IL_0221: ldc.i4.2
IL_0222: ldstr ""23""
IL_0227: stelem.ref
IL_0228: dup
IL_0229: ldc.i4.3
IL_022a: ldloca.s V_1
IL_022c: constrained. ""char""
IL_0232: callvirt ""string object.ToString()""
IL_0237: stelem.ref
IL_0238: dup
IL_0239: ldc.i4.4
IL_023a: ldloca.s V_0
IL_023c: constrained. ""char""
IL_0242: callvirt ""string object.ToString()""
IL_0247: stelem.ref
IL_0248: dup
IL_0249: ldc.i4.5
IL_024a: ldloca.s V_1
IL_024c: constrained. ""char""
IL_0252: callvirt ""string object.ToString()""
IL_0257: stelem.ref
IL_0258: call ""string string.Concat(params string[])""
IL_025d: call ""void System.Console.WriteLine(string)""
IL_0262: ret
}
");
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册