Skip to content

Commit

Permalink
Optimize string.EndsWith(char) for const values (#69038)
Browse files Browse the repository at this point in the history
  • Loading branch information
GrabYourPitchforks committed May 20, 2022
1 parent 6a00977 commit eeeeb7a
Show file tree
Hide file tree
Showing 32 changed files with 102 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2079,7 +2079,7 @@ internal static BindingFlags FilterPreCalculate(bool isPublic, bool isInherited,
listType = MemberListType.CaseSensitive;
}

if (allowPrefixLookup && name.EndsWith("*", StringComparison.Ordinal))
if (allowPrefixLookup && name.EndsWith('*'))
{
// We set prefixLookup to true if name ends with a "*".
// We will also set listType to All so that all members are included in
Expand Down
7 changes: 3 additions & 4 deletions src/libraries/Common/src/System/Net/CookieParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -855,10 +855,9 @@ private static FieldInfo IsQuotedVersionField

internal static string CheckQuoted(string value)
{
if (value.Length < 2 || value[0] != '\"' || value[value.Length - 1] != '\"')
return value;

return value.Length == 2 ? string.Empty : value.Substring(1, value.Length - 2);
return (value.Length >= 2 && value.StartsWith('\"') && value.EndsWith('\"'))
? value.Substring(1, value.Length - 2)
: value;
}

internal bool EndofHeader()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ internal static string GetPipePath(string serverName, string pipeName)
// cross-platform with Windows (which has only '\' as an invalid char).
if (Path.IsPathRooted(pipeName))
{
if (pipeName.IndexOfAny(s_invalidPathNameChars) >= 0 || pipeName[pipeName.Length - 1] == Path.DirectorySeparatorChar)
if (pipeName.IndexOfAny(s_invalidPathNameChars) >= 0 || pipeName.EndsWith(Path.DirectorySeparatorChar))
throw new PlatformNotSupportedException(SR.PlatformNotSupported_InvalidPipeNameChars);

// Caller is in full control of file location.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ private static bool TrySetTokenOnlyValue(NameValueHeaderValue nameValue, ref boo
// We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we
// have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespace.
string valueString = nameValue.Value;
if ((valueString.Length < 3) || (valueString[0] != '\"') || (valueString[valueString.Length - 1] != '\"'))
if ((valueString.Length < 3) || !valueString.StartsWith('\"') || !valueString.EndsWith('\"'))
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ private static void CheckValueFormat(string? value)
}

// Trailing/leading space are not allowed
if (value[0] == ' ' || value[0] == '\t' || value[^1] == ' ' || value[^1] == '\t')
if (value.StartsWith(' ') || value.StartsWith('\t') || value.EndsWith(' ') || value.EndsWith('\t'))
{
ThrowFormatException(value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ internal static string ReadBufferAsString(ArraySegment<byte> buffer, HttpContent
{
// Remove at most a single set of quotes.
if (charset.Length > 2 &&
charset[0] == '\"' &&
charset[charset.Length - 1] == '\"')
charset.StartsWith('\"') &&
charset.EndsWith('\"'))
{
encoding = Encoding.GetEncoding(charset.Substring(1, charset.Length - 2));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ internal void AddPrefix(string uriPrefix)
{
throw new ArgumentException(SR.net_listener_host, nameof(uriPrefix));
}
if (uriPrefix[uriPrefix.Length - 1] != '/')
if (!uriPrefix.EndsWith('/'))
{
throw new ArgumentException(SR.net_listener_slash, nameof(uriPrefix));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,8 +537,8 @@ internal string GetString()

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

while (i < l)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public static void UnbindContext(HttpListenerContext context)
string host = uri.Host;
int port = uri.Port;
string path = WebUtility.UrlDecode(uri.AbsolutePath);
string pathSlash = path[path.Length - 1] == '/' ? path : path + "/";
string pathSlash = path.EndsWith('/') ? path : path + "/";

HttpListener? bestMatch = null;
int bestLength = -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public string ContentId
ContentId = cid;
return cid;
}
if (cid.Length >= 2 && cid[0] == '<' && cid[cid.Length - 1] == '>')
if (cid.StartsWith('<') && cid.EndsWith('>'))
{
return cid.Substring(1, cid.Length - 2);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ private static bool TryParse(string address, string? displayName, Encoding? disp
return false;
}

if (displayName.Length >= 2 && displayName[0] == '\"' && displayName[^1] == '\"')
if (displayName.Length >= 2 && displayName.StartsWith('\"') && displayName.EndsWith('\"'))
{
// Peal bounding quotes, they'll get re-added later.
// Peel bounding quotes, they'll get re-added later.
displayName = displayName.Substring(1, displayName.Length - 2);
}
}
Expand Down
56 changes: 29 additions & 27 deletions src/libraries/System.Net.Primitives/src/System/Net/Cookie.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,11 @@ public string Name
}
internal bool InternalSetName(string? value)
{
if (string.IsNullOrEmpty(value) || value[0] == '$' || value.IndexOfAny(ReservedToName) != -1 || value[0] == ' ' || value[value.Length - 1] == ' ')
if (string.IsNullOrEmpty(value)
|| value.StartsWith('$')
|| value.StartsWith(' ')
|| value.EndsWith(' ')
|| value.IndexOfAny(ReservedToName) >= 0)
{
m_name = string.Empty;
return false;
Expand Down Expand Up @@ -339,7 +343,11 @@ internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDoma
}

// Check the name
if (string.IsNullOrEmpty(m_name) || m_name[0] == '$' || m_name.IndexOfAny(ReservedToName) != -1 || m_name[0] == ' ' || m_name[m_name.Length - 1] == ' ')
if (string.IsNullOrEmpty(m_name) ||
m_name.StartsWith('$') ||
m_name.StartsWith(' ') ||
m_name.EndsWith(' ') ||
m_name.IndexOfAny(ReservedToName) >= 0)
{
if (shouldThrow)
{
Expand All @@ -350,7 +358,7 @@ internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDoma

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

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

// Check Path syntax
if (Path != null && !(Path.Length > 2 && Path[0] == '\"' && Path[Path.Length - 1] == '\"')
&& (Path.IndexOfAny(ReservedToValue) != -1))
if (Path != null && !(Path.Length > 2 && Path.StartsWith('\"') && Path.EndsWith('\"'))
&& (Path.IndexOfAny(ReservedToValue) >= 0))
{
if (shouldThrow)
{
Expand Down Expand Up @@ -498,7 +506,7 @@ internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDoma
// Note: Normally Uri.AbsolutePath contains at least one "/" after parsing,
// but it's possible construct Uri with an empty path using a custom UriParser
int lastSlash;
if (path.Length == 0 || path[0] != '/' || (lastSlash = path.LastIndexOf('/')) == 0)
if (!path.StartsWith('/') || (lastSlash = path.LastIndexOf('/')) == 0)
{
m_path = "/";
break;
Expand Down Expand Up @@ -587,35 +595,29 @@ public string Port
else
{
// Parse port list
if (value[0] != '\"' || value[value.Length - 1] != '\"')
if (!value.StartsWith('\"') || !value.EndsWith('\"'))
{
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
}
string[] ports = value.Split(PortSplitDelimiters);

List<int> portList = new List<int>();
int port;
string[] ports = value.Split(PortSplitDelimiters, StringSplitOptions.RemoveEmptyEntries);
int[] parsedPorts = new int[ports.Length];

for (int i = 0; i < ports.Length; ++i)
{
// Skip spaces
if (ports[i] != string.Empty)
if (!int.TryParse(ports[i], out int port))
{
if (!int.TryParse(ports[i], out port))
{
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
}

// valid values for port 0 - 0xFFFF
if ((port < 0) || (port > 0xFFFF))
{
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
}
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
}

portList.Add(port);
// valid values for port 0 - 0xFFFF
if ((port < 0) || (port > 0xFFFF))
{
throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
}

parsedPorts[i] = port;
}
m_port_list = portList.ToArray();
m_port_list = parsedPorts;
m_port = value;
m_version = MaxSupportedVersion;
m_cookieVariant = CookieVariant.Rfc2965;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ private static bool PathMatch(string requestPath, string cookiePath)
if (!requestPath.StartsWith(cookiePath, StringComparison.Ordinal))
return false;
return requestPath.Length == cookiePath.Length ||
cookiePath.Length > 0 && cookiePath[^1] == '/' ||
cookiePath.EndsWith('/') ||
requestPath[cookiePath.Length] == '/';
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,8 +602,7 @@ protected override PipelineEntry[] BuildCommandsList(WebRequest req)
commandList.Add(new PipelineEntry(FormatFtpCommand("RNFR", baseDir + requestFilename), flags));

string renameTo;
if (!string.IsNullOrEmpty(request.RenameTo)
&& request.RenameTo.StartsWith("/", StringComparison.OrdinalIgnoreCase))
if (request.RenameTo is not null && request.RenameTo.StartsWith('/'))
{
renameTo = request.RenameTo; // Absolute path
}
Expand Down Expand Up @@ -774,7 +773,7 @@ private enum GetPathOption
}

// strip off trailing '/' on directory if present
if (directory.Length > 1 && directory[directory.Length - 1] == '/')
if (directory.Length > 1 && directory.EndsWith('/'))
directory = directory.Substring(0, directory.Length - 1);
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ private static string GetFolderPathCoreWithoutValidation(SpecialFolder folder)
// "$XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored."
// "If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used."
string? data = GetEnvironmentVariable("XDG_DATA_HOME");
if (string.IsNullOrEmpty(data) || data[0] != '/')
if (data is null || !data.StartsWith('/'))
{
data = Path.Combine(home, ".local", "share");
}
Expand Down Expand Up @@ -137,7 +137,7 @@ private static string GetXdgConfig(string home)
// "$XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored."
// "If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used."
string? config = GetEnvironmentVariable("XDG_CONFIG_HOME");
if (string.IsNullOrEmpty(config) || config[0] != '/')
if (config is null || !config.StartsWith('/'))
{
config = Path.Combine(home, ".config");
}
Expand All @@ -151,7 +151,7 @@ private static string ReadXdgDirectory(string homeDir, string key, string fallba
Debug.Assert(!string.IsNullOrEmpty(fallback), $"Expected non-empty fallback");

string? envPath = GetEnvironmentVariable(key);
if (!string.IsNullOrEmpty(envPath) && envPath[0] == '/')
if (envPath is not null && envPath.StartsWith('/'))
{
return envPath;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,14 +290,14 @@ private static int[] ConvertWin32GroupString(string win32Str)
return new int[] { 3 };
}

if (win32Str[0] == '0')
if (win32Str.StartsWith('0'))
{
return new int[] { 0 };
}

// Since its in n;n;n;n;n format, we can always get the length quickly
int[] values;
if (win32Str[^1] == '0')
if (win32Str.EndsWith('0'))
{
// Trailing 0 gets dropped. 1;0 -> 1
values = new int[win32Str.Length / 2];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ internal string EnglishName
// Our existing names mostly look like:
// "English" + "United States" -> "English (United States)"
// "Azeri (Latin)" + "Azerbaijan" -> "Azeri (Latin, Azerbaijan)"
if (EnglishLanguageName[^1] == ')')
if (EnglishLanguageName.EndsWith(')'))
{
// "Azeri (Latin)" + "Azerbaijan" -> "Azeri (Latin, Azerbaijan)"
englishDisplayName = string.Concat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ internal void AddDateWordOrPostfix(string? formatPostfix, string str)
m_dateWords.Add(str);
}

if (str[^1] == '.')
if (str.EndsWith('.'))
{
// Old version ignore the trailing dot in the date words. Support this as well.
string strWithoutDot = str[0..^1];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ private static bool FilterTypeNameImpl(Type cls, object filterCriteria, StringCo
throw new InvalidFilterCriteriaException(SR.InvalidFilterCriteriaException_CritString);
}
// Check to see if this is a prefix or exact match requirement
if (str.Length > 0 && str[^1] == '*')
if (str.EndsWith('*'))
{
ReadOnlySpan<char> slice = str.AsSpan(0, str.Length - 1);
return cls.Name.AsSpan().StartsWith(slice, comparison);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,20 @@ public bool EndsWith(string value, bool ignoreCase, CultureInfo? culture)

public bool EndsWith(char value)
{
// If the string is empty, *(&_firstChar + length - 1) will deref within
// the _stringLength field, which will be all-zero. We must forbid '\0'
// from going down the optimized code path because otherwise empty strings
// would appear to end with '\0', which is incorrect.
// n.b. This optimization relies on the layout of string and is not valid
// for other data types like char[] or Span<char>.
if (RuntimeHelpers.IsKnownConstant(value) && value != '\0')
{
// deref Length now to front-load the null check; also take this time to zero-extend
// n.b. (localLength - 1) could be negative!
nuint localLength = (uint)Length;
return Unsafe.Add(ref _firstChar, (nint)localLength - 1) == value;
}

int lastPos = Length - 1;
return ((uint)lastPos < (uint)Length) && this[lastPos] == value;
}
Expand Down Expand Up @@ -1004,10 +1018,16 @@ public bool StartsWith(string value, bool ignoreCase, CultureInfo? culture)

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

return Length != 0 && _firstChar == value;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ public override void WriteComment(string? text)
{
text = string.Empty;
}
else if (text.IndexOf("--", StringComparison.Ordinal) != -1 || (text.Length > 0 && text[text.Length - 1] == '-'))
else if (text.Contains("--") || text.StartsWith('-'))
{
throw System.Runtime.Serialization.DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ArgumentException(SR.XmlInvalidCommentChars, nameof(text)));
}
Expand Down
Loading

0 comments on commit eeeeb7a

Please sign in to comment.