diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StringConcat.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StringConcat.cs index 4633f0bf5f68c2b8afb736f137055d39343ef3a3..22c50afcca4f96fbcf9f00ed0f712bb669e032b1 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StringConcat.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StringConcat.cs @@ -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); } + + /// + /// 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. + /// + 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; + } + + /// + /// 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. + /// + /// The type symbol of the argument. + /// + /// true if ToString may be used; false if using ToString could lead to observable differences in behavior. + /// + 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; + } + } + } } diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenStringConcat.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenStringConcat.cs index ab0353d15b7fb2926a7319bdf142d72bc73d2001..2f8309ec594fb3c2c4fb85630d7a960bca64913b 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenStringConcat.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenStringConcat.cs @@ -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 +} +"); + } } }