Skip to content

Commit a9ccddb

Browse files
committed
accelerate alphabet indexof lookups with lookup table, and special cased cyclic lookup to avoid modulo
1 parent ea368e9 commit a9ccddb

File tree

9 files changed

+96
-37
lines changed

9 files changed

+96
-37
lines changed

perf/Science.Cryptography.Ciphers.Benchmarks/v1v2/CaesarBruteforceBenchmarks.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ public string Decrypt(string ciphertext, int key)
9595

9696
public static int IndexOfIgnoreCase(string source, char subject)
9797
{
98-
Char toCompare = subject.ToUpper();
98+
Char toCompare = subject.ToUpperInvariant();
9999

100100
for (int i = 0; i < source.Length; i++)
101101
{
102-
if (source[i].ToUpper() == toCompare)
102+
if (source[i].ToUpperInvariant() == toCompare)
103103
return i;
104104
}
105105

src/Science.Cryptography.Ciphers.Analysis/FrequencyAnalysis.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public static AbsoluteCharacterFrequencies Analyze(ReadOnlySpan<char> text) =>
7474
Analyze(text, _ => true, EqualityComparer<char>.Default);
7575

7676
public static AbsoluteCharacterFrequencies Analyze(ReadOnlySpan<char> text, Alphabet alphabet) =>
77-
Analyze(text, c => alphabet.Contains(c, StringComparison.OrdinalIgnoreCase), IgnoreCaseCharComparer.Instance);
77+
Analyze(text, c => alphabet.ContainsIgnoreCase(c), IgnoreCaseCharComparer.Instance);
7878

7979
public static double Compare(RelativeCharacterFrequencies reference, RelativeCharacterFrequencies subject) =>
8080
CompareCore(reference, subject);

src/Science.Cryptography.Ciphers.Analysis/NGramAnalysis.Two.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ internal static void AnalyzeAsciiLetterTwoGrams(ReadOnlySpan<char> text, Diction
1212
var lastIndex = text.Length - 2;
1313
for (int i = 0; i <= lastIndex; i++)
1414
{
15-
var c1 = text[i].ToUpper();
15+
var c1 = text[i].ToUpperInvariant();
1616
if (!c1.IsUpperAsciiLetter())
1717
{
1818
continue;
1919
}
2020

21-
var c2 = text[i + 1].ToUpper();
21+
var c2 = text[i + 1].ToUpperInvariant();
2222
if (!c2.IsUpperAsciiLetter())
2323
{
2424
i++;
@@ -39,7 +39,7 @@ internal static int GetTwoGramKey(ReadOnlySpan<char> segment)
3939
throw new ArgumentOutOfRangeException(nameof(segment));
4040
}
4141

42-
return GetTwoGramKey(segment[0].ToUpper(), segment[1].ToUpper());
42+
return GetTwoGramKey(segment[0].ToUpperInvariant(), segment[1].ToUpperInvariant());
4343
}
4444

4545
[MethodImpl(MethodImplOptions.AggressiveInlining)]

src/Science.Cryptography.Ciphers.Specialized/MalespinSlang.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ protected override void Crypt(ReadOnlySpan<char> input, Span<char> output, out i
2222
written = input.Length;
2323
}
2424

25-
private static char Translate(char ch) => (ch.ToUpper() switch
25+
private static char Translate(char ch) => (ch.ToUpperInvariant() switch
2626
{
2727
'A' => 'E',
2828
'E' => 'A',

src/Science.Cryptography.Ciphers/Alphabet.cs

+64-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections;
3+
using System.Collections.Frozen;
34
using System.Collections.Generic;
45
using System.Linq;
56

@@ -9,26 +10,34 @@ public class Alphabet : IReadOnlyList<char>, IEquatable<Alphabet>
910
{
1011
public Alphabet(ReadOnlySpan<char> characters)
1112
{
12-
// convert to uppercase
13-
var upper = new char[characters.Length];
14-
characters.ToUpperInvariant(upper);
13+
// build index lookup
14+
var lookup = new Dictionary<char, int>(characters.Length);
15+
for (int i = 0; i < characters.Length; i++)
16+
{
17+
lookup.Add(characters[i], i);
18+
}
19+
_indexLookup = lookup.ToFrozenDictionary();
1520

16-
// check for duplicates
17-
for (int i = 0; i < upper.Length - 1; i++)
21+
// build index lookup (case-insensitive)
22+
var lookupUpper = new Dictionary<char, int>(characters.Length);
23+
for (int i = 0; i < characters.Length; i++)
1824
{
19-
var remaining = upper.AsSpan()[(i + 1)..];
20-
if (remaining.Contains(upper[i]))
21-
{
22-
throw new ArgumentException("Duplicate character found in the alphabet.", nameof(characters));
23-
}
25+
lookupUpper.Add(characters[i].ToUpperInvariant(), i);
2426
}
27+
_indexLookupUpper = lookupUpper.ToFrozenDictionary();
2528

26-
_chars = upper;
27-
_str = new string(_chars);
29+
_chars = characters.ToArray();
30+
_str = new string(characters);
31+
DoubleLength = characters.Length * 2;
32+
MinusLength = -characters.Length;
2833
}
2934

3035
private readonly char[] _chars;
3136
private readonly string _str;
37+
private readonly FrozenDictionary<char, int> _indexLookup;
38+
private readonly FrozenDictionary<char, int> _indexLookupUpper;
39+
private readonly int DoubleLength;
40+
private readonly int MinusLength;
3241

3342
public static Alphabet FromKeyword(string keyword, Alphabet @base, bool throwOnDuplicates = false)
3443
{
@@ -60,13 +69,15 @@ public ReadOnlySpan<char> this[Range index]
6069
public int Length => _chars.Length;
6170
int IReadOnlyCollection<char>.Count => Length;
6271

63-
public bool Contains(char c) => _chars.Contains(c);
72+
public bool Contains(char c) => _indexLookup.ContainsKey(c);
6473

6574
public bool Contains(char c, IEqualityComparer<char> comparer) => IndexOf(c, comparer) != -1;
6675

6776
public bool Contains(char c, StringComparison comparison) => _str.Contains(c, comparison);
6877

69-
public int IndexOf(char c) => _str.IndexOf(c);
78+
public bool ContainsIgnoreCase(char c) => _indexLookupUpper.ContainsKey(c.ToUpperInvariant());
79+
80+
public int IndexOf(char c) => _indexLookup.TryGetValue(c, out var index) ? index : -1;
7081

7182
public int IndexOf(char c, StringComparison comparison) => _str.IndexOf(c, comparison);
7283

@@ -86,14 +97,42 @@ public int IndexOf(char c, IEqualityComparer<char> comparer)
8697
}
8798
}
8899

89-
public int IndexOfIgnoreCase(char c) => _str.IndexOf(c, StringComparison.OrdinalIgnoreCase);
100+
public int IndexOfIgnoreCase(char c) => _indexLookupUpper.TryGetValue(c.ToUpperInvariant(), out var index) ? index : -1;
90101

91-
public char AtMod(int index) => _chars[index switch
102+
public char AtMod(int index)
92103
{
93-
< 0 => (Length - (-index % Length)) % Length,
94-
_ => index % Length,
95-
}];
104+
// negative
105+
if (index < 0)
106+
{
107+
// spare modulo operation
108+
if (index > MinusLength)
109+
{
110+
index = index - MinusLength;
111+
}
96112

113+
else
114+
{
115+
index = (Length - (-index % Length)) % Length;
116+
}
117+
}
118+
119+
// overflow
120+
else if (index >= Length)
121+
{
122+
// spare modulo operation
123+
if (index < DoubleLength)
124+
{
125+
index = index - Length;
126+
}
127+
128+
else
129+
{
130+
index = index % Length;
131+
}
132+
}
133+
134+
return _chars[index];
135+
}
97136

98137
public override bool Equals(object obj) => Equals(obj as Alphabet);
99138

@@ -115,7 +154,12 @@ public int IndexOf(char c, IEqualityComparer<char> comparer)
115154

116155
public override string ToString() => _str;
117156

118-
public char[] ToCharArray() => _chars.ToArray();
157+
public char[] ToCharArray()
158+
{
159+
var copy = new char[_chars.Length];
160+
_chars.CopyTo(copy, 0);
161+
return copy;
162+
}
119163

120164
public ReadOnlyMemory<char> ToMemory() => _str.AsMemory();
121165

src/Science.Cryptography.Ciphers/Ciphers/WolfenbüttelerCipher.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ protected override void Crypt(ReadOnlySpan<char> input, Span<char> output, out i
2323
written = input.Length;
2424
}
2525

26-
private static char Translate(char ch) => (ch.ToUpper() switch
26+
private static char Translate(char ch) => (ch.ToUpperInvariant() switch
2727
{
2828
'A' => 'M',
2929
'M' => 'A',

src/Science.Cryptography.Ciphers/Extensions/CharExtensions.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ public static partial class CharExtensions
1212
public static bool IsUpper(this char @char) => Char.IsUpper(@char);
1313

1414
[MethodImpl(MethodImplOptions.AggressiveInlining)]
15-
public static char ToUpper(this char @char) => Char.ToUpperInvariant(@char);
15+
public static char ToUpperInvariant(this char @char) => Char.ToUpperInvariant(@char);
1616

1717
[MethodImpl(MethodImplOptions.AggressiveInlining)]
18-
public static char ToLower(this char @char) => Char.ToLowerInvariant(@char);
18+
public static char ToLowerInvariant(this char @char) => Char.ToLowerInvariant(@char);
1919

2020
[MethodImpl(MethodImplOptions.AggressiveInlining)]
21-
public static char ToSameCaseAs(this char newChar, char reference) => Char.IsLower(reference) ? newChar.ToLower() : newChar;
21+
public static char ToSameCaseAs(this char newChar, char reference) => Char.IsLower(reference) ? newChar.ToLowerInvariant() : newChar;
2222

2323
[MethodImpl(MethodImplOptions.AggressiveInlining)]
2424
public static bool IsUpperAsciiLetter(this char newChar) => newChar >= 'A' && newChar <= 'Z';

src/Science.Cryptography.Ciphers/Tools/ArrayHelper.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ public static void FillSlow(char[,] buffer, ReadOnlySpan<char> source, int size)
99
{
1010
for (int i = 0; i < source.Length; i++)
1111
{
12-
buffer[i / size, i % size] = source[i].ToUpper();
12+
buffer[i / size, i % size] = source[i].ToUpperInvariant();
1313
}
1414
}
1515

1616
public static void FillTransposedSlow(char[,] buffer, ReadOnlySpan<char> source, int size)
1717
{
1818
for (int i = 0; i < source.Length; i++)
1919
{
20-
buffer[i % size, i / size] = source[i].ToUpper();
20+
buffer[i % size, i / size] = source[i].ToUpperInvariant();
2121
}
2222
}
2323

@@ -31,7 +31,7 @@ public static void FillWithKeywordAndAlphabet(Span<char> buffer, ReadOnlySpan<ch
3131

3232
for (int i = 0; i < keyword.Length; i++)
3333
{
34-
var upper = keyword[i].ToUpper();
34+
var upper = keyword[i].ToUpperInvariant();
3535
if (buffer.IndexOf(upper) == -1)
3636
{
3737
buffer[written++] = upper;
@@ -45,7 +45,7 @@ public static void FillWithKeywordAndAlphabet(Span<char> buffer, ReadOnlySpan<ch
4545

4646
for (int i = 0; i < alphabet.Length; i++)
4747
{
48-
var upper = alphabet[i].ToUpper();
48+
var upper = alphabet[i].ToUpperInvariant();
4949
if (buffer.IndexOf(upper) == -1)
5050
{
5151
buffer[written++] = upper;
@@ -55,7 +55,7 @@ public static void FillWithKeywordAndAlphabet(Span<char> buffer, ReadOnlySpan<ch
5555

5656
public static bool TryFindOffsets(char[,] buffer, char ch, out (int row, int column) positions, int rows, int columns)
5757
{
58-
char upper = ch.ToUpper();
58+
char upper = ch.ToUpperInvariant();
5959

6060
int index = buffer.AsSpan(rows, columns).IndexOf(upper);
6161
if (index > -1)
@@ -73,7 +73,7 @@ public static bool TryFindOffsets(char[,] buffer, char ch, out (int row, int col
7373

7474
public static bool TryFindOffsetsSlow(char[,] square, char ch, out (int row, int column) positions, int rows, int columns)
7575
{
76-
char upper = ch.ToUpper();
76+
char upper = ch.ToUpperInvariant();
7777

7878
for (int row = 0; row < rows; row++)
7979
for (int column = 0; column < columns; column++)

tests/Science.Cryptography.Ciphers.Tests/CharIgnoreCaseComparerTests.cs

+15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ namespace Science.Cryptography.Ciphers.Tests;
55
[TestClass]
66
public class AlphabetTests
77
{
8+
[TestMethod]
9+
public void AtMod_Regular()
10+
{
11+
Assert.AreEqual('A', WellKnownAlphabets.English.AtMod(0));
12+
Assert.AreEqual('B', WellKnownAlphabets.English.AtMod(1));
13+
}
14+
15+
[TestMethod]
16+
public void AtMod_Overflow()
17+
{
18+
Assert.AreEqual('A', WellKnownAlphabets.English.AtMod(26));
19+
Assert.AreEqual('B', WellKnownAlphabets.English.AtMod(27));
20+
Assert.AreEqual('A', WellKnownAlphabets.English.AtMod(52));
21+
}
22+
823
[TestMethod]
924
public void AtMod_Negative()
1025
{

0 commit comments

Comments
 (0)