Skip to content

Commit

Permalink
Improve base64 methods on .NET 6 (#712)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored Nov 6, 2024
1 parent 2c2c578 commit 15b12f5
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 36 deletions.
7 changes: 6 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ csharp_indent_labels = one_less_than_current
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_namespace_declarations = file_scoped:none
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_expression_bodied_constructors = false:silent
Expand All @@ -74,6 +74,7 @@ csharp_style_prefer_range_operator = false

# IDE1006: Naming Styles
dotnet_diagnostic.IDE1006.severity = silent
csharp_prefer_system_threading_lock = true:suggestion

[*.{cs,vb}]
#### Naming styles ####
Expand Down Expand Up @@ -138,3 +139,7 @@ dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_namespace_match_folder = true:suggestion
5 changes: 4 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Version
run: dotnet --version

- name: Test
run: dotnet test -c Release

- name: Test - Compiled
run: dotnet test --configuration Release /p:Compiled=true
2 changes: 1 addition & 1 deletion Common.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>

<BuildNumber Condition="'$(BuildNumber)' == ''">0</BuildNumber>
<VersionPrefix>2.5.1</VersionPrefix>
<VersionPrefix>2.0.0</VersionPrefix>
<VersionSuffix>beta-$(BuildNumber)</VersionSuffix>
<FileVersion>$(VersionPrefix).$(BuildNumber)</FileVersion>

Expand Down
2 changes: 1 addition & 1 deletion Fluid.Benchmarks/ComparisonBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Fluid.Benchmarks
{
[MemoryDiagnoser, GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[MemoryDiagnoser, GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), ShortRunJob]
public class ComparisonBenchmarks
{
private FluidBenchmarks _fluidBenchmarks = new FluidBenchmarks();
Expand Down
2 changes: 2 additions & 0 deletions Fluid.Benchmarks/Fluid.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<NoWarn>$(NoWarn);NU5104</NoWarn>
<!-- Ignore vulnerable packages imported for benchmarking only -->
<NoWarn>$(NoWarn);NU1903</NoWarn>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand Down
38 changes: 38 additions & 0 deletions Fluid.Benchmarks/HashingBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using BenchmarkDotNet.Attributes;
using System.Text;

namespace Fluid.Benchmarks
{
// | Method | Mean | Error | StdDev | Gen0 | Allocated |
// |------------------------ |---------:|----------:|---------:|-------:|----------:|
// | HashSha256ToHex | 213.7 ns | 48.62 ns | 2.67 ns | 0.0279 | 264 B |
// | HashSha256StringBuilder | 666.4 ns | 369.69 ns | 20.26 ns | 0.1774 | 1672 B |

[MemoryDiagnoser, ShortRunJob]
public class HashingBenchmarks
{
private readonly string _value = "this is some text to hash";

[Benchmark]
public string HashSha256ToHex()
{
var hash = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(_value));
return Fluid.Utils.HexUtilities.ToHexLower(hash);
}

[Benchmark]
public string HashSha256StringBuilder()
{
using var provider = System.Security.Cryptography.SHA256.Create();
var builder = new StringBuilder(64);
#pragma warning disable CA1850 // Prefer static 'System.Security.Cryptography.MD5.HashData' method over 'ComputeHash'
foreach (var b in provider.ComputeHash(Encoding.UTF8.GetBytes(_value)))
#pragma warning restore CA1850
{
builder.Append(b.ToString("x2"));
}

return builder.ToString();
}
}
}
59 changes: 46 additions & 13 deletions Fluid/Filters/MiscFilters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Fluid.Filters
{
public static class MiscFilters
{

private const char KebabCaseSeparator = '-';

public static FilterCollection WithMiscFilters(this FilterCollection filters)
Expand Down Expand Up @@ -53,7 +54,7 @@ public static ValueTask<FluidValue> Handleize(FluidValue input, FilterArguments
var result = new StringBuilder();
var lastIndex = value.Length - 1;

for (int i = 0; i < value.Length; i++)
for (var i = 0; i < value.Length; i++)
{
var currentChar = value[i];
var lookAheadChar = i == lastIndex
Expand Down Expand Up @@ -96,7 +97,7 @@ public static ValueTask<FluidValue> Handleize(FluidValue input, FilterArguments
{
if (IsCapitalLetter(currentChar))
{
if (result[result.Length - 1] != KebabCaseSeparator && !char.IsDigit(lookAheadChar))
if (result[^1] != KebabCaseSeparator && !char.IsDigit(lookAheadChar))
{
result.Append(KebabCaseSeparator);
}
Expand All @@ -106,7 +107,10 @@ public static ValueTask<FluidValue> Handleize(FluidValue input, FilterArguments
}
}

static bool IsCapitalLetter(char c) => c >= 'A' && c <= 'Z';
static bool IsCapitalLetter(char c)
{
return (uint)(c - 'A') <= (uint)('Z' - 'A');
}

return new StringValue(result.ToString().ToLowerInvariant());
}
Expand Down Expand Up @@ -272,7 +276,7 @@ public static ValueTask<FluidValue> StripHtml(FluidValue input, FilterArguments
var inside = false;
for (var i = 0; i < html.Length; i++)
{
char current = html[i];
var current = html[i];

switch (current)
{
Expand Down Expand Up @@ -410,7 +414,10 @@ void ForStrf(DateTimeOffset value, string format, StringBuilder result)
switch (c)
{
case 'a':
string AbbreviatedDayName() => context.CultureInfo.DateTimeFormat.AbbreviatedDayNames[(int)value.DayOfWeek];
string AbbreviatedDayName()
{
return context.CultureInfo.DateTimeFormat.AbbreviatedDayNames[(int)value.DayOfWeek];
}

var abbreviatedDayName = AbbreviatedDayName();
result.Append(upperCaseFlag ? abbreviatedDayName.ToUpperInvariant() : abbreviatedDayName);
Expand Down Expand Up @@ -720,7 +727,7 @@ private static async ValueTask WriteJson(Utf8JsonWriter writer, FluidValue input
foreach (var property in properties)
{
var name = conv(property);
#pragma warning disable CA1859 // Change type of variable 'fluidValue' from 'Fluid.Values.FluidValue' to 'Fluid.Values.StringValue' for improved performance
#pragma warning disable CA1859 // It's suggesting a wrong conversion (StringValue)
var fluidValue = await input.GetValueAsync(name, ctx);
#pragma warning restore CA1859
if (fluidValue.IsNil())
Expand Down Expand Up @@ -845,18 +852,28 @@ public static ValueTask<FluidValue> MD5(FluidValue input, FilterArguments argume
return StringValue.Empty;
}

// c.f. HashingBenchmarks

#if NET6_0_OR_GREATER
#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms
var hash = System.Security.Cryptography.MD5.HashData(Encoding.UTF8.GetBytes(value));
return new StringValue(Fluid.Utils.HexUtilities.ToHexLower(hash));
#pragma warning restore CA5351
#else

#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms
using var provider = System.Security.Cryptography.MD5.Create();
#pragma warning restore CA5351
var builder = new StringBuilder(32);
#pragma warning disable CA1850 // Prefer static 'System.Security.Cryptography.MD5.HashData' method over 'ComputeHash'
foreach (byte b in provider.ComputeHash(Encoding.UTF8.GetBytes(value)))
foreach (var b in provider.ComputeHash(Encoding.UTF8.GetBytes(value)))
#pragma warning restore CA1850
{
builder.Append(b.ToString("x2").ToLowerInvariant());
builder.Append(b.ToString("x2"));
}

return new StringValue(builder.ToString());
#endif
}

public static ValueTask<FluidValue> Sha1(FluidValue input, FilterArguments arguments, TemplateContext context)
Expand All @@ -867,18 +884,27 @@ public static ValueTask<FluidValue> Sha1(FluidValue input, FilterArguments argum
return StringValue.Empty;
}

// c.f. HashingBenchmarks

#if NET6_0_OR_GREATER
#pragma warning disable CA5350 // Do Not Use Broken Cryptographic Algorithms
var hash = System.Security.Cryptography.SHA1.HashData(Encoding.UTF8.GetBytes(value));
#pragma warning restore CA5350
return new StringValue(Fluid.Utils.HexUtilities.ToHexLower(hash));
#else
#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms
using var provider = System.Security.Cryptography.SHA1.Create();
#pragma warning restore CA5350
var builder = new StringBuilder(40);
#pragma warning disable CA1850 // Prefer static 'System.Security.Cryptography.MD5.HashData' method over 'ComputeHash'
foreach (byte b in provider.ComputeHash(Encoding.UTF8.GetBytes(value)))
foreach (var b in provider.ComputeHash(Encoding.UTF8.GetBytes(value)))
#pragma warning restore CA1850
{
builder.Append(b.ToString("x2").ToLowerInvariant());
builder.Append(b.ToString("x2"));
}

return new StringValue(builder.ToString());
#endif
}

public static ValueTask<FluidValue> Sha256(FluidValue input, FilterArguments arguments, TemplateContext context)
Expand All @@ -889,16 +915,23 @@ public static ValueTask<FluidValue> Sha256(FluidValue input, FilterArguments arg
return StringValue.Empty;
}

// c.f. HashingBenchmarks

#if NET6_0_OR_GREATER
var hash = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(value));
return new StringValue(Fluid.Utils.HexUtilities.ToHexLower(hash));
#else
using var provider = System.Security.Cryptography.SHA256.Create();
var builder = new StringBuilder(64);
#pragma warning disable CA1850 // Prefer static 'System.Security.Cryptography.MD5.HashData' method over 'ComputeHash'
foreach (byte b in provider.ComputeHash(Encoding.UTF8.GetBytes(value)))
#pragma warning disable CA1850 // Prefer static 'System.Security.Cryptography.SHA256.HashData' method over 'ComputeHash'
foreach (var b in provider.ComputeHash(Encoding.UTF8.GetBytes(value)))
#pragma warning restore CA1850
{
builder.Append(b.ToString("x2").ToLowerInvariant());
builder.Append(b.ToString("x2"));
}

return new StringValue(builder.ToString());
#endif
}
}
}
24 changes: 24 additions & 0 deletions Fluid/Utils/HexUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#if NET6_0_OR_GREATER

namespace Fluid.Utils;

public class HexUtilities
{
private static ReadOnlySpan<byte> HexChars => "0123456789abcdef"u8;

public static string ToHexLower(byte[] hash)
{
return string.Create(hash.Length * 2, hash, (span, hash) =>
{
var j = 0;
var length = hash.Length;
for (var i = 0; i < length; i++)
{
var b = hash[i];
span[j++] = (char)HexChars[(b >> 4) & 0x0f];
span[j++] = (char)HexChars[b & 0x0f];
}
});
}
}
#endif
38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1082,35 +1082,35 @@ For rendering, Fluid is 26% faster than the second, Handlebars, 5 times faster t
Compared to DotLiquid, Fluid renders 11 times faster, and allocates 35 times less memory.

``` text
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2033)
12th Gen Intel Core i7-1260P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 9.0.100-preview.4.24209.11
[Host] : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX2
.NET SDK 9.0.100-rc.2.24474.11
[Host] : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|------------------- |--------------:|------------:|------------:|-------:|--------:|----------:|---------:|--------:|------------:|------------:|
| Fluid_Parse | 2.849 us | 0.0191 us | 0.0159 us | 1.00 | 0.00 | 0.3052 | - | - | 2.81 KB | 1.00 |
| Scriban_Parse | 3.297 us | 0.0407 us | 0.0381 us | 1.16 | 0.01 | 0.7744 | 0.0267 | - | 7.14 KB | 2.54 |
| DotLiquid_Parse | 6.558 us | 0.1118 us | 0.1046 us | 2.30 | 0.03 | 1.7624 | 0.0229 | - | 16.21 KB | 5.76 |
| LiquidNet_Parse | 25.064 us | 0.1409 us | 0.1100 us | 8.80 | 0.07 | 6.7444 | 0.6104 | - | 62.04 KB | 22.06 |
| Handlebars_Parse | 2,401.901 us | 41.1672 us | 38.5079 us | 843.36 | 15.09 | 15.6250 | 7.8125 | - | 156.52 KB | 55.65 |
| Fluid_Parse | 3.393 us | 0.0628 us | 0.0524 us | 1.00 | 0.02 | 0.3052 | - | - | 2.81 KB | 1.00 |
| Scriban_Parse | 3.785 us | 0.0696 us | 0.1063 us | 1.12 | 0.04 | 0.7744 | 0.0267 | - | 7.14 KB | 2.54 |
| DotLiquid_Parse | 7.339 us | 0.1385 us | 0.1228 us | 2.16 | 0.05 | 1.7395 | - | - | 16.21 KB | 5.76 |
| LiquidNet_Parse | 28.002 us | 0.5425 us | 0.6663 us | 8.25 | 0.23 | 6.7444 | 0.6104 | - | 62.04 KB | 22.06 |
| Handlebars_Parse | 2,597.261 us | 30.8705 us | 27.3659 us | 765.59 | 13.89 | 15.6250 | 7.8125 | - | 156.37 KB | 55.60 |
| | | | | | | | | | | |
| Fluid_ParseBig | 16.257 us | 0.1450 us | 0.1357 us | 1.00 | 0.00 | 1.2512 | 0.0305 | - | 11.64 KB | 1.00 |
| Scriban_ParseBig | 18.521 us | 0.1000 us | 0.0886 us | 1.14 | 0.01 | 3.4790 | 0.4883 | - | 32.07 KB | 2.75 |
| DotLiquid_ParseBig | 27.612 us | 0.4320 us | 0.4041 us | 1.70 | 0.03 | 10.2539 | 0.4883 | - | 94.36 KB | 8.11 |
| LiquidNet_ParseBig | 12,206.204 us | 188.5327 us | 176.3536 us | 750.86 | 12.96 | 3093.7500 | 15.6250 | - | 28543.38 KB | 2,452.05 |
| Fluid_ParseBig | 17.882 us | 0.2029 us | 0.1584 us | 1.00 | 0.01 | 1.2512 | 0.0305 | - | 11.64 KB | 1.00 |
| Scriban_ParseBig | 19.891 us | 0.3979 us | 0.3907 us | 1.11 | 0.02 | 3.4790 | 0.4883 | - | 32.07 KB | 2.75 |
| DotLiquid_ParseBig | 30.766 us | 0.6128 us | 1.0069 us | 1.72 | 0.06 | 10.2539 | 0.4883 | - | 94.36 KB | 8.11 |
| LiquidNet_ParseBig | 14,207.006 us | 347.1824 us | 984.8987 us | 794.52 | 55.23 | 3093.7500 | 15.6250 | - | 28543.38 KB | 2,452.05 |
| | | | | | | | | | | |
| Fluid_Render | 134.311 us | 1.5910 us | 1.4104 us | 1.00 | 0.00 | 10.2539 | 0.4883 | - | 95.86 KB | 1.00 |
| Scriban_Render | 615.143 us | 5.4851 us | 4.5803 us | 4.58 | 0.06 | 68.3594 | 68.3594 | 68.3594 | 498.64 KB | 5.20 |
| DotLiquid_Render | 1,403.693 us | 27.4426 us | 40.2251 us | 10.63 | 0.27 | 351.5625 | 140.6250 | 23.4375 | 3368.09 KB | 35.13 |
| LiquidNet_Render | 825.819 us | 8.6639 us | 7.6803 us | 6.15 | 0.08 | 339.8438 | 160.1563 | - | 3130.8 KB | 32.66 |
| Handlebars_Render | 238.959 us | 4.7119 us | 11.5585 us | 1.68 | 0.06 | 20.9961 | 3.4180 | - | 194.92 KB | 2.03 |
| Fluid_Render | 158.640 us | 3.1074 us | 7.4451 us | 1.00 | 0.06 | 10.2539 | 0.4883 | - | 95.87 KB | 1.00 |
| Handlebars_Render | 216.572 us | 4.2552 us | 9.1598 us | 1.37 | 0.08 | 20.9961 | 3.4180 | - | 194.92 KB | 2.03 |
| Scriban_Render | 768.660 us | 14.5379 us | 29.3673 us | 4.86 | 0.28 | 68.3594 | 68.3594 | 68.3594 | 498.65 KB | 5.20 |
| LiquidNet_Render | 1,073.246 us | 20.1804 us | 21.5928 us | 6.78 | 0.33 | 339.8438 | 160.1563 | - | 3130.83 KB | 32.66 |
| DotLiquid_Render | 1,812.898 us | 52.1755 us | 148.8597 us | 11.45 | 1.07 | 351.5625 | 140.6250 | 23.4375 | 3368.09 KB | 35.13 |
```

Tested on May 28, 2024 with
- Scriban 5.10.0
- Scriban 5.11.0
- DotLiquid 2.2.692
- Liquid.NET 0.10.0
- Handlebars.Net 2.1.6
Expand Down

0 comments on commit 15b12f5

Please sign in to comment.