From 804d4b97af01ad2aa4540db70e1c3c942a62b96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Mon, 16 Jun 2025 20:59:49 +0200 Subject: [PATCH 1/9] Avoid allocation of gluedPolynoms when possible --- QRCoder/QRCodeGenerator.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 8dcf7928..511ff6c6 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -1018,7 +1018,13 @@ private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polyno // Identify and merge terms with the same exponent. var toGlue = GetNotUniqueExponents(resultPolynom); +#if NETCOREAPP + var gluedPolynoms = toGlue.Length <= 128 + ? stackalloc PolynomItem[128].Slice(0, toGlue.Length) + : new PolynomItem[toGlue.Length]; +#else var gluedPolynoms = new PolynomItem[toGlue.Length]; +#endif var gluedPolynomsIndex = 0; foreach (var exponent in toGlue) { From 29d9e0ec4e26db4cd33e670755298243636e2ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Mon, 16 Jun 2025 21:00:09 +0200 Subject: [PATCH 2/9] Avoid using Linq in MultiplyAlphaPolynoms --- QRCoder/QRCodeGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 511ff6c6..801278ab 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -1042,7 +1042,7 @@ private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polyno // Remove duplicated exponents and add the corrected ones back. for (int i = resultPolynom.Count - 1; i >= 0; i--) - if (toGlue.Contains(resultPolynom[i].Exponent)) + if (Array.IndexOf(toGlue, resultPolynom[i].Exponent) >= 0) resultPolynom.RemoveAt(i); foreach (var polynom in gluedPolynoms) resultPolynom.Add(polynom); From bde87ee6efbf1b399a68ce88d85e844448100bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Mon, 16 Jun 2025 22:19:15 +0200 Subject: [PATCH 3/9] Avoid Dictionary allocations in GetNotUniqueExponents --- QRCoder/QRCodeGenerator.cs | 56 +++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 801278ab..aa1bea6c 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -4,6 +4,7 @@ #endif using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; @@ -1017,12 +1018,13 @@ private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polyno } // Identify and merge terms with the same exponent. - var toGlue = GetNotUniqueExponents(resultPolynom); #if NETCOREAPP + var toGlue = GetNotUniqueExponents(resultPolynom, resultPolynom.Count <= 128 ? stackalloc int[128].Slice(0, resultPolynom.Count) : new int[resultPolynom.Count]); var gluedPolynoms = toGlue.Length <= 128 ? stackalloc PolynomItem[128].Slice(0, toGlue.Length) : new PolynomItem[toGlue.Length]; #else + var toGlue = GetNotUniqueExponents(resultPolynom); var gluedPolynoms = new PolynomItem[toGlue.Length]; #endif var gluedPolynomsIndex = 0; @@ -1042,7 +1044,11 @@ private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polyno // Remove duplicated exponents and add the corrected ones back. for (int i = resultPolynom.Count - 1; i >= 0; i--) +#if NETCOREAPP + if (toGlue.Contains(resultPolynom[i].Exponent)) +#else if (Array.IndexOf(toGlue, resultPolynom[i].Exponent) >= 0) +#endif resultPolynom.RemoveAt(i); foreach (var polynom in gluedPolynoms) resultPolynom.Add(polynom); @@ -1052,20 +1058,55 @@ private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polyno return resultPolynom; // Auxiliary function to identify exponents that appear more than once in the polynomial. - int[] GetNotUniqueExponents(Polynom list) +#if NETCOREAPP + static ReadOnlySpan GetNotUniqueExponents(Polynom list, Span buffer) { - var dic = new Dictionary(list.Count); + Debug.Assert(list.Count == buffer.Length); + + int idx = 0; foreach (var row in list) { -#if NETCOREAPP - if (dic.TryAdd(row.Exponent, false)) - dic[row.Exponent] = true; + buffer[idx++] = row.Exponent; + } + + buffer.Sort(); + + idx = 0; + int expCount = 0; + int last = buffer[0]; + + for (int i = 1; i < buffer.Length; ++i) + { + if (buffer[i] == last) + { + expCount++; + } + else + { + if (expCount > 0) + { + Debug.Assert(idx <= i - 1); + + buffer[idx++] = last; + expCount = 0; + } + } + + last = buffer[i]; + } + + return buffer.Slice(0, idx); + } #else + static int[] GetNotUniqueExponents(Polynom list) + { + var dic = new Dictionary(list.Count); + foreach (var row in list) + { if (!dic.ContainsKey(row.Exponent)) dic.Add(row.Exponent, false); else dic[row.Exponent] = true; -#endif } // Collect all exponents that appeared more than once. @@ -1086,6 +1127,7 @@ int[] GetNotUniqueExponents(Polynom list) return result; } +#endif } /// From 59b269077edf6b123c21d32537f0e78d7ce1260b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Tue, 17 Jun 2025 16:32:39 +0200 Subject: [PATCH 4/9] Added comment to GetNotUniqueExponents on how it works --- QRCoder/QRCodeGenerator.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index aa1bea6c..4ae59d63 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -1061,6 +1061,17 @@ private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polyno #if NETCOREAPP static ReadOnlySpan GetNotUniqueExponents(Polynom list, Span buffer) { + // It works as follows: + // 1. a scratch buffer of the same size as the list is passed in + // 2. exponents are written / copied to that scratch buffer + // 3. scratch buffer is sorted, thus the exponents are in order + // 4. for each item in the scratch buffer (= ordered exponents) it's compared w/ the previous one + // * if equal, then increment a counter + // * else check if the counter is $>0$ and if so write the exponent to the result + // + // For writing the result the same scratch buffer is used, as by definition the index to write the result + // is `<=` the iteration index, so no overlap, etc. can occur. + Debug.Assert(list.Count == buffer.Length); int idx = 0; From 58480c9452649b4318db3e2af4d7ff71c04e8650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Tue, 17 Jun 2025 19:33:53 +0200 Subject: [PATCH 5/9] GaloisField.ShrinkAlphaExp use bit-hacks instead of division / modulo --- QRCoder/QRCodeGenerator/GaloisField.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/QRCoder/QRCodeGenerator/GaloisField.cs b/QRCoder/QRCodeGenerator/GaloisField.cs index d1e95f96..23e79457 100644 --- a/QRCoder/QRCodeGenerator/GaloisField.cs +++ b/QRCoder/QRCodeGenerator/GaloisField.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; namespace QRCoder; @@ -43,6 +44,9 @@ public static int GetAlphaExpFromIntVal(int intVal) /// This is particularly necessary when performing multiplications in the field which can result in exponents exceeding the field's maximum. /// public static int ShrinkAlphaExp(int alphaExp) - => (alphaExp % 256) + (alphaExp / 256); + { + Debug.Assert(alphaExp >= 0); + return (int)((uint)alphaExp % 256 + (uint)alphaExp / 256); + } } } From d69487503c40811e16155f14095807ff86045304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Tue, 17 Jun 2025 19:34:40 +0200 Subject: [PATCH 6/9] CodewordBlock uses a pooled byte-array --- QRCoder/QRCodeGenerator.cs | 26 ++++++++++++++++++------ QRCoder/QRCodeGenerator/CodewordBlock.cs | 19 ++++++++++++++--- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 4ae59d63..9018ff0e 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -218,6 +218,13 @@ private static QRCodeData GenerateQrCode(BitArray bitArray, ECCLevel eccLevel, i // Place interleaved data on module matrix var qrData = PlaceModules(); +#if NETCOREAPP + foreach (var codeWord in codeWordWithECC) + { + codeWord.Dispose(); + } +#endif + return qrData; @@ -287,7 +294,7 @@ int CalculateInterleavedLength() for (var i = 0; i < eccInfo.ECCPerBlock; i++) { foreach (var codeBlock in codeWordWithECC) - if (codeBlock.ECCWords.Length > i) + if (codeBlock.ECCWords.Count > i) length += 8; } length += CapacityTables.GetRemainderBits(version); @@ -310,8 +317,8 @@ BitArray InterleaveData() for (var i = 0; i < eccInfo.ECCPerBlock; i++) { foreach (var codeBlock in codeWordWithECC) - if (codeBlock.ECCWords.Length > i) - pos = DecToBin(codeBlock.ECCWords[i], 8, data, pos); + if (codeBlock.ECCWords.Count > i) + pos = DecToBin(codeBlock.ECCWords.Array![i], 8, data, pos); } return data; @@ -485,7 +492,7 @@ private static void GetVersionString(BitArray vStr, int version) /// This method applies polynomial division, using the message polynomial and a generator polynomial, /// to compute the remainder which forms the ECC codewords. /// - private static byte[] CalculateECCWords(BitArray bitArray, int offset, int count, ECCInfo eccInfo, Polynom generatorPolynomBase) + private static ArraySegment CalculateECCWords(BitArray bitArray, int offset, int count, ECCInfo eccInfo, Polynom generatorPolynomBase) { var eccWords = eccInfo.ECCPerBlock; // Calculate the message polynomial from the bit array data. @@ -533,9 +540,16 @@ private static byte[] CalculateECCWords(BitArray bitArray, int offset, int count generatorPolynom.Dispose(); // Convert the resulting polynomial into a byte array representing the ECC codewords. - var ret = new byte[leadTermSource.Count]; +#if NETCOREAPP + var array = ArrayPool.Shared.Rent(leadTermSource.Count); + var ret = new ArraySegment(array, 0, leadTermSource.Count); +#else + var ret = new ArraySegment(new byte[leadTermSource.Count]); + var array = ret.Array!; +#endif + for (var i = 0; i < leadTermSource.Count; i++) - ret[i] = (byte)leadTermSource[i].Coefficient; + array[i] = (byte)leadTermSource[i].Coefficient; // Free memory used by the message polynomial. leadTermSource.Dispose(); diff --git a/QRCoder/QRCodeGenerator/CodewordBlock.cs b/QRCoder/QRCodeGenerator/CodewordBlock.cs index 6960219e..28bce668 100644 --- a/QRCoder/QRCodeGenerator/CodewordBlock.cs +++ b/QRCoder/QRCodeGenerator/CodewordBlock.cs @@ -1,3 +1,9 @@ +using System; + +#if NETCOREAPP +using System.Buffers; +#endif + namespace QRCoder; public partial class QRCodeGenerator @@ -6,7 +12,10 @@ public partial class QRCodeGenerator /// Represents a block of codewords in a QR code. QR codes are divided into several blocks for error correction purposes. /// Each block contains a series of data codewords followed by error correction codewords. /// - private struct CodewordBlock + private readonly struct CodewordBlock +#if NETCOREAPP + : IDisposable +#endif { /// /// Initializes a new instance of the CodewordBlock struct with specified arrays of code words and error correction (ECC) words. @@ -14,7 +23,7 @@ private struct CodewordBlock /// The offset of the data codewords within the main BitArray. Data codewords carry the actual information. /// The length in bits of the data codewords within the main BitArray. /// The array of error correction codewords for this block. These codewords help recover the data if the QR code is damaged. - public CodewordBlock(int codeWordsOffset, int codeWordsLength, byte[] eccWords) + public CodewordBlock(int codeWordsOffset, int codeWordsLength, ArraySegment eccWords) { CodeWordsOffset = codeWordsOffset; CodeWordsLength = codeWordsLength; @@ -34,6 +43,10 @@ public CodewordBlock(int codeWordsOffset, int codeWordsLength, byte[] eccWords) /// /// Gets the error correction codewords associated with this block. /// - public byte[] ECCWords { get; } + public ArraySegment ECCWords { get; } + +#if NETCOREAPP + public void Dispose() => ArrayPool.Shared.Return(ECCWords.Array!); +#endif } } From 9baf6cecdf95a26f8ce571755319756b15d74835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Tue, 17 Jun 2025 21:44:44 +0200 Subject: [PATCH 7/9] Avoid allocating closures in CapacityTables --- QRCoder/QRCodeGenerator/CapacityTables.cs | 31 ++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/QRCoder/QRCodeGenerator/CapacityTables.cs b/QRCoder/QRCodeGenerator/CapacityTables.cs index 59f0d2c9..32d3acc4 100644 --- a/QRCoder/QRCodeGenerator/CapacityTables.cs +++ b/QRCoder/QRCodeGenerator/CapacityTables.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -36,7 +37,17 @@ private static class CapacityTables /// block group details, and other parameters required for encoding error correction data. /// public static ECCInfo GetEccInfo(int version, ECCLevel eccLevel) - => _capacityECCTable.Single(x => x.Version == version && x.ErrorCorrectionLevel == eccLevel); + { + foreach (var item in _capacityECCTable) + { + if (item.Version == version && item.ErrorCorrectionLevel == eccLevel) + { + return item; + } + } + + throw new InvalidOperationException("No item found"); // same exception type as Linq would throw + } /// /// Retrieves the capacity information for a specific QR code version. @@ -92,11 +103,19 @@ public static int CalculateMinimumVersion(int length, EncodingMode encMode, ECCL } // if no version was found, throw an exception - var maxSizeByte = _capacityTable.Where( - x => x.Details.Any( - y => (y.ErrorCorrectionLevel == eccLevel)) - ).Max(x => x.Details.Single(y => y.ErrorCorrectionLevel == eccLevel).CapacityDict[encMode]); - throw new QRCoder.Exceptions.DataTooLongException(eccLevel.ToString(), encMode.ToString(), maxSizeByte); + // In order to get the maxSizeByte we use a throw-helper method to avoid the allocation of a closure + Throw(encMode, eccLevel); + throw null!; // this is needed to make the compiler happy + + static void Throw(EncodingMode encMode, ECCLevel eccLevel) + { + var maxSizeByte = _capacityTable.Where( + x => x.Details.Any( + y => (y.ErrorCorrectionLevel == eccLevel)) + ).Max(x => x.Details.Single(y => y.ErrorCorrectionLevel == eccLevel).CapacityDict[encMode]); + + throw new Exceptions.DataTooLongException(eccLevel.ToString(), encMode.ToString(), maxSizeByte); + } } /// From 38277b10f011cadb026aefb1b81e4670db3c3cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Tue, 17 Jun 2025 21:59:15 +0200 Subject: [PATCH 8/9] Avoid allocation closure in QRCodeGenerator --- QRCoder/QRCodeGenerator.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 9018ff0e..e6cdac55 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -121,8 +121,14 @@ public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, boo //Version was passed as fixed version via parameter. Thus let's check if chosen version is valid. if (minVersion > version) { - var maxSizeByte = CapacityTables.GetVersionInfo(version).Details.First(x => x.ErrorCorrectionLevel == eccLevel).CapacityDict[encoding]; - throw new QRCoder.Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte); + // Use a throw-helper to avoid allocating a closure + Throw(eccLevel, encoding, version); + + static void Throw(ECCLevel eccLevel, EncodingMode encoding, int version) + { + var maxSizeByte = CapacityTables.GetVersionInfo(version).Details.First(x => x.ErrorCorrectionLevel == eccLevel).CapacityDict[encoding]; + throw new Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte); + } } } From 8bc3c30deb854984b4b6ba1317e9467072609277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Tue, 17 Jun 2025 22:25:45 +0200 Subject: [PATCH 9/9] Use a simple cache for the list codeWordWithECC --- QRCoder/QRCodeGenerator.cs | 10 ++-------- QRCoder/QRCodeGenerator/CodewordBlock.cs | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index e6cdac55..4ae2b9d2 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -224,16 +224,10 @@ private static QRCodeData GenerateQrCode(BitArray bitArray, ECCLevel eccLevel, i // Place interleaved data on module matrix var qrData = PlaceModules(); -#if NETCOREAPP - foreach (var codeWord in codeWordWithECC) - { - codeWord.Dispose(); - } -#endif + CodewordBlock.ReturnList(codeWordWithECC); return qrData; - // fills the bit array with a repeating pattern to reach the required length void PadData() { @@ -268,7 +262,7 @@ List CalculateECCBlocks() using (var generatorPolynom = CalculateGeneratorPolynom(eccInfo.ECCPerBlock)) { //Calculate error correction words - codewordBlocks = new List(eccInfo.BlocksInGroup1 + eccInfo.BlocksInGroup2); + codewordBlocks = CodewordBlock.GetList(eccInfo.BlocksInGroup1 + eccInfo.BlocksInGroup2); AddCodeWordBlocks(1, eccInfo.BlocksInGroup1, eccInfo.CodewordsInGroup1, 0, bitArray.Length, generatorPolynom); int offset = eccInfo.BlocksInGroup1 * eccInfo.CodewordsInGroup1 * 8; AddCodeWordBlocks(2, eccInfo.BlocksInGroup2, eccInfo.CodewordsInGroup2, offset, bitArray.Length - offset, generatorPolynom); diff --git a/QRCoder/QRCodeGenerator/CodewordBlock.cs b/QRCoder/QRCodeGenerator/CodewordBlock.cs index 28bce668..63c52115 100644 --- a/QRCoder/QRCodeGenerator/CodewordBlock.cs +++ b/QRCoder/QRCodeGenerator/CodewordBlock.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Threading; #if NETCOREAPP using System.Buffers; @@ -13,9 +15,6 @@ public partial class QRCodeGenerator /// Each block contains a series of data codewords followed by error correction codewords. /// private readonly struct CodewordBlock -#if NETCOREAPP - : IDisposable -#endif { /// /// Initializes a new instance of the CodewordBlock struct with specified arrays of code words and error correction (ECC) words. @@ -45,8 +44,21 @@ public CodewordBlock(int codeWordsOffset, int codeWordsLength, ArraySegment public ArraySegment ECCWords { get; } + private static List? _codewordBlocks; + + public static List GetList(int capacity) + => Interlocked.Exchange(ref _codewordBlocks, null) ?? new List(capacity); + + public static void ReturnList(List list) + { #if NETCOREAPP - public void Dispose() => ArrayPool.Shared.Return(ECCWords.Array!); + foreach (var item in list) + { + ArrayPool.Shared.Return(item.ECCWords.Array!); + } #endif + list.Clear(); + Interlocked.CompareExchange(ref _codewordBlocks, list, null); + } } }