Skip to content

Commit 59f087d

Browse files
authored
Optimize AWSSDKUtils.ToHex() for speed and memory (#3293)
1 parent 40f7871 commit 59f087d

File tree

6 files changed

+100
-26
lines changed

6 files changed

+100
-26
lines changed

sdk/src/Core/AWSSDK.Core.NetStandard.csproj

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@
2020
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
2121
<GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
2222

23+
<LangVersion>9.0</LangVersion>
2324
<NoWarn>$(NoWarn);CS1591;CA1822</NoWarn>
2425
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
2526
<SignAssembly>True</SignAssembly>
26-
</PropertyGroup>
27-
<!-- Async Enumerable Compatibility -->
28-
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
29-
<LangVersion>8.0</LangVersion>
3027
</PropertyGroup>
3128
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
3229
<WarningsAsErrors>IL2026,IL2075</WarningsAsErrors>

sdk/src/Core/Amazon.Util/AWSSDKUtils.cs

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
using Amazon.Runtime.Internal.Util;
2222
using System;
23+
using System.Buffers;
2324
using System.Collections.Generic;
2425
using System.Globalization;
2526
using System.IO;
@@ -757,14 +758,32 @@ public static long ConvertTimeSpanToMilliseconds(TimeSpan timeSpan)
757758
/// <returns>String version of the data</returns>
758759
public static string ToHex(byte[] data, bool lowercase)
759760
{
760-
StringBuilder sb = new StringBuilder();
761-
762-
for (int i = 0; i < data.Length; i++)
761+
#if NET8_0_OR_GREATER
762+
if (!lowercase)
763763
{
764-
sb.Append(data[i].ToString(lowercase ? "x2" : "X2", CultureInfo.InvariantCulture));
764+
return Convert.ToHexString(data);
765765
}
766+
#endif
766767

767-
return sb.ToString();
768+
#if NETCOREAPP3_1_OR_GREATER
769+
return string.Create(data.Length * 2, (data, lowercase), static (chars, state) =>
770+
{
771+
ToHexString(state.data, chars, state.lowercase);
772+
});
773+
#else
774+
char[] chars = ArrayPool<char>.Shared.Rent(data.Length * 2);
775+
776+
try
777+
{
778+
ToHexString(data, chars, lowercase);
779+
780+
return new string(chars, 0, data.Length * 2);
781+
}
782+
finally
783+
{
784+
ArrayPool<char>.Shared.Return(chars);
785+
}
786+
#endif
768787
}
769788

770789
/// <summary>
@@ -1149,6 +1168,23 @@ public static string UrlEncode(int rfcNumber, string data, bool path)
11491168
return encoded.ToString();
11501169
}
11511170

1171+
private static void ToHexString(Span<byte> source, Span<char> destination, bool lowercase)
1172+
{
1173+
Func<int, char> converter = lowercase ? (Func<int, char>)ToLowerHex : (Func<int, char>)ToUpperHex;
1174+
1175+
for (int i = source.Length - 1; i >= 0; i--)
1176+
{
1177+
// Break apart the byte into two four-bit components and
1178+
// then convert each into their hexadecimal equivalent.
1179+
byte b = source[i];
1180+
int hiNibble = b >> 4;
1181+
int loNibble = b & 0xF;
1182+
1183+
destination[i * 2] = converter(hiNibble);
1184+
destination[i * 2 + 1] = converter(loNibble);
1185+
}
1186+
}
1187+
11521188
private static char ToUpperHex(int value)
11531189
{
11541190
// Maps 0-9 to the Unicode range of '0' - '9' (0x30 - 0x39).
@@ -1159,7 +1195,18 @@ private static char ToUpperHex(int value)
11591195
// Maps 10-15 to the Unicode range of 'A' - 'F' (0x41 - 0x46).
11601196
return (char)(value - 10 + 'A');
11611197
}
1162-
1198+
1199+
private static char ToLowerHex(int value)
1200+
{
1201+
// Maps 0-9 to the Unicode range of '0' - '9' (0x30 - 0x39).
1202+
if (value <= 9)
1203+
{
1204+
return (char)(value + '0');
1205+
}
1206+
// Maps 10-15 to the Unicode range of 'a' - 'f' (0x61 - 0x66).
1207+
return (char)(value - 10 + 'a');
1208+
}
1209+
11631210
internal static string UrlEncodeSlash(string data)
11641211
{
11651212
if (string.IsNullOrEmpty(data))
@@ -1316,18 +1363,6 @@ public static void Sleep(TimeSpan ts)
13161363
Sleep((int)ts.TotalMilliseconds);
13171364
}
13181365

1319-
/// <summary>
1320-
/// Convert bytes to a hex string
1321-
/// </summary>
1322-
/// <param name="value">Bytes to convert.</param>
1323-
/// <returns>Hexadecimal string representing the byte array.</returns>
1324-
public static string BytesToHexString(byte[] value)
1325-
{
1326-
string hex = BitConverter.ToString(value);
1327-
hex = hex.Replace("-", string.Empty);
1328-
return hex;
1329-
}
1330-
13311366
/// <summary>
13321367
/// Convert a hex string to bytes
13331368
/// </summary>

sdk/src/Services/S3/Custom/Internal/AmazonS3ResponseHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,8 @@ private static void CompareHashes(string etag, byte[] hash)
191191
return;
192192

193193
etag = etag.Trim(etagTrimChars);
194-
195-
string hexHash = AWSSDKUtils.BytesToHexString(hash);
194+
195+
string hexHash = AWSSDKUtils.ToHex(hash, false);
196196
if (!string.Equals(etag, hexHash, StringComparison.OrdinalIgnoreCase))
197197
throw new AmazonClientException("Expected hash not equal to calculated hash");
198198
}

sdk/test/NetStandard/UnitTests/AWSSDK.UnitTests.Custom.NetStandard.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ This project file should not be used as part of a release pipeline.
2323
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
2424
<GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
2525

26+
<LangVersion>9.0</LangVersion>
2627
<NoWarn>CS1591,CS0612,CS0618,NU1701</NoWarn>
27-
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
28+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
2829
<SignAssembly>true</SignAssembly>
29-
<LangVersion Condition="'$(TargetFramework)' == 'netstandard2.0'">8.0</LangVersion>
3030
</PropertyGroup>
3131
<Choose>
3232
<When Condition=" '$(AWSKeyFile)' == '' ">
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Amazon.Util;
2+
using System.Text;
3+
using Xunit;
4+
5+
namespace UnitTests.NetStandard.Core
6+
{
7+
[Trait("Category", "Core")]
8+
public class AWSSDKUtilsTests
9+
{
10+
[Fact]
11+
public void ToHexUppercase()
12+
{
13+
var bytes = Encoding.UTF8.GetBytes("Hello World");
14+
var hexString = AWSSDKUtils.ToHex(bytes, false);
15+
16+
Assert.Equal("48656C6C6F20576F726C64", hexString);
17+
}
18+
19+
[Fact]
20+
public void ToHexLowercase()
21+
{
22+
var bytes = Encoding.UTF8.GetBytes("Hello World");
23+
var hexString = AWSSDKUtils.ToHex(bytes, true);
24+
25+
Assert.Equal("48656c6c6f20576f726c64", hexString);
26+
}
27+
}
28+
}

sdk/test/UnitTests/Custom/Util/AWSSDKUtilsTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System.Reflection;
2020
using Moq;
2121
using Amazon.Util.Internal;
22+
using System.Text;
2223

2324
namespace AWSSDK.UnitTests
2425
{
@@ -164,5 +165,18 @@ public void ConvertFromUnixEpochMilliseconds()
164165

165166
Assert.AreEqual(expectedDateTime, dateTime);
166167
}
168+
169+
[TestCategory("UnitTest")]
170+
[TestCategory("Util")]
171+
[DataRow("Hello World", true, "48656c6c6f20576f726c64")]
172+
[DataRow("Hello World", false, "48656C6C6F20576F726C64")]
173+
[DataTestMethod]
174+
public void ToHex(string input, bool lowercase, string expectedResult)
175+
{
176+
var bytes = Encoding.UTF8.GetBytes(input);
177+
var hexString = AWSSDKUtils.ToHex(bytes, lowercase);
178+
179+
Assert.AreEqual(expectedResult, hexString);
180+
}
167181
}
168182
}

0 commit comments

Comments
 (0)