Skip to content

Commit eeeeb7a

Browse files
Optimize string.EndsWith(char) for const values (#69038)
1 parent 6a00977 commit eeeeb7a

File tree

32 files changed

+102
-76
lines changed

32 files changed

+102
-76
lines changed

src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2079,7 +2079,7 @@ private static void FilterHelper(
20792079
listType = MemberListType.CaseSensitive;
20802080
}
20812081

2082-
if (allowPrefixLookup && name.EndsWith("*", StringComparison.Ordinal))
2082+
if (allowPrefixLookup && name.EndsWith('*'))
20832083
{
20842084
// We set prefixLookup to true if name ends with a "*".
20852085
// We will also set listType to All so that all members are included in

src/libraries/Common/src/System/Net/CookieParser.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -855,10 +855,9 @@ private static FieldInfo IsQuotedVersionField
855855

856856
internal static string CheckQuoted(string value)
857857
{
858-
if (value.Length < 2 || value[0] != '\"' || value[value.Length - 1] != '\"')
859-
return value;
860-
861-
return value.Length == 2 ? string.Empty : value.Substring(1, value.Length - 2);
858+
return (value.Length >= 2 && value.StartsWith('\"') && value.EndsWith('\"'))
859+
? value.Substring(1, value.Length - 2)
860+
: value;
862861
}
863862

864863
internal bool EndofHeader()

src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Unix.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ internal static string GetPipePath(string serverName, string pipeName)
202202
// cross-platform with Windows (which has only '\' as an invalid char).
203203
if (Path.IsPathRooted(pipeName))
204204
{
205-
if (pipeName.IndexOfAny(s_invalidPathNameChars) >= 0 || pipeName[pipeName.Length - 1] == Path.DirectorySeparatorChar)
205+
if (pipeName.IndexOfAny(s_invalidPathNameChars) >= 0 || pipeName.EndsWith(Path.DirectorySeparatorChar))
206206
throw new PlatformNotSupportedException(SR.PlatformNotSupported_InvalidPipeNameChars);
207207

208208
// Caller is in full control of file location.

src/libraries/System.Net.Http/src/System/Net/Http/Headers/CacheControlHeaderValue.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ private static bool TrySetOptionalTokenList(NameValueHeaderValue nameValue, ref
537537
// We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we
538538
// have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespace.
539539
string valueString = nameValue.Value;
540-
if ((valueString.Length < 3) || (valueString[0] != '\"') || (valueString[valueString.Length - 1] != '\"'))
540+
if ((valueString.Length < 3) || !valueString.StartsWith('\"') || !valueString.EndsWith('\"'))
541541
{
542542
return false;
543543
}

src/libraries/System.Net.Http/src/System/Net/Http/Headers/NameValueHeaderValue.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ private static void CheckValueFormat(string? value)
358358
}
359359

360360
// Trailing/leading space are not allowed
361-
if (value[0] == ' ' || value[0] == '\t' || value[^1] == ' ' || value[^1] == '\t')
361+
if (value.StartsWith(' ') || value.StartsWith('\t') || value.EndsWith(' ') || value.EndsWith('\t'))
362362
{
363363
ThrowFormatException(value);
364364
}

src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ internal static string ReadBufferAsString(ArraySegment<byte> buffer, HttpContent
192192
{
193193
// Remove at most a single set of quotes.
194194
if (charset.Length > 2 &&
195-
charset[0] == '\"' &&
196-
charset[charset.Length - 1] == '\"')
195+
charset.StartsWith('\"') &&
196+
charset.EndsWith('\"'))
197197
{
198198
encoding = Encoding.GetEncoding(charset.Substring(1, charset.Length - 2));
199199
}

src/libraries/System.Net.HttpListener/src/System/Net/HttpListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ internal void AddPrefix(string uriPrefix)
146146
{
147147
throw new ArgumentException(SR.net_listener_host, nameof(uriPrefix));
148148
}
149-
if (uriPrefix[uriPrefix.Length - 1] != '/')
149+
if (!uriPrefix.EndsWith('/'))
150150
{
151151
throw new ArgumentException(SR.net_listener_slash, nameof(uriPrefix));
152152
}

src/libraries/System.Net.HttpListener/src/System/Net/HttpListenerRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,8 +537,8 @@ internal string GetString()
537537

538538
internal static void FillFromString(NameValueCollection nvc, string s, bool urlencoded, Encoding encoding)
539539
{
540+
int i = s.StartsWith('?') ? 1 : 0;
540541
int l = s.Length;
541-
int i = (l > 0 && s[0] == '?') ? 1 : 0;
542542

543543
while (i < l)
544544
{

src/libraries/System.Net.HttpListener/src/System/Net/Managed/HttpEndPointListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public static void UnbindContext(HttpListenerContext context)
178178
string host = uri.Host;
179179
int port = uri.Port;
180180
string path = WebUtility.UrlDecode(uri.AbsolutePath);
181-
string pathSlash = path[path.Length - 1] == '/' ? path : path + "/";
181+
string pathSlash = path.EndsWith('/') ? path : path + "/";
182182

183183
HttpListener? bestMatch = null;
184184
int bestLength = -1;

src/libraries/System.Net.Mail/src/System/Net/Mail/Attachment.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ public string ContentId
204204
ContentId = cid;
205205
return cid;
206206
}
207-
if (cid.Length >= 2 && cid[0] == '<' && cid[cid.Length - 1] == '>')
207+
if (cid.StartsWith('<') && cid.EndsWith('>'))
208208
{
209209
return cid.Substring(1, cid.Length - 2);
210210
}

src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,9 @@ private static bool TryParse(string address, string? displayName, Encoding? disp
141141
return false;
142142
}
143143

144-
if (displayName.Length >= 2 && displayName[0] == '\"' && displayName[^1] == '\"')
144+
if (displayName.Length >= 2 && displayName.StartsWith('\"') && displayName.EndsWith('\"'))
145145
{
146-
// Peal bounding quotes, they'll get re-added later.
146+
// Peel bounding quotes, they'll get re-added later.
147147
displayName = displayName.Substring(1, displayName.Length - 2);
148148
}
149149
}

src/libraries/System.Net.Primitives/src/System/Net/Cookie.cs

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,11 @@ public string Name
235235
}
236236
internal bool InternalSetName(string? value)
237237
{
238-
if (string.IsNullOrEmpty(value) || value[0] == '$' || value.IndexOfAny(ReservedToName) != -1 || value[0] == ' ' || value[value.Length - 1] == ' ')
238+
if (string.IsNullOrEmpty(value)
239+
|| value.StartsWith('$')
240+
|| value.StartsWith(' ')
241+
|| value.EndsWith(' ')
242+
|| value.IndexOfAny(ReservedToName) >= 0)
239243
{
240244
m_name = string.Empty;
241245
return false;
@@ -339,7 +343,11 @@ internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDoma
339343
}
340344

341345
// Check the name
342-
if (string.IsNullOrEmpty(m_name) || m_name[0] == '$' || m_name.IndexOfAny(ReservedToName) != -1 || m_name[0] == ' ' || m_name[m_name.Length - 1] == ' ')
346+
if (string.IsNullOrEmpty(m_name) ||
347+
m_name.StartsWith('$') ||
348+
m_name.StartsWith(' ') ||
349+
m_name.EndsWith(' ') ||
350+
m_name.IndexOfAny(ReservedToName) >= 0)
343351
{
344352
if (shouldThrow)
345353
{
@@ -350,7 +358,7 @@ internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDoma
350358

351359
// Check the value
352360
if (m_value == null ||
353-
(!(m_value.Length > 2 && m_value[0] == '\"' && m_value[m_value.Length - 1] == '\"') && m_value.IndexOfAny(ReservedToValue) != -1))
361+
(!(m_value.Length > 2 && m_value.StartsWith('\"') && m_value.EndsWith('\"')) && m_value.IndexOfAny(ReservedToValue) >= 0))
354362
{
355363
if (shouldThrow)
356364
{
@@ -360,8 +368,8 @@ internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDoma
360368
}
361369

362370
// Check Comment syntax
363-
if (Comment != null && !(Comment.Length > 2 && Comment[0] == '\"' && Comment[Comment.Length - 1] == '\"')
364-
&& (Comment.IndexOfAny(ReservedToValue) != -1))
371+
if (Comment != null && !(Comment.Length > 2 && Comment.StartsWith('\"') && Comment.EndsWith('\"'))
372+
&& (Comment.IndexOfAny(ReservedToValue) >= 0))
365373
{
366374
if (shouldThrow)
367375
{
@@ -371,8 +379,8 @@ internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDoma
371379
}
372380

373381
// Check Path syntax
374-
if (Path != null && !(Path.Length > 2 && Path[0] == '\"' && Path[Path.Length - 1] == '\"')
375-
&& (Path.IndexOfAny(ReservedToValue) != -1))
382+
if (Path != null && !(Path.Length > 2 && Path.StartsWith('\"') && Path.EndsWith('\"'))
383+
&& (Path.IndexOfAny(ReservedToValue) >= 0))
376384
{
377385
if (shouldThrow)
378386
{
@@ -498,7 +506,7 @@ internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDoma
498506
// Note: Normally Uri.AbsolutePath contains at least one "/" after parsing,
499507
// but it's possible construct Uri with an empty path using a custom UriParser
500508
int lastSlash;
501-
if (path.Length == 0 || path[0] != '/' || (lastSlash = path.LastIndexOf('/')) == 0)
509+
if (!path.StartsWith('/') || (lastSlash = path.LastIndexOf('/')) == 0)
502510
{
503511
m_path = "/";
504512
break;
@@ -587,35 +595,29 @@ public string Port
587595
else
588596
{
589597
// Parse port list
590-
if (value[0] != '\"' || value[value.Length - 1] != '\"')
598+
if (!value.StartsWith('\"') || !value.EndsWith('\"'))
591599
{
592600
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
593601
}
594-
string[] ports = value.Split(PortSplitDelimiters);
595-
596-
List<int> portList = new List<int>();
597-
int port;
602+
string[] ports = value.Split(PortSplitDelimiters, StringSplitOptions.RemoveEmptyEntries);
603+
int[] parsedPorts = new int[ports.Length];
598604

599605
for (int i = 0; i < ports.Length; ++i)
600606
{
601-
// Skip spaces
602-
if (ports[i] != string.Empty)
607+
if (!int.TryParse(ports[i], out int port))
603608
{
604-
if (!int.TryParse(ports[i], out port))
605-
{
606-
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
607-
}
608-
609-
// valid values for port 0 - 0xFFFF
610-
if ((port < 0) || (port > 0xFFFF))
611-
{
612-
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
613-
}
609+
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
610+
}
614611

615-
portList.Add(port);
612+
// valid values for port 0 - 0xFFFF
613+
if ((port < 0) || (port > 0xFFFF))
614+
{
615+
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
616616
}
617+
618+
parsedPorts[i] = port;
617619
}
618-
m_port_list = portList.ToArray();
620+
m_port_list = parsedPorts;
619621
m_port = value;
620622
m_version = MaxSupportedVersion;
621623
m_cookieVariant = CookieVariant.Rfc2965;

src/libraries/System.Net.Primitives/src/System/Net/CookieContainer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ private static bool PathMatch(string requestPath, string cookiePath)
927927
if (!requestPath.StartsWith(cookiePath, StringComparison.Ordinal))
928928
return false;
929929
return requestPath.Length == cookiePath.Length ||
930-
cookiePath.Length > 0 && cookiePath[^1] == '/' ||
930+
cookiePath.EndsWith('/') ||
931931
requestPath[cookiePath.Length] == '/';
932932
}
933933

src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -602,8 +602,7 @@ protected override PipelineEntry[] BuildCommandsList(WebRequest req)
602602
commandList.Add(new PipelineEntry(FormatFtpCommand("RNFR", baseDir + requestFilename), flags));
603603

604604
string renameTo;
605-
if (!string.IsNullOrEmpty(request.RenameTo)
606-
&& request.RenameTo.StartsWith("/", StringComparison.OrdinalIgnoreCase))
605+
if (request.RenameTo is not null && request.RenameTo.StartsWith('/'))
607606
{
608607
renameTo = request.RenameTo; // Absolute path
609608
}
@@ -774,7 +773,7 @@ private static void GetPathInfo(GetPathOption pathOption,
774773
}
775774

776775
// strip off trailing '/' on directory if present
777-
if (directory.Length > 1 && directory[directory.Length - 1] == '/')
776+
if (directory.Length > 1 && directory.EndsWith('/'))
778777
directory = directory.Substring(0, directory.Length - 1);
779778
}
780779

@@ -954,11 +953,11 @@ private void TryUpdateResponseUri(string str, FtpWebRequest request)
954953
escapedFilename = escapedFilename.Replace("#", "%23");
955954

956955
// help us out if the user forgot to add a slash to the directory name
957-
string orginalPath = baseUri.AbsolutePath;
958-
if (orginalPath.Length > 0 && orginalPath[orginalPath.Length - 1] != '/')
956+
string originalPath = baseUri.AbsolutePath;
957+
if (originalPath.Length > 0 && !originalPath.EndsWith('/'))
959958
{
960959
UriBuilder uriBuilder = new UriBuilder(baseUri);
961-
uriBuilder.Path = orginalPath + "/";
960+
uriBuilder.Path = originalPath + "/";
962961
baseUri = uriBuilder.Uri;
963962
}
964963

src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ private static string GetFolderPathCoreWithoutValidation(SpecialFolder folder)
9393
// "$XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored."
9494
// "If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used."
9595
string? data = GetEnvironmentVariable("XDG_DATA_HOME");
96-
if (string.IsNullOrEmpty(data) || data[0] != '/')
96+
if (data is null || !data.StartsWith('/'))
9797
{
9898
data = Path.Combine(home, ".local", "share");
9999
}
@@ -137,7 +137,7 @@ private static string GetXdgConfig(string home)
137137
// "$XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored."
138138
// "If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used."
139139
string? config = GetEnvironmentVariable("XDG_CONFIG_HOME");
140-
if (string.IsNullOrEmpty(config) || config[0] != '/')
140+
if (config is null || !config.StartsWith('/'))
141141
{
142142
config = Path.Combine(home, ".config");
143143
}
@@ -151,7 +151,7 @@ private static string ReadXdgDirectory(string homeDir, string key, string fallba
151151
Debug.Assert(!string.IsNullOrEmpty(fallback), $"Expected non-empty fallback");
152152

153153
string? envPath = GetEnvironmentVariable(key);
154-
if (!string.IsNullOrEmpty(envPath) && envPath[0] == '/')
154+
if (envPath is not null && envPath.StartsWith('/'))
155155
{
156156
return envPath;
157157
}

src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Nls.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,14 @@ private static int[] ConvertWin32GroupString(string win32Str)
290290
return new int[] { 3 };
291291
}
292292

293-
if (win32Str[0] == '0')
293+
if (win32Str.StartsWith('0'))
294294
{
295295
return new int[] { 0 };
296296
}
297297

298298
// Since its in n;n;n;n;n format, we can always get the length quickly
299299
int[] values;
300-
if (win32Str[^1] == '0')
300+
if (win32Str.EndsWith('0'))
301301
{
302302
// Trailing 0 gets dropped. 1;0 -> 1
303303
values = new int[win32Str.Length / 2];

src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -999,7 +999,7 @@ internal string EnglishName
999999
// Our existing names mostly look like:
10001000
// "English" + "United States" -> "English (United States)"
10011001
// "Azeri (Latin)" + "Azerbaijan" -> "Azeri (Latin, Azerbaijan)"
1002-
if (EnglishLanguageName[^1] == ')')
1002+
if (EnglishLanguageName.EndsWith(')'))
10031003
{
10041004
// "Azeri (Latin)" + "Azerbaijan" -> "Azeri (Latin, Azerbaijan)"
10051005
englishDisplayName = string.Concat(

src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormatInfoScanner.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ internal void AddDateWordOrPostfix(string? formatPostfix, string str)
228228
m_dateWords.Add(str);
229229
}
230230

231-
if (str[^1] == '.')
231+
if (str.EndsWith('.'))
232232
{
233233
// Old version ignore the trailing dot in the date words. Support this as well.
234234
string strWithoutDot = str[0..^1];

src/libraries/System.Private.CoreLib/src/System/Reflection/Module.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ private static bool FilterTypeNameImpl(Type cls, object filterCriteria, StringCo
182182
throw new InvalidFilterCriteriaException(SR.InvalidFilterCriteriaException_CritString);
183183
}
184184
// Check to see if this is a prefix or exact match requirement
185-
if (str.Length > 0 && str[^1] == '*')
185+
if (str.EndsWith('*'))
186186
{
187187
ReadOnlySpan<char> slice = str.AsSpan(0, str.Length - 1);
188188
return cls.Name.AsSpan().StartsWith(slice, comparison);

src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,20 @@ public bool EndsWith(string value, bool ignoreCase, CultureInfo? culture)
594594

595595
public bool EndsWith(char value)
596596
{
597+
// If the string is empty, *(&_firstChar + length - 1) will deref within
598+
// the _stringLength field, which will be all-zero. We must forbid '\0'
599+
// from going down the optimized code path because otherwise empty strings
600+
// would appear to end with '\0', which is incorrect.
601+
// n.b. This optimization relies on the layout of string and is not valid
602+
// for other data types like char[] or Span<char>.
603+
if (RuntimeHelpers.IsKnownConstant(value) && value != '\0')
604+
{
605+
// deref Length now to front-load the null check; also take this time to zero-extend
606+
// n.b. (localLength - 1) could be negative!
607+
nuint localLength = (uint)Length;
608+
return Unsafe.Add(ref _firstChar, (nint)localLength - 1) == value;
609+
}
610+
597611
int lastPos = Length - 1;
598612
return ((uint)lastPos < (uint)Length) && this[lastPos] == value;
599613
}
@@ -1004,10 +1018,16 @@ public bool StartsWith(string value, bool ignoreCase, CultureInfo? culture)
10041018

10051019
public bool StartsWith(char value)
10061020
{
1021+
// If the string is empty, _firstChar will contain the null terminator.
1022+
// We forbid '\0' from going down the optimized code path because otherwise
1023+
// empty strings would appear to begin with '\0', which is incorrect.
1024+
// n.b. This optimization relies on the layout of string and is not valid
1025+
// for other data types like char[] or Span<char>.
10071026
if (RuntimeHelpers.IsKnownConstant(value) && value != '\0')
10081027
{
10091028
return _firstChar == value;
10101029
}
1030+
10111031
return Length != 0 && _firstChar == value;
10121032
}
10131033

src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlBaseWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ public override void WriteComment(string? text)
442442
{
443443
text = string.Empty;
444444
}
445-
else if (text.IndexOf("--", StringComparison.Ordinal) != -1 || (text.Length > 0 && text[text.Length - 1] == '-'))
445+
else if (text.Contains("--") || text.StartsWith('-'))
446446
{
447447
throw System.Runtime.Serialization.DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ArgumentException(SR.XmlInvalidCommentChars, nameof(text)));
448448
}

0 commit comments

Comments
 (0)