Skip to content

Commit

Permalink
Optimize AWSSDKUtils.ToHex() for speed and memory (#3293)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenaw authored Jun 5, 2024
1 parent 40f7871 commit 59f087d
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 26 deletions.
5 changes: 1 addition & 4 deletions sdk/src/Core/AWSSDK.Core.NetStandard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,10 @@
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
<GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>

<LangVersion>9.0</LangVersion>
<NoWarn>$(NoWarn);CS1591;CA1822</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<SignAssembly>True</SignAssembly>
</PropertyGroup>
<!-- Async Enumerable Compatibility -->
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
<WarningsAsErrors>IL2026,IL2075</WarningsAsErrors>
Expand Down
71 changes: 53 additions & 18 deletions sdk/src/Core/Amazon.Util/AWSSDKUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

using Amazon.Runtime.Internal.Util;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
Expand Down Expand Up @@ -757,14 +758,32 @@ public static long ConvertTimeSpanToMilliseconds(TimeSpan timeSpan)
/// <returns>String version of the data</returns>
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<char>.Shared.Rent(data.Length * 2);

try
{
ToHexString(data, chars, lowercase);

return new string(chars, 0, data.Length * 2);
}
finally
{
ArrayPool<char>.Shared.Return(chars);
}
#endif
}

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

private static void ToHexString(Span<byte> source, Span<char> destination, bool lowercase)
{
Func<int, char> converter = lowercase ? (Func<int, char>)ToLowerHex : (Func<int, char>)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).
Expand All @@ -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))
Expand Down Expand Up @@ -1316,18 +1363,6 @@ public static void Sleep(TimeSpan ts)
Sleep((int)ts.TotalMilliseconds);
}

/// <summary>
/// Convert bytes to a hex string
/// </summary>
/// <param name="value">Bytes to convert.</param>
/// <returns>Hexadecimal string representing the byte array.</returns>
public static string BytesToHexString(byte[] value)
{
string hex = BitConverter.ToString(value);
hex = hex.Replace("-", string.Empty);
return hex;
}

/// <summary>
/// Convert a hex string to bytes
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ This project file should not be used as part of a release pipeline.
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
<GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>

<LangVersion>9.0</LangVersion>
<NoWarn>CS1591,CS0612,CS0618,NU1701</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<SignAssembly>true</SignAssembly>
<LangVersion Condition="'$(TargetFramework)' == 'netstandard2.0'">8.0</LangVersion>
</PropertyGroup>
<Choose>
<When Condition=" '$(AWSKeyFile)' == '' ">
Expand Down
28 changes: 28 additions & 0 deletions sdk/test/NetStandard/UnitTests/Core/AWSSDKUtilsTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
14 changes: 14 additions & 0 deletions sdk/test/UnitTests/Custom/Util/AWSSDKUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.Reflection;
using Moq;
using Amazon.Util.Internal;
using System.Text;

namespace AWSSDK.UnitTests
{
Expand Down Expand Up @@ -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);
}
}
}

0 comments on commit 59f087d

Please sign in to comment.