Skip to content

Commit 14f696d

Browse files
authored
Merge pull request #42 from ullmark/perf
Performance updates
2 parents f3891f8 + 8fc5b79 commit 14f696d

5 files changed

Lines changed: 125 additions & 120 deletions

File tree

src/Hashids.net/ArrayExtensions.cs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
52
using System.Buffers;
63

74
namespace HashidsNet
85
{
96
public static class ArrayExtensions
107
{
11-
public static T[] Copy<T>(this T[] array)
12-
{
13-
return SubArray(array, 0, array.Length);
14-
}
15-
168
public static T[] SubArray<T>(this T[] array, int index)
179
{
1810
return SubArray(array, index, array.Length - index);
@@ -53,4 +45,4 @@ public static void ReturnToPool<T>(this T[] array)
5345
ArrayPool<T>.Shared.Return(array);
5446
}
5547
}
56-
}
48+
}

src/Hashids.net/Hashids.cs

Lines changed: 98 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Text;
55
using System.Text.RegularExpressions;
6+
using Microsoft.Extensions.ObjectPool;
67

78
namespace HashidsNet
89
{
@@ -24,9 +25,11 @@ public partial class Hashids : IHashids
2425
private char[] _salt;
2526
private readonly int _minHashLength;
2627

27-
// Creates the Regex in the first usage, speed up first use of non hex methods
28-
private static readonly Lazy<Regex> hexValidator = new Lazy<Regex>(() => new Regex("^[0-9a-fA-F]+$", RegexOptions.Compiled));
29-
private static readonly Lazy<Regex> hexSplitter = new Lazy<Regex>(() => new Regex(@"[\w\W]{1,12}", RegexOptions.Compiled));
28+
private readonly ObjectPool<StringBuilder> _sbPool = new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy());
29+
30+
// Creates the Regex in the first usage, speed up first use of non-hex methods
31+
private static readonly Lazy<Regex> hexValidator = new(() => new Regex("^[0-9a-fA-F]+$", RegexOptions.Compiled));
32+
private static readonly Lazy<Regex> hexSplitter = new(() => new Regex(@"[\w\W]{1,12}", RegexOptions.Compiled));
3033

3134
/// <summary>
3235
/// Instantiates a new Hashids with the default setup.
@@ -70,6 +73,57 @@ public Hashids(
7073
SetupGuards();
7174
}
7275

76+
private void SetupSeps()
77+
{
78+
// seps should contain only characters present in alphabet;
79+
_seps = _seps.Intersect(_alphabet).ToArray();
80+
81+
// alphabet should not contain seps.
82+
_alphabet = _alphabet.Except(_seps).ToArray();
83+
84+
ConsistentShuffle(_seps, _seps.Length, _salt, _salt.Length);
85+
86+
if (_seps.Length == 0 || ((float) _alphabet.Length / _seps.Length) > SEP_DIV)
87+
{
88+
var sepsLength = (int) Math.Ceiling((float) _alphabet.Length / SEP_DIV);
89+
90+
if (sepsLength == 1)
91+
{
92+
sepsLength = 2;
93+
}
94+
95+
if (sepsLength > _seps.Length)
96+
{
97+
var diff = sepsLength - _seps.Length;
98+
_seps = _seps.Append(_alphabet, 0, diff);
99+
_alphabet = _alphabet.SubArray(diff);
100+
}
101+
else
102+
{
103+
_seps = _seps.SubArray(0, sepsLength);
104+
}
105+
}
106+
107+
ConsistentShuffle(_alphabet, _alphabet.Length, _salt, _salt.Length);
108+
}
109+
110+
private void SetupGuards()
111+
{
112+
var guardCount = (int) Math.Ceiling(_alphabet.Length / GUARD_DIV);
113+
114+
if (_alphabet.Length < 3)
115+
{
116+
_guards = _seps.SubArray(0, guardCount);
117+
_seps = _seps.SubArray(guardCount);
118+
}
119+
120+
else
121+
{
122+
_guards = _alphabet.SubArray(0, guardCount);
123+
_alphabet = _alphabet.SubArray(guardCount);
124+
}
125+
}
126+
73127
/// <summary>
74128
/// Encodes the provided numbers into a hashed string.
75129
/// </summary>
@@ -105,46 +159,6 @@ public virtual int[] Decode(string hash)
105159
return Array.ConvertAll(numbers, n => (int) n);
106160
}
107161

108-
/// <summary>
109-
/// Encodes the provided hex string to a hashids hash.
110-
/// </summary>
111-
/// <param name="hex"></param>
112-
/// <returns></returns>
113-
public virtual string EncodeHex(string hex)
114-
{
115-
if (!hexValidator.Value.IsMatch(hex))
116-
return string.Empty;
117-
118-
var matches = hexSplitter.Value.Matches(hex);
119-
var numbers = new List<long>(matches.Count);
120-
121-
foreach (Match match in matches)
122-
{
123-
var number = Convert.ToInt64(string.Concat("1", match.Value), 16);
124-
numbers.Add(number);
125-
}
126-
127-
return EncodeLong(numbers.ToArray());
128-
}
129-
130-
/// <summary>
131-
/// Decodes the provided hash into a hex-string.
132-
/// </summary>
133-
/// <param name="hash"></param>
134-
/// <returns></returns>
135-
public virtual string DecodeHex(string hash)
136-
{
137-
var builder = new StringBuilder();
138-
var numbers = DecodeLong(hash);
139-
140-
foreach (var number in numbers)
141-
{
142-
builder.Append(string.Format("{0:X}", number).Substring(1));
143-
}
144-
145-
return builder.ToString();
146-
}
147-
148162
/// <summary>
149163
/// Decodes the provided hashed string into an array of longs.
150164
/// </summary>
@@ -178,55 +192,45 @@ public string EncodeLong(IEnumerable<long> numbers)
178192
return EncodeLong(numbers.ToArray());
179193
}
180194

181-
private void SetupSeps()
195+
/// <summary>
196+
/// Encodes the provided hex string to a hashids hash.
197+
/// </summary>
198+
/// <param name="hex"></param>
199+
/// <returns></returns>
200+
public virtual string EncodeHex(string hex)
182201
{
183-
// seps should contain only characters present in alphabet;
184-
_seps = _seps.Intersect(_alphabet).ToArray();
185-
186-
// alphabet should not contain seps.
187-
_alphabet = _alphabet.Except(_seps).ToArray();
202+
if (!hexValidator.Value.IsMatch(hex))
203+
return string.Empty;
188204

189-
ConsistentShuffle(_seps, _seps.Length, _salt, _salt.Length);
205+
var matches = hexSplitter.Value.Matches(hex);
206+
var numbers = new List<long>(matches.Count);
190207

191-
if (_seps.Length == 0 || ((float) _alphabet.Length / _seps.Length) > SEP_DIV)
208+
foreach (Match match in matches)
192209
{
193-
var sepsLength = (int) Math.Ceiling((float) _alphabet.Length / SEP_DIV);
194-
195-
if (sepsLength == 1)
196-
{
197-
sepsLength = 2;
198-
}
199-
200-
if (sepsLength > _seps.Length)
201-
{
202-
var diff = sepsLength - _seps.Length;
203-
_seps = _seps.Append(_alphabet, 0, diff);
204-
_alphabet = _alphabet.SubArray(diff);
205-
}
206-
else
207-
{
208-
_seps = _seps.SubArray(0, sepsLength);
209-
}
210+
var number = Convert.ToInt64(string.Concat("1", match.Value), 16);
211+
numbers.Add(number);
210212
}
211213

212-
ConsistentShuffle(_alphabet, _alphabet.Length, _salt, _salt.Length);
214+
return EncodeLong(numbers.ToArray());
213215
}
214216

215-
private void SetupGuards()
217+
/// <summary>
218+
/// Decodes the provided hash into a hex-string.
219+
/// </summary>
220+
/// <param name="hash"></param>
221+
/// <returns></returns>
222+
public virtual string DecodeHex(string hash)
216223
{
217-
var guardCount = (int) Math.Ceiling(_alphabet.Length / GUARD_DIV);
224+
var builder = _sbPool.Get();
225+
var numbers = DecodeLong(hash);
218226

219-
if (_alphabet.Length < 3)
220-
{
221-
_guards = _seps.SubArray(0, guardCount);
222-
_seps = _seps.SubArray(guardCount);
223-
}
227+
foreach (var number in numbers)
228+
foreach (var ch in number.ToString("X").AsSpan().Slice(1))
229+
builder.Append(ch);
224230

225-
else
226-
{
227-
_guards = _alphabet.SubArray(0, guardCount);
228-
_alphabet = _alphabet.SubArray(guardCount);
229-
}
231+
var result = builder.ToString();
232+
_sbPool.Return(builder);
233+
return result;
230234
}
231235

232236
/// <summary>
@@ -245,7 +249,7 @@ private string GenerateHashFrom(long[] numbers)
245249
numbersHashInt += numbers[i] % (i + 100);
246250
}
247251

248-
var builder = new StringBuilder();
252+
var builder = _sbPool.Get();
249253

250254
char[] buffer = null;
251255
var alphabet = _alphabet.CopyPooled();
@@ -320,15 +324,9 @@ private string GenerateHashFrom(long[] numbers)
320324
buffer.ReturnToPool();
321325
}
322326

323-
return builder.ToString();
324-
}
325-
326-
private char[] CreateBuffer(int alphabetLength, char lottery)
327-
{
328-
var buffer = System.Buffers.ArrayPool<char>.Shared.Rent(alphabetLength);
329-
buffer[0] = lottery;
330-
Array.Copy(_salt, 0, buffer, 1, Math.Min(_salt.Length, alphabetLength - 1));
331-
return buffer;
327+
var result = builder.ToString();
328+
_sbPool.Return(builder);
329+
return result;
332330
}
333331

334332
private char[] Hash(long input, char[] alphabet, int alphabetLength)
@@ -352,25 +350,12 @@ private long Unhash(string input, char[] alphabet, int alphabetLength)
352350
for (var i = 0; i < input.Length; i++)
353351
{
354352
var pos = Array.IndexOf(alphabet, input[i]);
355-
number += pos * LongPow(alphabetLength, input.Length - i - 1);
353+
number = number * alphabetLength + pos;
356354
}
357355

358356
return number;
359357
}
360358

361-
private static long LongPow(int target, int power)
362-
{
363-
if (power == 0) return 1;
364-
long result = target;
365-
while (power > 1)
366-
{
367-
result *= target;
368-
power--;
369-
}
370-
371-
return result;
372-
}
373-
374359
private long[] GetNumbersFrom(string hash)
375360
{
376361
if (string.IsNullOrWhiteSpace(hash))
@@ -432,6 +417,14 @@ private long[] GetNumbersFrom(string hash)
432417
return result.ToArray();
433418
}
434419

420+
private char[] CreateBuffer(int alphabetLength, char lottery)
421+
{
422+
var buffer = System.Buffers.ArrayPool<char>.Shared.Rent(alphabetLength);
423+
buffer[0] = lottery;
424+
Array.Copy(_salt, 0, buffer, 1, Math.Min(_salt.Length, alphabetLength - 1));
425+
return buffer;
426+
}
427+
435428
private void ConsistentShuffle(char[] alphabet, int alphabetLength, char[] salt, int saltLength)
436429
{
437430
if (salt.Length == 0)

src/Hashids.net/Hashids.net.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@
1818
</PropertyGroup>
1919

2020
<ItemGroup>
21+
<PackageReference Include="Microsoft.Extensions.ObjectPool">
22+
<Version>5.0.5</Version>
23+
</PackageReference>
2124
<PackageReference Include="System.Buffers">
2225
<Version>4.5.1</Version>
2326
</PackageReference>
27+
<PackageReference Include="System.Memory">
28+
<Version>4.5.4</Version>
29+
</PackageReference>
2430
</ItemGroup>
2531

2632
</Project>

src/Hashids.net/IHashids.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System;
2-
using System.Collections;
3-
using System.Collections.Generic;
1+
using System.Collections.Generic;
42

53
namespace HashidsNet
64
{

test/Hashids.net.benchmark/Program.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,35 @@ public static void Main(string[] args)
1414
public class HashBenchmark
1515
{
1616
private readonly HashidsNet.Hashids _hashids;
17-
private readonly int[] _input = { 12345, 1234567890, int.MaxValue };
17+
private readonly int[] _ints = { 12345, 1234567890, int.MaxValue };
18+
private readonly long[] _longs = { 12345, 1234567890123456789, long.MaxValue };
19+
private readonly string _hex = "507f1f77bcf86cd799439011";
1820

1921
public HashBenchmark()
2022
{
2123
_hashids = new HashidsNet.Hashids();
2224
}
2325

2426
[Benchmark]
25-
public void Run()
27+
public void RoundtripInts()
2628
{
27-
var encodedValue = _hashids.Encode(_input);
29+
var encodedValue = _hashids.Encode(_ints);
2830
var decodedValue = _hashids.Decode(encodedValue);
2931
}
32+
33+
[Benchmark]
34+
public void RoundtripLongs()
35+
{
36+
var encodedValue = _hashids.EncodeLong(_longs);
37+
var decodedValue = _hashids.DecodeLong(encodedValue);
38+
}
39+
40+
[Benchmark]
41+
public void RoundtripHex()
42+
{
43+
var encodedValue = _hashids.EncodeHex(_hex);
44+
var decodedValue = _hashids.DecodeHex(encodedValue);
45+
}
3046
}
3147
}
3248
}

0 commit comments

Comments
 (0)