Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit de32aed

Browse files
committed
Avoid defensive copy in String.Concat(string[]) (#4559)
* Avoid defensive copy in String.Concat(string[]) Today the String.Concat(params string[]) implementation makes a defensive copy of the input string[] in order to avoid issues where another thread mutates the array concurrently with the concatenation. Such a situation is possible but exceedingly rare, so we can redo the implementation to make it cheaper when there isn't concurrent mutation and more expensive when there is, rather than always having it be more expensive. This commit changes string.Concat to optimistically assume there won't be such concurrent mutation. It avoids allocating the defensive string[] array copy, and instead just proceeds to allocate a string of the right length and copy the input strings into it. If along the way it discovers that something has changed such that the string lengths no longer add up to exactly what was precomputed, then it falls back to the original implementation. Example microbenchmark: ```C# using System; using System.Diagnostics; public class Program { public static void Main() { string result; string[] inputs = new[] { "abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx", "yz" }; var sw = new Stopwatch(); while (true) { int gen0 = GC.CollectionCount(0); sw.Restart(); for (int i = 0; i < 20000000; i++) result = string.Concat(inputs); sw.Stop(); Console.WriteLine($"{GC.CollectionCount(0) - gen0}: {sw.Elapsed.TotalSeconds}"); } } } ``` Before the change: ``` > corerun test.exe 1525: 2.0733829 1526: 2.0599474 1526: 2.0717786 1526: 2.0318797 ^C ``` After the change: ``` > corerun test.exe 763: 1.4700695 762: 1.446919 763: 1.4581139 763: 1.4568889 ^C ```
1 parent 13b8466 commit de32aed

File tree

1 file changed

+62
-30
lines changed

1 file changed

+62
-30
lines changed

src/mscorlib/src/System/String.cs

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3174,7 +3174,17 @@ public static String Concat(params Object[] args) {
31743174
throw new OutOfMemoryException();
31753175
}
31763176
}
3177-
return ConcatArray(sArgs, totalLength);
3177+
3178+
string result = FastAllocateString(totalLength);
3179+
int currPos = 0;
3180+
for (int i = 0; i < sArgs.Length; i++)
3181+
{
3182+
Contract.Assert(currPos <= totalLength - sArgs[i].Length, "[String.Concat](currPos <= totalLength - sArgs[i].Length)");
3183+
FillStringChecked(result, currPos, sArgs[i]);
3184+
currPos += sArgs[i].Length;
3185+
}
3186+
3187+
return result;
31783188
}
31793189

31803190
[ComVisible(false)]
@@ -3319,44 +3329,66 @@ public static String Concat(String str0, String str1, String str2, String str3)
33193329
return result;
33203330
}
33213331

3322-
[System.Security.SecuritySafeCritical] // auto-generated
3323-
private static String ConcatArray(String[] values, int totalLength) {
3324-
String result = FastAllocateString(totalLength);
3325-
int currPos=0;
3326-
3327-
for (int i=0; i<values.Length; i++) {
3328-
Contract.Assert((currPos <= totalLength - values[i].Length),
3329-
"[String.ConcatArray](currPos <= totalLength - values[i].Length)");
3330-
3331-
FillStringChecked(result, currPos, values[i]);
3332-
currPos+=values[i].Length;
3333-
}
3334-
3335-
return result;
3336-
}
3337-
33383332
public static String Concat(params String[] values) {
33393333
if (values == null)
33403334
throw new ArgumentNullException("values");
33413335
Contract.Ensures(Contract.Result<String>() != null);
3342-
// Spec#: Consider a postcondition saying the length of this string == the sum of each string in array
33433336
Contract.EndContractBlock();
3344-
int totalLength=0;
33453337

3346-
// Always make a copy to prevent changing the array on another thread.
3347-
String[] internalValues = new String[values.Length];
3348-
3349-
for (int i=0; i<values.Length; i++) {
3338+
// It's possible that the input values array could be changed concurrently on another
3339+
// thread, such that we can't trust that each read of values[i] will be equivalent.
3340+
// Worst case, we can make a defensive copy of the array and use that, but we first
3341+
// optimistically try the allocation and copies assuming that the array isn't changing,
3342+
// which represents the 99.999% case, in particular since string.Concat is used for
3343+
// string concatenation by the languages, with the input array being a params array.
3344+
3345+
// Sum the lengths of all input strings
3346+
long totalLengthLong = 0;
3347+
for (int i = 0; i < values.Length; i++)
3348+
{
33503349
string value = values[i];
3351-
internalValues[i] = ((value==null)?(String.Empty):(value));
3352-
totalLength += internalValues[i].Length;
3353-
// check for overflow
3354-
if (totalLength < 0) {
3355-
throw new OutOfMemoryException();
3350+
if (value != null)
3351+
{
3352+
totalLengthLong += value.Length;
33563353
}
33573354
}
3358-
3359-
return ConcatArray(internalValues, totalLength);
3355+
3356+
// If it's too long, fail, or if it's empty, return an empty string.
3357+
if (totalLengthLong > int.MaxValue)
3358+
{
3359+
throw new OutOfMemoryException();
3360+
}
3361+
int totalLength = (int)totalLengthLong;
3362+
if (totalLength == 0)
3363+
{
3364+
return string.Empty;
3365+
}
3366+
3367+
// Allocate a new string and copy each input string into it
3368+
string result = FastAllocateString(totalLength);
3369+
int copiedLength = 0;
3370+
for (int i = 0; i < values.Length; i++)
3371+
{
3372+
string value = values[i];
3373+
if (!string.IsNullOrEmpty(value))
3374+
{
3375+
int valueLen = value.Length;
3376+
if (valueLen > totalLength - copiedLength)
3377+
{
3378+
copiedLength = -1;
3379+
break;
3380+
}
3381+
3382+
FillStringChecked(result, copiedLength, value);
3383+
copiedLength += valueLen;
3384+
}
3385+
}
3386+
3387+
// If we copied exactly the right amount, return the new string. Otherwise,
3388+
// something changed concurrently to mutate the input array: fall back to
3389+
// doing the concatenation again, but this time with a defensive copy. This
3390+
// fall back should be extremely rare.
3391+
return copiedLength == totalLength ? result : Concat((string[])values.Clone());
33603392
}
33613393

33623394
[System.Security.SecuritySafeCritical] // auto-generated

0 commit comments

Comments
 (0)