Skip to content

Commit a8d81ac

Browse files
committed
vectorized ROT-47 cipher
1 parent 26da0d2 commit a8d81ac

File tree

6 files changed

+186
-13
lines changed

6 files changed

+186
-13
lines changed

docs/performance-improvements.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ Payload length: 44 characters
5151

5252
Measured speed up: **14x**
5353

54+
## ROT-47 cipher
55+
In version 2, a fast path was added for shifting ASCII characters:
56+
57+
| | Method | Mean | Error | StdDev | Allocated |
58+
|------|-------------------------- |----------:|---------:|---------:|----------:|
59+
|v1 | Rot47Cipher_General | 23.393 ns | 0.0773 ns | 0.0723 ns | - |
60+
|**v2**| Rot47Cipher_Ascii_Avx2128 | 7.873 ns | 0.0778 ns | 0.0728 ns | - |
61+
62+
Payload length: 44 characters
63+
64+
Measured speed up: **3x**
65+
5466
## Frequency analysis
5567
The old v1 implementation was based on a very simple, but expensive functional LINQ implementation. In the new version, memory allocation was greatly reduced:
5668

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
using BenchmarkDotNet.Running;
22

3-
BenchmarkRunner.Run<AtbashBenchmarks>();
3+
BenchmarkRunner.Run<Rot47Benchmarks>();
44
//BenchmarkRunner.Run(typeof(Program).Assembly);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using BenchmarkDotNet.Attributes;
2+
3+
using Science.Cryptography.Ciphers.Specialized;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System;
7+
using Science.Cryptography.Ciphers;
8+
9+
[MemoryDiagnoser]
10+
public class Rot47Benchmarks
11+
{
12+
private static readonly Rot47Cipher General = new();
13+
private static readonly AsciiRot47Cipher Optimized = new();
14+
15+
private const string Plaintext = "The quick brown fox jumps over the lazy dog.";
16+
private static readonly char[] Output = new char[64];
17+
18+
[Benchmark]
19+
public void Atbash()
20+
{
21+
General.Encrypt(Plaintext, Output, out _);
22+
}
23+
24+
[Benchmark]
25+
public void SlowXor_I64_K32()
26+
{
27+
Optimized.Encrypt(Plaintext, Output, out _);
28+
}
29+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using System;
2+
using System.Composition;
3+
using System.Numerics;
4+
using System.Runtime.CompilerServices;
5+
using System.Runtime.InteropServices;
6+
using System.Runtime.Intrinsics;
7+
using System.Runtime.Intrinsics.X86;
8+
9+
using TVector = System.Runtime.Intrinsics.Vector256<short>;
10+
11+
namespace Science.Cryptography.Ciphers.Specialized;
12+
13+
/// <summary>
14+
/// Represents the Atbash cipher.
15+
/// </summary>
16+
[Export("ASCII-ROT-47", typeof(ICipher))]
17+
public class AsciiRot47Cipher : ReciprocalCipher
18+
{
19+
private static readonly TVector VectorOfSpace = Vector256.Create((short)' ');
20+
private static readonly TVector VectorOf47 = Vector256.Create((short)47);
21+
private static readonly TVector VectorOf126 = Vector256.Create((short)126);
22+
private static readonly TVector VectorOf33 = Vector256.Create((short)33);
23+
private static readonly TVector VectorOf94 = Vector256.Create((short)94);
24+
25+
protected override void Crypt(ReadOnlySpan<char> text, Span<char> result, out int written)
26+
{
27+
if (result.Length < text.Length)
28+
{
29+
throw new ArgumentException("Size of output buffer is insufficient.", nameof(result));
30+
}
31+
32+
var totalVectorizedLength = 0;
33+
34+
// process vectorized
35+
if (Avx2.IsSupported && Vector256.IsHardwareAccelerated)
36+
{
37+
var vectorCount = text.Length / TVector.Count;
38+
totalVectorizedLength = vectorCount * TVector.Count;
39+
var inputAsShort = MemoryMarshal.Cast<char, short>(text);
40+
var outputAsShort = MemoryMarshal.Cast<char, short>(result);
41+
for (int offset = 0; offset < totalVectorizedLength; offset += TVector.Count)
42+
{
43+
var inputBlock = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(inputAsShort[offset..]));
44+
var outputBlock = CryptBlockAvx2(inputBlock);
45+
outputBlock.StoreUnsafe(ref MemoryMarshal.GetReference(outputAsShort[offset..]));
46+
}
47+
}
48+
49+
// process the remaining input
50+
if (totalVectorizedLength < text.Length)
51+
{
52+
var remainingInput = text[totalVectorizedLength..];
53+
var remainingOutput = result[totalVectorizedLength..];
54+
CryptSlow(remainingInput, remainingOutput);
55+
}
56+
57+
written = text.Length;
58+
}
59+
60+
internal static void CryptSlow(ReadOnlySpan<char> text, Span<char> result)
61+
{
62+
for (int i = 0; i < text.Length; i++)
63+
{
64+
var ch = text[i];
65+
if (ch == ' ')
66+
{
67+
result[i] = ' ';
68+
continue;
69+
}
70+
71+
int value = ch + 47;
72+
73+
if (value > 126)
74+
{
75+
value -= 94;
76+
}
77+
else if (value < 33)
78+
{
79+
value += 94;
80+
}
81+
82+
result[i] = (char)value;
83+
}
84+
}
85+
86+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
87+
private static TVector CryptBlockAvx2(TVector input)
88+
{
89+
// whitespace mask
90+
var spaceMask = Avx2.CompareEqual(VectorOfSpace, input);
91+
92+
// add 47
93+
var transformed = Avx2.Add(input, VectorOf47);
94+
95+
// subtract 94 if greater than 126
96+
var greaterThan126Mask = Avx2.CompareGreaterThan(transformed, VectorOf126);
97+
var subtracted = Avx2.Subtract(transformed, VectorOf94);
98+
transformed = Avx2.BlendVariable(transformed, subtracted, greaterThan126Mask);
99+
100+
// add 94 if less than 33
101+
var lessThan33Mask = Avx2.CompareGreaterThan(VectorOf33, transformed);
102+
var added = Avx2.Add(transformed, VectorOf94);
103+
transformed = Avx2.BlendVariable(transformed, added, lessThan33Mask);
104+
105+
// restore whitespace
106+
transformed = Avx2.BlendVariable(transformed, VectorOfSpace, spaceMask);
107+
108+
return transformed;
109+
}
110+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2+
3+
using Science.Cryptography.Ciphers.Specialized;
4+
using System;
5+
6+
namespace Science.Cryptography.Ciphers.Tests;
7+
8+
[TestClass]
9+
public class AsciiRot47CipherTests
10+
{
11+
[TestMethod]
12+
public void Rot47()
13+
{
14+
var cipher = new Rot47Cipher();
15+
16+
const string plaintext = "The quick brown fox jumps over the lazy dog";
17+
const string ciphertext = "%96 BF:4< 3C@H? 7@I ;F>AD @G6C E96 =2KJ 5@8";
18+
19+
Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext));
20+
Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext));
21+
}
22+
23+
[TestMethod]
24+
public void AsciiRot47()
25+
{
26+
var cipher = new AsciiRot47Cipher();
27+
28+
const string plaintext = "The quick brown fox jumps over the lazy dog";
29+
const string ciphertext = "%96 BF:4< 3C@H? 7@I ;F>AD @G6C E96 =2KJ 5@8";
30+
31+
Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext));
32+
Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext));
33+
}
34+
}

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,6 @@ public void Bacon()
103103
Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext), true);
104104
}
105105

106-
[TestMethod]
107-
public void Rot47()
108-
{
109-
var cipher = new Rot47Cipher();
110-
111-
const string plaintext = "My string!";
112-
const string ciphertext = "|J DEC:?8P";
113-
114-
Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext));
115-
Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext));
116-
}
117-
118106
[TestMethod]
119107
public void Autokey()
120108
{

0 commit comments

Comments
 (0)