diff --git a/docs/performance-improvements.md b/docs/performance-improvements.md
index 15e640f..f5eb1bb 100644
--- a/docs/performance-improvements.md
+++ b/docs/performance-improvements.md
@@ -51,6 +51,18 @@ Payload length: 44 characters
Measured speed up: **14x**
+## ROT-47 cipher
+In version 2, a fast path was added for shifting ASCII characters:
+
+| | Method | Mean | Error | StdDev | Allocated |
+|------|-------------------------- |----------:|---------:|---------:|----------:|
+|v1 | Rot47Cipher_General | 23.393 ns | 0.0773 ns | 0.0723 ns | - |
+|**v2**| Rot47Cipher_Ascii_Avx2128 | 7.873 ns | 0.0778 ns | 0.0728 ns | - |
+
+Payload length: 44 characters
+
+Measured speed up: **3x**
+
## 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:
diff --git a/perf/Science.Cryptography.Ciphers.Benchmarks/Program.cs b/perf/Science.Cryptography.Ciphers.Benchmarks/Program.cs
index a0e71a9..e3d6761 100644
--- a/perf/Science.Cryptography.Ciphers.Benchmarks/Program.cs
+++ b/perf/Science.Cryptography.Ciphers.Benchmarks/Program.cs
@@ -1,4 +1,4 @@
using BenchmarkDotNet.Running;
-BenchmarkRunner.Run();
+BenchmarkRunner.Run();
//BenchmarkRunner.Run(typeof(Program).Assembly);
diff --git a/perf/Science.Cryptography.Ciphers.Benchmarks/Rot47Benchmarks.cs b/perf/Science.Cryptography.Ciphers.Benchmarks/Rot47Benchmarks.cs
new file mode 100644
index 0000000..d505475
--- /dev/null
+++ b/perf/Science.Cryptography.Ciphers.Benchmarks/Rot47Benchmarks.cs
@@ -0,0 +1,29 @@
+using BenchmarkDotNet.Attributes;
+
+using Science.Cryptography.Ciphers.Specialized;
+using System.Collections.Generic;
+using System.Linq;
+using System;
+using Science.Cryptography.Ciphers;
+
+[MemoryDiagnoser]
+public class Rot47Benchmarks
+{
+ private static readonly Rot47Cipher General = new();
+ private static readonly AsciiRot47Cipher Optimized = new();
+
+ private const string Plaintext = "The quick brown fox jumps over the lazy dog.";
+ private static readonly char[] Output = new char[64];
+
+ [Benchmark]
+ public void Atbash()
+ {
+ General.Encrypt(Plaintext, Output, out _);
+ }
+
+ [Benchmark]
+ public void SlowXor_I64_K32()
+ {
+ Optimized.Encrypt(Plaintext, Output, out _);
+ }
+}
\ No newline at end of file
diff --git a/src/Science.Cryptography.Ciphers.Specialized/Optimized/AsciiRot47Cipher.cs b/src/Science.Cryptography.Ciphers.Specialized/Optimized/AsciiRot47Cipher.cs
new file mode 100644
index 0000000..6f4d98d
--- /dev/null
+++ b/src/Science.Cryptography.Ciphers.Specialized/Optimized/AsciiRot47Cipher.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Composition;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
+
+using TVector = System.Runtime.Intrinsics.Vector256;
+
+namespace Science.Cryptography.Ciphers.Specialized;
+
+///
+/// Represents the Atbash cipher.
+///
+[Export("ASCII-ROT-47", typeof(ICipher))]
+public class AsciiRot47Cipher : ReciprocalCipher
+{
+ private static readonly TVector VectorOfSpace = Vector256.Create((short)' ');
+ private static readonly TVector VectorOf47 = Vector256.Create((short)47);
+ private static readonly TVector VectorOf126 = Vector256.Create((short)126);
+ private static readonly TVector VectorOf33 = Vector256.Create((short)33);
+ private static readonly TVector VectorOf94 = Vector256.Create((short)94);
+
+ protected override void Crypt(ReadOnlySpan text, Span result, out int written)
+ {
+ if (result.Length < text.Length)
+ {
+ throw new ArgumentException("Size of output buffer is insufficient.", nameof(result));
+ }
+
+ var totalVectorizedLength = 0;
+
+ // process vectorized
+ if (Avx2.IsSupported && Vector256.IsHardwareAccelerated)
+ {
+ var vectorCount = text.Length / TVector.Count;
+ totalVectorizedLength = vectorCount * TVector.Count;
+ var inputAsShort = MemoryMarshal.Cast(text);
+ var outputAsShort = MemoryMarshal.Cast(result);
+ for (int offset = 0; offset < totalVectorizedLength; offset += TVector.Count)
+ {
+ var inputBlock = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(inputAsShort[offset..]));
+ var outputBlock = CryptBlockAvx2(inputBlock);
+ outputBlock.StoreUnsafe(ref MemoryMarshal.GetReference(outputAsShort[offset..]));
+ }
+ }
+
+ // process the remaining input
+ if (totalVectorizedLength < text.Length)
+ {
+ var remainingInput = text[totalVectorizedLength..];
+ var remainingOutput = result[totalVectorizedLength..];
+ CryptSlow(remainingInput, remainingOutput);
+ }
+
+ written = text.Length;
+ }
+
+ internal static void CryptSlow(ReadOnlySpan text, Span result)
+ {
+ for (int i = 0; i < text.Length; i++)
+ {
+ var ch = text[i];
+ if (ch == ' ')
+ {
+ result[i] = ' ';
+ continue;
+ }
+
+ int value = ch + 47;
+
+ if (value > 126)
+ {
+ value -= 94;
+ }
+ else if (value < 33)
+ {
+ value += 94;
+ }
+
+ result[i] = (char)value;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static TVector CryptBlockAvx2(TVector input)
+ {
+ // whitespace mask
+ var spaceMask = Avx2.CompareEqual(VectorOfSpace, input);
+
+ // add 47
+ var transformed = Avx2.Add(input, VectorOf47);
+
+ // subtract 94 if greater than 126
+ var greaterThan126Mask = Avx2.CompareGreaterThan(transformed, VectorOf126);
+ var subtracted = Avx2.Subtract(transformed, VectorOf94);
+ transformed = Avx2.BlendVariable(transformed, subtracted, greaterThan126Mask);
+
+ // add 94 if less than 33
+ var lessThan33Mask = Avx2.CompareGreaterThan(VectorOf33, transformed);
+ var added = Avx2.Add(transformed, VectorOf94);
+ transformed = Avx2.BlendVariable(transformed, added, lessThan33Mask);
+
+ // restore whitespace
+ transformed = Avx2.BlendVariable(transformed, VectorOfSpace, spaceMask);
+
+ return transformed;
+ }
+}
\ No newline at end of file
diff --git a/tests/Science.Cryptography.Ciphers.Tests/Ciphers/AsciiRot47CipherTests.cs b/tests/Science.Cryptography.Ciphers.Tests/Ciphers/AsciiRot47CipherTests.cs
new file mode 100644
index 0000000..23c8dcd
--- /dev/null
+++ b/tests/Science.Cryptography.Ciphers.Tests/Ciphers/AsciiRot47CipherTests.cs
@@ -0,0 +1,34 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Science.Cryptography.Ciphers.Specialized;
+using System;
+
+namespace Science.Cryptography.Ciphers.Tests;
+
+[TestClass]
+public class AsciiRot47CipherTests
+{
+ [TestMethod]
+ public void Rot47()
+ {
+ var cipher = new Rot47Cipher();
+
+ const string plaintext = "The quick brown fox jumps over the lazy dog";
+ const string ciphertext = "%96 BF:4< 3C@H? 7@I ;F>AD @G6C E96 =2KJ 5@8";
+
+ Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext));
+ Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext));
+ }
+
+ [TestMethod]
+ public void AsciiRot47()
+ {
+ var cipher = new AsciiRot47Cipher();
+
+ const string plaintext = "The quick brown fox jumps over the lazy dog";
+ const string ciphertext = "%96 BF:4< 3C@H? 7@I ;F>AD @G6C E96 =2KJ 5@8";
+
+ Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext));
+ Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext));
+ }
+}
diff --git a/tests/Science.Cryptography.Ciphers.Tests/Ciphers/CipherTests.cs b/tests/Science.Cryptography.Ciphers.Tests/Ciphers/CipherTests.cs
index fc89c19..e91a7c7 100644
--- a/tests/Science.Cryptography.Ciphers.Tests/Ciphers/CipherTests.cs
+++ b/tests/Science.Cryptography.Ciphers.Tests/Ciphers/CipherTests.cs
@@ -103,18 +103,6 @@ public void Bacon()
Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext), true);
}
- [TestMethod]
- public void Rot47()
- {
- var cipher = new Rot47Cipher();
-
- const string plaintext = "My string!";
- const string ciphertext = "|J DEC:?8P";
-
- Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext));
- Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext));
- }
-
[TestMethod]
public void Autokey()
{