Skip to content

Commit

Permalink
caesar bruteforce benchmarks
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter-Juhasz committed Jun 25, 2022
1 parent 39aa621 commit 3d6b00d
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 86 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
push:
branches:
- main
paths-ignore:
- docs/**
pull_request:
branches:
- main
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
push:
branches:
- main
paths-ignore:
- docs/**
pull_request:
branches:
- main
Expand Down Expand Up @@ -70,3 +72,4 @@ jobs:

- name: NuGet Push
run: nuget push src/Science.Cryptography.*/bin/Release/Science.Cryptography.*.nupkg -Source https://api.nuget.org/v3/index.json
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,10 @@ The full list of assets:
- [List of languages](docs/assets.md#list-of-languages)
- [List of encodings](docs/assets.md#list-of-encodings)

## Accepting PRs
* Enigma
* Hill
* Permutation
* Rail fence
* Myszkowski Transposition
* Nihilist
* Solitaire
* Trifid
* Any other missing cipher
* Unit tests
## Contribution
- Add any [missing cipher](https://github.com/Peter-Juhasz/Science.Cryptography.Ciphers/issues?q=is%3Aissue+is%3Aopen+label%3Acipher)
- Prefer performance (no heap allocations, SIMD operations, ...)
- *Feel free to add any not listed*
- Performance improvements
- Post benchmark and its results as evidence to show change in efficiency
- Unit tests
38 changes: 30 additions & 8 deletions docs/performance-improvements.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,39 @@ Payload and key length: 64/64, 64/32, 43/32, 64/32 characters

(*bestcase* represents the scenario where the payload is an exact multiple of key size)

Measured speed up: **27x** regular case, **81x** best case
Measured speed up: **27x** regular case, **81x** best case, memory allocation reduction from **400 bytes to zero**.

## Atbash cipher
In version 2, a fast path was added for ASCII encoding:

| | Method | Mean | Error | StdDev | Allocated |
|---|---------------- |------------:|----------:|----------:|----------:|
|**v2**| Atbash | 3,974.15 ns | 10.977 ns | 10.268 ns | - |
|**v2**| Atbash_Ascii | 47.36 ns | 0.256 ns | 0.239 ns | - |
|**v2**| General | 3,974.15 ns | 10.977 ns | 10.268 ns | - |
|**v2**| Ascii | 47.36 ns | 0.256 ns | 0.239 ns | - |

Payload length: 43 characters

Measured speed up: **84x**

## Frequency analysis
The old v1 implementation was based on a very simple, but expensive functional LINQ implementation. In the new version, memory allocation was greatly reduced:

| | Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
|------|--------- |-----------:|--------:|--------:|-------:|-------:|----------:|
|v1 | General | 2,331.2 ns | 9.72 ns | 9.10 ns | 0.7935 | 0.0076 | 4,992 B |
|**v2**| General | 512.9 ns | 2.11 ns | 1.76 ns | 0.2499 | - | 1,568 B |
|**v2**| Ascii_Optimized | 235.0 ns | 0.72 ns | 0.63 ns | - | - | - |

Measured speed up: **10x**, memory allocation reduction from **5 KB to zero** (ascii).

## Relative letter frequencies scorer
The old v1 implementation was based on a very simple functional LINQ implementation. In the new version, memory allocation was greatly reduced:

| | Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
|------|--------- |---------:|----------:|----------:|-------:|-------:|----------:|
|v1 | General_Linq | 3.968 us | 0.0223 us | 0.0209 us | 1.0147 | 0.0076 | 6,368 B |
|**v2**| General_Optimized | 1.464 us | 0.0042 us | 0.0035 us | 0.0210 | - | 136 B |
|**v2**| General_Ascii | 1.495 us | 0.0056 us | 0.0052 us | 0.0210 | - | 136 B |
|**v2**| General | 1.464 us | 0.0042 us | 0.0035 us | 0.0210 | - | 136 B |
|**v2**| Ascii_Optimized | 1.495 us | 0.0056 us | 0.0052 us | 0.0210 | - | 136 B |

Measured speed up: **2.7x**, memory allocation reduction: **47x**

Expand All @@ -57,13 +68,24 @@ In the new version, buffers can be shared, reading uses non-allocating enumerati

| | Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
|------|----------- |-----------:|---------:|---------:|-------:|-------:|----------:|
|v1 | Old | 5,530.7 ns | 27.76 ns | 25.97 ns | 1.7319 | 0.0458 | 10,888 B |
|**v2**| New | 1,124.4 ns | 2.14 ns | 2.01 ns | 0.3719 | 0.0019 | 2,336 B |
|**v2**| New_Ascii_2Grams | 306.5 ns | 1.45 ns | 1.36 ns | - | - | - |
|v1 | General | 5,530.7 ns | 27.76 ns | 25.97 ns | 1.7319 | 0.0458 | 10,888 B |
|**v2**| General | 1,124.4 ns | 2.14 ns | 2.01 ns | 0.3719 | 0.0019 | 2,336 B |
|**v2**| Ascii_2Grams | 306.5 ns | 1.45 ns | 1.36 ns | - | - | - |

Measured speed up: **18x**, memory allocation reduction from **18 KB to zero** (ascii).

## Caesar brute-force

| | Method | Mean | Error | StdDev | Gen 0 | Allocated |
|------|------------- |---------:|---------:|---------:|-------:|----------:|
|v1 | General | 41.41 us | 0.244 us | 0.217 us | 1.2817 | 8 KB |
|**v2**| General | 81.51 us | 0.180 us | 0.150 us | 0.9766 | 6 KB |
|**v2**| General_Optimized | 80.76 us | 0.262 us | 0.245 us | 0.8545 | 5 KB |

Measured speed up: *-2x*, memory allocation reduced to **62.5%**

## Appendix
Performance was measured on the following setup:

```
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Expand Down
2 changes: 1 addition & 1 deletion src/Science.Cryptography.Ciphers.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<V1V2NGramAnalysisBenchmarks>();
BenchmarkRunner.Run<V1V2CaesarBruteforceBenchmarks>();
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using BenchmarkDotNet.Attributes;

using System.Collections.Generic;
using System.Linq;
using System;
using System.Collections;
using Science.Cryptography.Ciphers.Analysis;
using Science.Cryptography.Ciphers;
using System.Composition;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

[MemoryDiagnoser]
public class V1V2CaesarBruteforceBenchmarks
{
private static readonly string[] _buffer = new string[26];
private static readonly Science.Cryptography.Ciphers.ShiftCipher _cipher = new(WellKnownAlphabets.English);
private static readonly string Text = "the quick brown fox jumps over the lazy dog";


[Benchmark]
public void V1()
{
Analyze(Text);
}

[Benchmark]
public void V2()
{
CaesarBruteforce.Analyze(Text, WellKnownAlphabets.English);
}

[Benchmark]
public void V2_Optimized()
{
CaesarBruteforce.Analyze(Text, _cipher, _buffer);
}

#region V1
private static IReadOnlyDictionary<int, string> Analyze(string text, string charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
{
if (text == null)
throw new ArgumentNullException(nameof(text));

if (charset == null)
throw new ArgumentNullException(nameof(charset));


var cipher = new ShiftCipher(charset);

return Enumerable.Range(0, charset.Length)
.ToDictionary(k => k, k => cipher.Encrypt(text, k))
;
}

private class ShiftCipher
{
public ShiftCipher(string charset)
{
if (charset == null)
throw new ArgumentNullException(nameof(charset));

this.Charset = charset;
}

public string Charset { get; set; }

protected string Crypt(string text, int key)
{
char[] result = new char[text.Length];

for (int i = 0; i < text.Length; i++)
{
int idx = IndexOfIgnoreCase(Charset, text[i]);

result[i] = idx != -1
? At(this.Charset, idx + key).ToSameCaseAs(text[i])
: text[i]
;
}

return new String(result);
}

public string Encrypt(string plaintext, int key)
{
return this.Crypt(plaintext, key);
}

public string Decrypt(string ciphertext, int key)
{
return this.Crypt(ciphertext, -key);
}


public static int IndexOfIgnoreCase(string source, char subject)
{
Char toCompare = subject.ToUpper();

for (int i = 0; i < source.Length; i++)
{
if (source[i].ToUpper() == toCompare)
return i;
}

return -1;
}

public static char At(string source, int index)
{
return source[Mod(index, source.Length)];
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Mod(int a, int b)
{
return a >= 0 ? a % b : (b + a) % b;
}
}
#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,30 @@
[MemoryDiagnoser]
public class V1V2FrequencyAnalysisBenchmarks
{
private static readonly ISpeculativePlaintextScorer General = new RelativeLetterFrequenciesSpeculativePlaintextScorer(Languages.English.RelativeFrequenciesOfLetters).GetForPartition();
private static readonly ISpeculativePlaintextScorer Ascii = new AsciiRelativeLetterFrequenciesSpeculativePlaintextScorer(Languages.English.RelativeFrequenciesOfLetters).GetForPartition();
private static readonly Dictionary<char, int> _buffer = new Dictionary<char, int>();

private static readonly string Text = "the quick brown fox jumps over the lazy dog";


[Benchmark]
public void V1()
{
Classify(Text);
Analyze(Text);
}

[Benchmark]
public void V2()
{
General.Score(Text);
FrequencyAnalysis.Analyze(Text);
}

[Benchmark]
public void V2_Ascii()
{
Ascii.Score(Text);
}

public double Classify(string speculativePlaintext)
{
return Compare(Languages.English.RelativeFrequenciesOfLetters, Analyze(speculativePlaintext).AsRelativeFrequencies());
FrequencyAnalysis.AnalyzeAsciiLetters(Text, _buffer);
}

#region V1
private static AbsoluteCharacterFrequencies Analyze(string text)
{
if (text == null)
Expand All @@ -50,14 +45,6 @@ private static AbsoluteCharacterFrequencies Analyze(string text)
);
}

private static double Compare(IReadOnlyDictionary<char, double> reference, IReadOnlyDictionary<char, double> subject)
{
return 1 - (
from r in reference
select Math.Abs(r.Value - (subject.ContainsKey(r.Key) ? subject[r.Key] : 0))
).Sum();
}

private class AbsoluteCharacterFrequencies : IReadOnlyDictionary<char, int>
{
public AbsoluteCharacterFrequencies(IReadOnlyDictionary<char, int> frequencies)
Expand Down Expand Up @@ -117,52 +104,5 @@ public IReadOnlyDictionary<char, double> AsRelativeFrequencies()
IEnumerator IEnumerable.GetEnumerator() => _frequencies.GetEnumerator();
#endregion
}
private class RelativeCharacterFrequencies : IReadOnlyDictionary<char, double>
{
public RelativeCharacterFrequencies(IReadOnlyDictionary<char, double> frequencies)
{
if (frequencies == null)
throw new ArgumentNullException(nameof(frequencies));

_frequencies = frequencies;
}

private readonly IReadOnlyDictionary<char, double> _frequencies;

/// <summary>
/// Gets the occurrences of a given <paramref name="character"/>.
/// </summary>
/// <param name="character"></param>
/// <returns></returns>
public double this[char character]
{
get
{
double frequency = 0;
_frequencies.TryGetValue(character, out frequency);
return frequency;
}
}


public IReadOnlyDictionary<char, double> ToDictionary() => _frequencies;


#region IReadOnlyDictionary<char, double>
IEnumerable<char> IReadOnlyDictionary<char, double>.Keys => _frequencies.Keys;
IEnumerable<double> IReadOnlyDictionary<char, double>.Values => _frequencies.Values;

int IReadOnlyCollection<KeyValuePair<char, double>>.Count => _frequencies.Count;

double IReadOnlyDictionary<char, double>.this[char key] => this[key];

bool IReadOnlyDictionary<char, double>.ContainsKey(char key) => _frequencies.ContainsKey(key);

bool IReadOnlyDictionary<char, double>.TryGetValue(char key, out double value) => _frequencies.TryGetValue(key, out value);

IEnumerator<KeyValuePair<char, double>> IEnumerable<KeyValuePair<char, double>>.GetEnumerator() => _frequencies.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => _frequencies.GetEnumerator();
#endregion
}
#endregion
}
Loading

0 comments on commit 3d6b00d

Please sign in to comment.