From 59f087daaca4edddfa2003254ed29322cb2ab7fd Mon Sep 17 00:00:00 2001 From: Steven Weerdenburg Date: Tue, 4 Jun 2024 23:15:14 -0400 Subject: [PATCH] Optimize `AWSSDKUtils.ToHex()` for speed and memory (#3293) --- sdk/src/Core/AWSSDK.Core.NetStandard.csproj | 5 +- sdk/src/Core/Amazon.Util/AWSSDKUtils.cs | 71 ++++++++++++++----- .../Internal/AmazonS3ResponseHandler.cs | 4 +- ...AWSSDK.UnitTests.Custom.NetStandard.csproj | 4 +- .../UnitTests/Core/AWSSDKUtilsTests.cs | 28 ++++++++ .../UnitTests/Custom/Util/AWSSDKUtilsTests.cs | 14 ++++ 6 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 sdk/test/NetStandard/UnitTests/Core/AWSSDKUtilsTests.cs diff --git a/sdk/src/Core/AWSSDK.Core.NetStandard.csproj b/sdk/src/Core/AWSSDK.Core.NetStandard.csproj index 765c0c38c269..3357152edb6a 100644 --- a/sdk/src/Core/AWSSDK.Core.NetStandard.csproj +++ b/sdk/src/Core/AWSSDK.Core.NetStandard.csproj @@ -20,13 +20,10 @@ false false + 9.0 $(NoWarn);CS1591;CA1822 true True - - - - 8.0 IL2026,IL2075 diff --git a/sdk/src/Core/Amazon.Util/AWSSDKUtils.cs b/sdk/src/Core/Amazon.Util/AWSSDKUtils.cs index 47fb116ac302..0b23e6149e06 100644 --- a/sdk/src/Core/Amazon.Util/AWSSDKUtils.cs +++ b/sdk/src/Core/Amazon.Util/AWSSDKUtils.cs @@ -20,6 +20,7 @@ using Amazon.Runtime.Internal.Util; using System; +using System.Buffers; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -757,14 +758,32 @@ public static long ConvertTimeSpanToMilliseconds(TimeSpan timeSpan) /// String version of the data public static string ToHex(byte[] data, bool lowercase) { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < data.Length; i++) +#if NET8_0_OR_GREATER + if (!lowercase) { - sb.Append(data[i].ToString(lowercase ? "x2" : "X2", CultureInfo.InvariantCulture)); + return Convert.ToHexString(data); } +#endif - return sb.ToString(); +#if NETCOREAPP3_1_OR_GREATER + return string.Create(data.Length * 2, (data, lowercase), static (chars, state) => + { + ToHexString(state.data, chars, state.lowercase); + }); +#else + char[] chars = ArrayPool.Shared.Rent(data.Length * 2); + + try + { + ToHexString(data, chars, lowercase); + + return new string(chars, 0, data.Length * 2); + } + finally + { + ArrayPool.Shared.Return(chars); + } +#endif } /// @@ -1149,6 +1168,23 @@ public static string UrlEncode(int rfcNumber, string data, bool path) return encoded.ToString(); } + private static void ToHexString(Span source, Span destination, bool lowercase) + { + Func converter = lowercase ? (Func)ToLowerHex : (Func)ToUpperHex; + + for (int i = source.Length - 1; i >= 0; i--) + { + // Break apart the byte into two four-bit components and + // then convert each into their hexadecimal equivalent. + byte b = source[i]; + int hiNibble = b >> 4; + int loNibble = b & 0xF; + + destination[i * 2] = converter(hiNibble); + destination[i * 2 + 1] = converter(loNibble); + } + } + private static char ToUpperHex(int value) { // Maps 0-9 to the Unicode range of '0' - '9' (0x30 - 0x39). @@ -1159,7 +1195,18 @@ private static char ToUpperHex(int value) // Maps 10-15 to the Unicode range of 'A' - 'F' (0x41 - 0x46). return (char)(value - 10 + 'A'); } - + + private static char ToLowerHex(int value) + { + // Maps 0-9 to the Unicode range of '0' - '9' (0x30 - 0x39). + if (value <= 9) + { + return (char)(value + '0'); + } + // Maps 10-15 to the Unicode range of 'a' - 'f' (0x61 - 0x66). + return (char)(value - 10 + 'a'); + } + internal static string UrlEncodeSlash(string data) { if (string.IsNullOrEmpty(data)) @@ -1316,18 +1363,6 @@ public static void Sleep(TimeSpan ts) Sleep((int)ts.TotalMilliseconds); } - /// - /// Convert bytes to a hex string - /// - /// Bytes to convert. - /// Hexadecimal string representing the byte array. - public static string BytesToHexString(byte[] value) - { - string hex = BitConverter.ToString(value); - hex = hex.Replace("-", string.Empty); - return hex; - } - /// /// Convert a hex string to bytes /// diff --git a/sdk/src/Services/S3/Custom/Internal/AmazonS3ResponseHandler.cs b/sdk/src/Services/S3/Custom/Internal/AmazonS3ResponseHandler.cs index b9d517ea88cc..d6fa9dcd0c22 100644 --- a/sdk/src/Services/S3/Custom/Internal/AmazonS3ResponseHandler.cs +++ b/sdk/src/Services/S3/Custom/Internal/AmazonS3ResponseHandler.cs @@ -191,8 +191,8 @@ private static void CompareHashes(string etag, byte[] hash) return; etag = etag.Trim(etagTrimChars); - - string hexHash = AWSSDKUtils.BytesToHexString(hash); + + string hexHash = AWSSDKUtils.ToHex(hash, false); if (!string.Equals(etag, hexHash, StringComparison.OrdinalIgnoreCase)) throw new AmazonClientException("Expected hash not equal to calculated hash"); } diff --git a/sdk/test/NetStandard/UnitTests/AWSSDK.UnitTests.Custom.NetStandard.csproj b/sdk/test/NetStandard/UnitTests/AWSSDK.UnitTests.Custom.NetStandard.csproj index 7904f350479b..9cf3acac6a13 100644 --- a/sdk/test/NetStandard/UnitTests/AWSSDK.UnitTests.Custom.NetStandard.csproj +++ b/sdk/test/NetStandard/UnitTests/AWSSDK.UnitTests.Custom.NetStandard.csproj @@ -23,10 +23,10 @@ This project file should not be used as part of a release pipeline. false false + 9.0 CS1591,CS0612,CS0618,NU1701 - true + true true - 8.0 diff --git a/sdk/test/NetStandard/UnitTests/Core/AWSSDKUtilsTests.cs b/sdk/test/NetStandard/UnitTests/Core/AWSSDKUtilsTests.cs new file mode 100644 index 000000000000..c7e5574664b4 --- /dev/null +++ b/sdk/test/NetStandard/UnitTests/Core/AWSSDKUtilsTests.cs @@ -0,0 +1,28 @@ +using Amazon.Util; +using System.Text; +using Xunit; + +namespace UnitTests.NetStandard.Core +{ + [Trait("Category", "Core")] + public class AWSSDKUtilsTests + { + [Fact] + public void ToHexUppercase() + { + var bytes = Encoding.UTF8.GetBytes("Hello World"); + var hexString = AWSSDKUtils.ToHex(bytes, false); + + Assert.Equal("48656C6C6F20576F726C64", hexString); + } + + [Fact] + public void ToHexLowercase() + { + var bytes = Encoding.UTF8.GetBytes("Hello World"); + var hexString = AWSSDKUtils.ToHex(bytes, true); + + Assert.Equal("48656c6c6f20576f726c64", hexString); + } + } +} diff --git a/sdk/test/UnitTests/Custom/Util/AWSSDKUtilsTests.cs b/sdk/test/UnitTests/Custom/Util/AWSSDKUtilsTests.cs index 14201fab0e96..d81f11cc2b45 100644 --- a/sdk/test/UnitTests/Custom/Util/AWSSDKUtilsTests.cs +++ b/sdk/test/UnitTests/Custom/Util/AWSSDKUtilsTests.cs @@ -19,6 +19,7 @@ using System.Reflection; using Moq; using Amazon.Util.Internal; +using System.Text; namespace AWSSDK.UnitTests { @@ -164,5 +165,18 @@ public void ConvertFromUnixEpochMilliseconds() Assert.AreEqual(expectedDateTime, dateTime); } + + [TestCategory("UnitTest")] + [TestCategory("Util")] + [DataRow("Hello World", true, "48656c6c6f20576f726c64")] + [DataRow("Hello World", false, "48656C6C6F20576F726C64")] + [DataTestMethod] + public void ToHex(string input, bool lowercase, string expectedResult) + { + var bytes = Encoding.UTF8.GetBytes(input); + var hexString = AWSSDKUtils.ToHex(bytes, lowercase); + + Assert.AreEqual(expectedResult, hexString); + } } }