Improve performance of Path.ChangeExtension (#21766)
authorStephen Toub <stoub@microsoft.com>
Thu, 3 Jan 2019 21:34:56 +0000 (16:34 -0500)
committerJan Kotas <jkotas@microsoft.com>
Thu, 3 Jan 2019 21:34:56 +0000 (11:34 -1000)
In the common case where it need to replace a non-empty extension with a non-empty extension, it currently incurs a substring without the original extension prior to then concatenating with the new extension.  This PR avoids that.

(As the Path implementation is in corelib, this uses string.FastAllocateString and then formats into it with a span; if we wanted to avoid that, string.Create could also be used.)

* Add internal String.Concat overloads for spans

* Use span-based string.Concat overloads in several places

src/System.Private.CoreLib/shared/System/Globalization/CultureData.Unix.cs
src/System.Private.CoreLib/shared/System/Globalization/CultureData.cs
src/System.Private.CoreLib/shared/System/Globalization/DateTimeFormatInfo.cs
src/System.Private.CoreLib/shared/System/Globalization/DateTimeFormatInfoScanner.cs
src/System.Private.CoreLib/shared/System/IO/Path.cs
src/System.Private.CoreLib/shared/System/String.Manipulation.cs

index d870742..c800c48 100644 (file)
@@ -29,7 +29,6 @@ namespace System.Globalization
 
             Debug.Assert(!GlobalizationMode.Invariant);
 
-            string alternateSortName = string.Empty;
             string realNameBuffer = _sRealName;
 
             // Basic validation
@@ -39,16 +38,17 @@ namespace System.Globalization
             }
 
             // Replace _ (alternate sort) with @collation= for ICU
+            ReadOnlySpan<char> alternateSortName = default;
             int index = realNameBuffer.IndexOf('_');
             if (index > 0)
             {
                 if (index >= (realNameBuffer.Length - 1) // must have characters after _
-                    || realNameBuffer.Substring(index + 1).Contains('_')) // only one _ allowed
+                    || realNameBuffer.IndexOf('_', index + 1) >= 0) // only one _ allowed
                 {
                     return false; // fail
                 }
-                alternateSortName = realNameBuffer.Substring(index + 1);
-                realNameBuffer = realNameBuffer.Substring(0, index) + ICU_COLLATION_KEYWORD + alternateSortName;
+                alternateSortName = realNameBuffer.AsSpan(index + 1);
+                realNameBuffer = string.Concat(realNameBuffer.AsSpan(0, index), ICU_COLLATION_KEYWORD, alternateSortName);
             }
 
             // Get the locale name from ICU
@@ -61,7 +61,7 @@ namespace System.Globalization
             index = _sWindowsName.IndexOf(ICU_COLLATION_KEYWORD, StringComparison.Ordinal);
             if (index >= 0)
             {
-                _sName = _sWindowsName.Substring(0, index) + "_" + alternateSortName;
+                _sName = string.Concat(_sWindowsName.AsSpan(0, index), "_", alternateSortName);
             }
             else
             {
index 5f37af2..48e0206 100644 (file)
@@ -956,9 +956,11 @@ namespace System.Globalization
                             if (this.SENGLISHLANGUAGE[this.SENGLISHLANGUAGE.Length - 1] == ')')
                             {
                                 // "Azeri (Latin)" + "Azerbaijan" -> "Azeri (Latin, Azerbaijan)"
-                                _sEnglishDisplayName =
-                                    this.SENGLISHLANGUAGE.Substring(0, _sEnglishLanguage.Length - 1) +
-                                    ", " + this.SENGCOUNTRY + ")";
+                                _sEnglishDisplayName = string.Concat(
+                                    this.SENGLISHLANGUAGE.AsSpan(0, _sEnglishLanguage.Length - 1),
+                                    ", ",
+                                    this.SENGCOUNTRY,
+                                    ")");
                             }
                             else
                             {
@@ -1645,7 +1647,7 @@ namespace System.Globalization
                             sep = "";
                         }
 
-                        time = time.Substring(0, j) + sep + time.Substring(endIndex);
+                        time = string.Concat(time.AsSpan(0, j), sep, time.AsSpan(endIndex));
                         break;
                     case 'm':
                     case 'H':
index 3fa178f..8123d37 100644 (file)
@@ -2377,7 +2377,6 @@ namespace System.Globalization
                 // This is determined in DateTimeFormatInfoScanner.  Use this flag to determine if we should treat date separator as ignorable symbol.
                 bool useDateSepAsIgnorableSymbol = false;
 
-                string monthPostfix = null;
                 if (dateWords != null)
                 {
                     // There are DateWords.  It could be a real date word (such as "de"), or a monthPostfix.
@@ -2389,7 +2388,7 @@ namespace System.Globalization
                             // This is a month postfix
                             case DateTimeFormatInfoScanner.MonthPostfixChar:
                                 // Get the real month postfix.
-                                monthPostfix = dateWords[i].Substring(1);
+                                ReadOnlySpan<char> monthPostfix = dateWords[i].AsSpan(1);
                                 // Add the month name + postfix into the token.
                                 AddMonthNames(temp, monthPostfix);
                                 break;
@@ -2421,7 +2420,7 @@ namespace System.Globalization
                     InsertHash(temp, this.DateSeparator, TokenType.SEP_Date, 0);
                 }
                 // Add the regular month names.
-                AddMonthNames(temp, null);
+                AddMonthNames(temp);
 
                 // Add the abbreviated month names.
                 for (int i = 1; i <= 13; i++)
@@ -2548,22 +2547,21 @@ namespace System.Globalization
             return (temp);
         }
 
-        private void AddMonthNames(TokenHashValue[] temp, string monthPostfix)
+        private void AddMonthNames(TokenHashValue[] temp, ReadOnlySpan<char> monthPostfix = default)
         {
             for (int i = 1; i <= 13; i++)
             {
-                string str;
                 //str = internalGetMonthName(i, MonthNameStyles.Regular, false);
                 // We have to call public methods here to work with inherited DTFI.
                 // Insert the month name first, so that they are at the front of abbreviated
                 // month names.
-                str = GetMonthName(i);
+                string str = GetMonthName(i);
                 if (str.Length > 0)
                 {
-                    if (monthPostfix != null)
+                    if (!monthPostfix.IsEmpty)
                     {
                         // Insert the month name with the postfix first, so it can be matched first.
-                        InsertHash(temp, str + monthPostfix, TokenType.MonthToken, i);
+                        InsertHash(temp, string.Concat(str, monthPostfix), TokenType.MonthToken, i);
                     }
                     else
                     {
index de43c2d..ae3c1c2 100644 (file)
@@ -245,7 +245,7 @@ namespace System.Globalization
                         }
                         if (str[str.Length - 1] == '.')
                         {
-                            // Old version ignore the trialing dot in the date words. Support this as well.
+                            // Old version ignore the trailing dot in the date words. Support this as well.
                             string strWithoutDot = str.Substring(0, str.Length - 1);
                             if (!m_dateWords.Contains(strWithoutDot))
                             {
index 3d510f4..364b847 100644 (file)
@@ -47,30 +47,38 @@ namespace System.IO
         // is null, any existing extension is removed from path.
         public static string ChangeExtension(string path, string extension)
         {
-            if (path != null)
+            if (path == null)
+                return null;
+
+            int subLength = path.Length;
+            if (subLength == 0)
+                return string.Empty;
+
+            for (int i = path.Length - 1; i >= 0; i--)
             {
-                string s = path;
-                for (int i = path.Length - 1; i >= 0; i--)
+                char ch = path[i];
+
+                if (ch == '.')
                 {
-                    char ch = path[i];
-                    if (ch == '.')
-                    {
-                        s = path.Substring(0, i);
-                        break;
-                    }
-                    if (PathInternal.IsDirectorySeparator(ch)) break;
+                    subLength = i;
+                    break;
                 }
 
-                if (extension != null && path.Length != 0)
+                if (PathInternal.IsDirectorySeparator(ch))
                 {
-                    s = (extension.Length == 0 || extension[0] != '.') ?
-                        s + "." + extension :
-                        s + extension;
+                    break;
                 }
+            }
 
-                return s;
+            if (extension == null)
+            {
+                return path.Substring(0, subLength);
             }
-            return null;
+
+            ReadOnlySpan<char> subpath = path.AsSpan(0, subLength);
+            return extension.StartsWith('.') ?
+                string.Concat(subpath, extension) :
+                string.Concat(subpath, ".", extension);
         }
 
         /// <summary>
index 3359410..0cd5d1c 100644 (file)
@@ -341,6 +341,55 @@ namespace System
             return result;
         }
 
+        // TODO: Expose publicly: https://github.com/dotnet/corefx/issues/34330.
+        internal static string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1)
+        {
+            string result = FastAllocateString(checked(str0.Length + str1.Length));
+            Span<char> resultSpan = new Span<char>(ref result.GetRawStringData(), result.Length);
+
+            str0.CopyTo(resultSpan);
+            str1.CopyTo(resultSpan.Slice(str0.Length));
+
+            return result;
+        }
+
+        // TODO: Expose publicly: https://github.com/dotnet/corefx/issues/34330.
+        internal static string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1, ReadOnlySpan<char> str2)
+        {
+            string result = FastAllocateString(checked(str0.Length + str1.Length + str2.Length));
+            Span<char> resultSpan = new Span<char>(ref result.GetRawStringData(), result.Length);
+
+            str0.CopyTo(resultSpan);
+            resultSpan = resultSpan.Slice(str0.Length);
+
+            str1.CopyTo(resultSpan);
+            resultSpan = resultSpan.Slice(str1.Length);
+
+            str2.CopyTo(resultSpan);
+
+            return result;
+        }
+
+        // TODO: Expose publicly: https://github.com/dotnet/corefx/issues/34330.
+        internal static string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1, ReadOnlySpan<char> str2, ReadOnlySpan<char> str3)
+        {
+            string result = FastAllocateString(checked(str0.Length + str1.Length + str2.Length + str3.Length));
+            Span<char> resultSpan = new Span<char>(ref result.GetRawStringData(), result.Length);
+
+            str0.CopyTo(resultSpan);
+            resultSpan = resultSpan.Slice(str0.Length);
+
+            str1.CopyTo(resultSpan);
+            resultSpan = resultSpan.Slice(str1.Length);
+
+            str2.CopyTo(resultSpan);
+            resultSpan = resultSpan.Slice(str2.Length);
+
+            str3.CopyTo(resultSpan);
+
+            return result;
+        }
+
         public static string Concat(params string[] values)
         {
             if (values == null)