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() {