From 913ec0ea80171b6819167956d94f74543dce7899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juh=C3=A1sz=20P=C3=A9ter?= Date: Fri, 27 Dec 2024 12:18:05 +0100 Subject: [PATCH] vectorized Atbash cipher for ASCII --- .../AtbashBenchmarks.cs | 8 +- .../Program.cs | 2 +- .../Optimized/AsciiAtbashCipher.cs | 94 ++++++++++++++++++- .../Ciphers/AsciiAtbashCipherTests.cs | 21 +++++ .../Ciphers/AtbashCipherTests.cs | 21 +++++ .../Ciphers/CipherTests.cs | 24 ----- 6 files changed, 136 insertions(+), 34 deletions(-) create mode 100644 tests/Science.Cryptography.Ciphers.Tests/Ciphers/AsciiAtbashCipherTests.cs create mode 100644 tests/Science.Cryptography.Ciphers.Tests/Ciphers/AtbashCipherTests.cs diff --git a/perf/Science.Cryptography.Ciphers.Benchmarks/AtbashBenchmarks.cs b/perf/Science.Cryptography.Ciphers.Benchmarks/AtbashBenchmarks.cs index 6ab4404..c0981f5 100644 --- a/perf/Science.Cryptography.Ciphers.Benchmarks/AtbashBenchmarks.cs +++ b/perf/Science.Cryptography.Ciphers.Benchmarks/AtbashBenchmarks.cs @@ -12,18 +12,18 @@ public class AtbashBenchmarks private static readonly AtbashCipher General = new(); private static readonly AsciiAtbashCipher Optimized = new(); - private static readonly char[] Input = new char[43]; - private static readonly char[] Output = new char[43]; + 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(Input, Output, out _); + General.Encrypt(Plaintext, Output, out _); } [Benchmark] public void SlowXor_I64_K32() { - Optimized.Encrypt(Input, Output, out _); + Optimized.Encrypt(Plaintext, Output, out _); } } \ No newline at end of file diff --git a/perf/Science.Cryptography.Ciphers.Benchmarks/Program.cs b/perf/Science.Cryptography.Ciphers.Benchmarks/Program.cs index 9672fe1..a0e71a9 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/src/Science.Cryptography.Ciphers.Specialized/Optimized/AsciiAtbashCipher.cs b/src/Science.Cryptography.Ciphers.Specialized/Optimized/AsciiAtbashCipher.cs index cca32c3..eaa7db6 100644 --- a/src/Science.Cryptography.Ciphers.Specialized/Optimized/AsciiAtbashCipher.cs +++ b/src/Science.Cryptography.Ciphers.Specialized/Optimized/AsciiAtbashCipher.cs @@ -1,5 +1,11 @@ using System; using System.Composition; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +using TVector = System.Runtime.Intrinsics.Vector128; namespace Science.Cryptography.Ciphers.Specialized; @@ -9,8 +15,15 @@ namespace Science.Cryptography.Ciphers.Specialized; [Export("ASCII-Atbash", typeof(ICipher))] public class AsciiAtbashCipher : ReciprocalCipher { - private const int A = (int)'A'; - private const int a = (int)'a'; + private const int APlusZ = (int)('A' + 'Z'); + private const int LowercaseAPlusZ = (int)('a' + 'z'); + + private static readonly TVector VectorOfAPlusZ = Vector128.Create((short)('A' + 'Z')); + private static readonly TVector VectorOfLowercaseAPlusZ = Vector128.Create((short)('a' + 'z')); + private static readonly TVector VectorOfAMinus1 = Vector128.Create((short)('A' - 1)); + private static readonly TVector VectorOfZPlus1 = Vector128.Create((short)('Z' + 1)); + private static readonly TVector VectorOfLowercaseAMinus1 = Vector128.Create((short)('a' - 1)); + private static readonly TVector VectorOfLowercaseZPlus1 = Vector128.Create((short)('z' + 1)); protected override void Crypt(ReadOnlySpan text, Span result, out int written) { @@ -19,18 +32,89 @@ protected override void Crypt(ReadOnlySpan text, Span result, out in throw new ArgumentException("Size of output buffer is insufficient.", nameof(result)); } + if (Avx2.IsSupported) + { + // process the vectorized input + var vectorCount = text.Length / TVector.Count; + var totalVectorizedLength = vectorCount * TVector.Count; + var vectorizedText = MemoryMarshal.Cast(text); + var vectorizedResult = MemoryMarshal.Cast(result); + for (int offset = 0; offset < totalVectorizedLength; offset += TVector.Count) + { + var input = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(vectorizedText[offset..])); + var output = CryptBlockAvx2(input); + output.StoreUnsafe(ref MemoryMarshal.GetReference(vectorizedResult[offset..])); + } + + // process the remaining input + if (totalVectorizedLength < text.Length) + { + var remainingInput = text[totalVectorizedLength..]; + var remainingOutput = result[totalVectorizedLength..]; + CryptSlow(remainingInput, remainingOutput); + } + } + else + { + CryptSlow(text, result); + } + + written = text.Length; + } + + internal static void CryptSlow(ReadOnlySpan text, Span result) + { for (int i = 0; i < text.Length; i++) { var ch = text[i]; var idx = (int)ch; result[i] = ch switch { - >= 'A' and <= 'Z' => (char)(A + (25 - (idx - A))), - >= 'a' and <= 'z' => (char)(a + (25 - (idx - a))), + >= 'A' and <= 'Z' => (char)(APlusZ - idx), + >= 'a' and <= 'z' => (char)(LowercaseAPlusZ - idx), _ => ch, }; } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TVector CryptBlockAvx2(TVector input) + { + // uppercase + var isUpperMask = Avx2.And( + Avx2.CompareGreaterThan(input, VectorOfAMinus1), + Avx2.CompareLessThan(input, VectorOfZPlus1) + ); + TVector transformedUppercase; + if (isUpperMask == TVector.Zero) + { + transformedUppercase = input; + } + else + { + transformedUppercase = Avx2.Subtract(VectorOfAPlusZ, input); + transformedUppercase = Avx2.BlendVariable(input, transformedUppercase, isUpperMask); + } + + // lowercase + var isLowerMask = Avx2.And( + Avx2.CompareGreaterThan(input, VectorOfLowercaseAMinus1), + Avx2.CompareLessThan(input, VectorOfLowercaseZPlus1) + ); + TVector transformedLowercase; + if (isLowerMask == TVector.Zero) + { + transformedLowercase = input; + } + else + { + transformedLowercase = Avx2.Subtract(VectorOfLowercaseAPlusZ, input); + transformedLowercase = Avx2.BlendVariable(transformedLowercase, transformedLowercase, isLowerMask); + } + + // merge + var transformed = Avx2.BlendVariable(transformedUppercase, transformedLowercase, isLowerMask); - written = text.Length; + return transformed; } } \ No newline at end of file diff --git a/tests/Science.Cryptography.Ciphers.Tests/Ciphers/AsciiAtbashCipherTests.cs b/tests/Science.Cryptography.Ciphers.Tests/Ciphers/AsciiAtbashCipherTests.cs new file mode 100644 index 0000000..472ffeb --- /dev/null +++ b/tests/Science.Cryptography.Ciphers.Tests/Ciphers/AsciiAtbashCipherTests.cs @@ -0,0 +1,21 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Science.Cryptography.Ciphers.Specialized; + +namespace Science.Cryptography.Ciphers.Tests; + +[TestClass] +public class AsciiAtbashCipherTests +{ + [TestMethod] + public void AsciiAtbash() + { + var cipher = new AsciiAtbashCipher(); + + const string plaintext = "AbcdefghijklmnopqrstuvwxyzZYX"; + const string ciphertext = "ZyxwvutsrqponmlkjihgfedcbaABC"; + + Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext)); + Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext)); + } +} diff --git a/tests/Science.Cryptography.Ciphers.Tests/Ciphers/AtbashCipherTests.cs b/tests/Science.Cryptography.Ciphers.Tests/Ciphers/AtbashCipherTests.cs new file mode 100644 index 0000000..d63a575 --- /dev/null +++ b/tests/Science.Cryptography.Ciphers.Tests/Ciphers/AtbashCipherTests.cs @@ -0,0 +1,21 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Science.Cryptography.Ciphers.Specialized; + +namespace Science.Cryptography.Ciphers.Tests; + +[TestClass] +public class AtbashCipherTests +{ + [TestMethod] + public void Atbash() + { + var cipher = new AtbashCipher(); + + const string plaintext = "Abcdefghijklmnopqrstuvwxyz"; + const string ciphertext = "Zyxwvutsrqponmlkjihgfedcba"; + + 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 18a41ef..fc89c19 100644 --- a/tests/Science.Cryptography.Ciphers.Tests/Ciphers/CipherTests.cs +++ b/tests/Science.Cryptography.Ciphers.Tests/Ciphers/CipherTests.cs @@ -56,30 +56,6 @@ public void Multiplicative_Encrypt() Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext, 3)); } - [TestMethod] - public void Atbash() - { - var cipher = new AtbashCipher(); - - const string plaintext = "Abcdefghijklmnopqrstuvwxyz"; - const string ciphertext = "Zyxwvutsrqponmlkjihgfedcba"; - - Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext)); - Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext)); - } - - [TestMethod] - public void AsciiAtbash() - { - var cipher = new AsciiAtbashCipher(); - - const string plaintext = "Abcdefghijklmnopqrstuvwxyz"; - const string ciphertext = "Zyxwvutsrqponmlkjihgfedcba"; - - Assert.AreEqual(ciphertext, cipher.Encrypt(plaintext)); - Assert.AreEqual(plaintext, cipher.Decrypt(ciphertext)); - } - /* [TestMethod] public void Bifid()