Skip to content

Commit 9ed6c79

Browse files
authored
Reduced some allocations in QRCodeGenerator (NETCORE_APP only) (#595)
* Avoid allocation of gluedPolynoms when possible * Avoid using Linq in MultiplyAlphaPolynoms * Avoid Dictionary allocations in GetNotUniqueExponents * Added comment to GetNotUniqueExponents on how it works * GaloisField.ShrinkAlphaExp use bit-hacks instead of division / modulo * CodewordBlock uses a pooled byte-array * Avoid allocating closures in CapacityTables * Avoid allocation closure in QRCodeGenerator * Use a simple cache for the list codeWordWithECC
1 parent 4f546a6 commit 9ed6c79

File tree

4 files changed

+147
-26
lines changed

4 files changed

+147
-26
lines changed

QRCoder/QRCodeGenerator.cs

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#endif
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Globalization;
89
using System.Linq;
910
using System.Runtime.CompilerServices;
@@ -120,8 +121,14 @@ public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, boo
120121
//Version was passed as fixed version via parameter. Thus let's check if chosen version is valid.
121122
if (minVersion > version)
122123
{
123-
var maxSizeByte = CapacityTables.GetVersionInfo(version).Details.First(x => x.ErrorCorrectionLevel == eccLevel).CapacityDict[encoding];
124-
throw new QRCoder.Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte);
124+
// Use a throw-helper to avoid allocating a closure
125+
Throw(eccLevel, encoding, version);
126+
127+
static void Throw(ECCLevel eccLevel, EncodingMode encoding, int version)
128+
{
129+
var maxSizeByte = CapacityTables.GetVersionInfo(version).Details.First(x => x.ErrorCorrectionLevel == eccLevel).CapacityDict[encoding];
130+
throw new Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte);
131+
}
125132
}
126133
}
127134

@@ -296,8 +303,9 @@ private static QRCodeData GenerateQrCode(BitArray bitArray, ECCLevel eccLevel, i
296303
// Place interleaved data on module matrix
297304
var qrData = PlaceModules();
298305

299-
return qrData;
306+
CodewordBlock.ReturnList(codeWordWithECC);
300307

308+
return qrData;
301309

302310
// fills the bit array with a repeating pattern to reach the required length
303311
void PadData()
@@ -345,7 +353,7 @@ List<CodewordBlock> CalculateECCBlocks()
345353
using (var generatorPolynom = CalculateGeneratorPolynom(eccInfo.ECCPerBlock))
346354
{
347355
//Calculate error correction words
348-
codewordBlocks = new List<CodewordBlock>(eccInfo.BlocksInGroup1 + eccInfo.BlocksInGroup2);
356+
codewordBlocks = CodewordBlock.GetList(eccInfo.BlocksInGroup1 + eccInfo.BlocksInGroup2);
349357
AddCodeWordBlocks(1, eccInfo.BlocksInGroup1, eccInfo.CodewordsInGroup1, 0, bitArray.Length, generatorPolynom);
350358
int offset = eccInfo.BlocksInGroup1 * eccInfo.CodewordsInGroup1 * 8;
351359
AddCodeWordBlocks(2, eccInfo.BlocksInGroup2, eccInfo.CodewordsInGroup2, offset, bitArray.Length - offset, generatorPolynom);
@@ -384,7 +392,7 @@ int CalculateInterleavedLength()
384392
for (var i = 0; i < eccInfo.ECCPerBlock; i++)
385393
{
386394
foreach (var codeBlock in codeWordWithECC)
387-
if (codeBlock.ECCWords.Length > i)
395+
if (codeBlock.ECCWords.Count > i)
388396
length += 8;
389397
}
390398
length += CapacityTables.GetRemainderBits(version);
@@ -414,8 +422,8 @@ BitArray InterleaveData()
414422
for (var i = 0; i < eccInfo.ECCPerBlock; i++)
415423
{
416424
foreach (var codeBlock in codeWordWithECC)
417-
if (codeBlock.ECCWords.Length > i)
418-
pos = DecToBin(codeBlock.ECCWords[i], 8, data, pos);
425+
if (codeBlock.ECCWords.Count > i)
426+
pos = DecToBin(codeBlock.ECCWords.Array![i], 8, data, pos);
419427
}
420428

421429
return data;
@@ -641,7 +649,7 @@ private static void GetVersionString(BitArray vStr, int version)
641649
/// This method applies polynomial division, using the message polynomial and a generator polynomial,
642650
/// to compute the remainder which forms the ECC codewords.
643651
/// </summary>
644-
private static byte[] CalculateECCWords(BitArray bitArray, int offset, int count, ECCInfo eccInfo, Polynom generatorPolynomBase)
652+
private static ArraySegment<byte> CalculateECCWords(BitArray bitArray, int offset, int count, ECCInfo eccInfo, Polynom generatorPolynomBase)
645653
{
646654
var eccWords = eccInfo.ECCPerBlock;
647655
// Calculate the message polynomial from the bit array data.
@@ -689,9 +697,16 @@ private static byte[] CalculateECCWords(BitArray bitArray, int offset, int count
689697
generatorPolynom.Dispose();
690698

691699
// Convert the resulting polynomial into a byte array representing the ECC codewords.
692-
var ret = new byte[leadTermSource.Count];
700+
#if NETCOREAPP
701+
var array = ArrayPool<byte>.Shared.Rent(leadTermSource.Count);
702+
var ret = new ArraySegment<byte>(array, 0, leadTermSource.Count);
703+
#else
704+
var ret = new ArraySegment<byte>(new byte[leadTermSource.Count]);
705+
var array = ret.Array!;
706+
#endif
707+
693708
for (var i = 0; i < leadTermSource.Count; i++)
694-
ret[i] = (byte)leadTermSource[i].Coefficient;
709+
array[i] = (byte)leadTermSource[i].Coefficient;
695710

696711
// Free memory used by the message polynomial.
697712
leadTermSource.Dispose();
@@ -1235,8 +1250,15 @@ private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polyno
12351250
}
12361251

12371252
// Identify and merge terms with the same exponent.
1253+
#if NETCOREAPP
1254+
var toGlue = GetNotUniqueExponents(resultPolynom, resultPolynom.Count <= 128 ? stackalloc int[128].Slice(0, resultPolynom.Count) : new int[resultPolynom.Count]);
1255+
var gluedPolynoms = toGlue.Length <= 128
1256+
? stackalloc PolynomItem[128].Slice(0, toGlue.Length)
1257+
: new PolynomItem[toGlue.Length];
1258+
#else
12381259
var toGlue = GetNotUniqueExponents(resultPolynom);
12391260
var gluedPolynoms = new PolynomItem[toGlue.Length];
1261+
#endif
12401262
var gluedPolynomsIndex = 0;
12411263
foreach (var exponent in toGlue)
12421264
{
@@ -1254,7 +1276,11 @@ private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polyno
12541276

12551277
// Remove duplicated exponents and add the corrected ones back.
12561278
for (int i = resultPolynom.Count - 1; i >= 0; i--)
1279+
#if NETCOREAPP
12571280
if (toGlue.Contains(resultPolynom[i].Exponent))
1281+
#else
1282+
if (Array.IndexOf(toGlue, resultPolynom[i].Exponent) >= 0)
1283+
#endif
12581284
resultPolynom.RemoveAt(i);
12591285
foreach (var polynom in gluedPolynoms)
12601286
resultPolynom.Add(polynom);
@@ -1264,20 +1290,66 @@ private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polyno
12641290
return resultPolynom;
12651291

12661292
// Auxiliary function to identify exponents that appear more than once in the polynomial.
1267-
int[] GetNotUniqueExponents(Polynom list)
1293+
#if NETCOREAPP
1294+
static ReadOnlySpan<int> GetNotUniqueExponents(Polynom list, Span<int> buffer)
12681295
{
1269-
var dic = new Dictionary<int, bool>(list.Count);
1296+
// It works as follows:
1297+
// 1. a scratch buffer of the same size as the list is passed in
1298+
// 2. exponents are written / copied to that scratch buffer
1299+
// 3. scratch buffer is sorted, thus the exponents are in order
1300+
// 4. for each item in the scratch buffer (= ordered exponents) it's compared w/ the previous one
1301+
// * if equal, then increment a counter
1302+
// * else check if the counter is $>0$ and if so write the exponent to the result
1303+
//
1304+
// For writing the result the same scratch buffer is used, as by definition the index to write the result
1305+
// is `<=` the iteration index, so no overlap, etc. can occur.
1306+
1307+
Debug.Assert(list.Count == buffer.Length);
1308+
1309+
int idx = 0;
12701310
foreach (var row in list)
12711311
{
1272-
#if NETCOREAPP
1273-
if (dic.TryAdd(row.Exponent, false))
1274-
dic[row.Exponent] = true;
1312+
buffer[idx++] = row.Exponent;
1313+
}
1314+
1315+
buffer.Sort();
1316+
1317+
idx = 0;
1318+
int expCount = 0;
1319+
int last = buffer[0];
1320+
1321+
for (int i = 1; i < buffer.Length; ++i)
1322+
{
1323+
if (buffer[i] == last)
1324+
{
1325+
expCount++;
1326+
}
1327+
else
1328+
{
1329+
if (expCount > 0)
1330+
{
1331+
Debug.Assert(idx <= i - 1);
1332+
1333+
buffer[idx++] = last;
1334+
expCount = 0;
1335+
}
1336+
}
1337+
1338+
last = buffer[i];
1339+
}
1340+
1341+
return buffer.Slice(0, idx);
1342+
}
12751343
#else
1344+
static int[] GetNotUniqueExponents(Polynom list)
1345+
{
1346+
var dic = new Dictionary<int, bool>(list.Count);
1347+
foreach (var row in list)
1348+
{
12761349
if (!dic.ContainsKey(row.Exponent))
12771350
dic.Add(row.Exponent, false);
12781351
else
12791352
dic[row.Exponent] = true;
1280-
#endif
12811353
}
12821354

12831355
// Collect all exponents that appeared more than once.
@@ -1298,6 +1370,7 @@ int[] GetNotUniqueExponents(Polynom list)
12981370

12991371
return result;
13001372
}
1373+
#endif
13011374
}
13021375

13031376
/// <inheritdoc cref="IDisposable.Dispose"/>

QRCoder/QRCodeGenerator/CapacityTables.cs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34

@@ -37,7 +38,17 @@ private static class CapacityTables
3738
/// block group details, and other parameters required for encoding error correction data.
3839
/// </returns>
3940
public static ECCInfo GetEccInfo(int version, ECCLevel eccLevel)
40-
=> _capacityECCTable.Single(x => x.Version == version && x.ErrorCorrectionLevel == eccLevel);
41+
{
42+
foreach (var item in _capacityECCTable)
43+
{
44+
if (item.Version == version && item.ErrorCorrectionLevel == eccLevel)
45+
{
46+
return item;
47+
}
48+
}
49+
50+
throw new InvalidOperationException("No item found"); // same exception type as Linq would throw
51+
}
4152

4253
/// <summary>
4354
/// Retrieves the capacity information for a specific QR code version.
@@ -94,11 +105,19 @@ public static int CalculateMinimumVersion(int length, EncodingMode encMode, ECCL
94105
}
95106

96107
// if no version was found, throw an exception
97-
var maxSizeByte = _capacityTable.Where(
98-
x => x.Details.Any(
99-
y => (y.ErrorCorrectionLevel == eccLevel))
100-
).Max(x => x.Details.Single(y => y.ErrorCorrectionLevel == eccLevel).CapacityDict[encMode]);
101-
throw new QRCoder.Exceptions.DataTooLongException(eccLevel.ToString(), encMode.ToString(), maxSizeByte);
108+
// In order to get the maxSizeByte we use a throw-helper method to avoid the allocation of a closure
109+
Throw(encMode, eccLevel);
110+
throw null!; // this is needed to make the compiler happy
111+
112+
static void Throw(EncodingMode encMode, ECCLevel eccLevel)
113+
{
114+
var maxSizeByte = _capacityTable.Where(
115+
x => x.Details.Any(
116+
y => (y.ErrorCorrectionLevel == eccLevel))
117+
).Max(x => x.Details.Single(y => y.ErrorCorrectionLevel == eccLevel).CapacityDict[encMode]);
118+
119+
throw new Exceptions.DataTooLongException(eccLevel.ToString(), encMode.ToString(), maxSizeByte);
120+
}
102121
}
103122

104123

QRCoder/QRCodeGenerator/CodewordBlock.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
5+
#if NETCOREAPP
6+
using System.Buffers;
7+
#endif
8+
19
namespace QRCoder;
210

311
public partial class QRCodeGenerator
@@ -6,15 +14,15 @@ public partial class QRCodeGenerator
614
/// Represents a block of codewords in a QR code. QR codes are divided into several blocks for error correction purposes.
715
/// Each block contains a series of data codewords followed by error correction codewords.
816
/// </summary>
9-
private struct CodewordBlock
17+
private readonly struct CodewordBlock
1018
{
1119
/// <summary>
1220
/// Initializes a new instance of the CodewordBlock struct with specified arrays of code words and error correction (ECC) words.
1321
/// </summary>
1422
/// <param name="codeWordsOffset">The offset of the data codewords within the main BitArray. Data codewords carry the actual information.</param>
1523
/// <param name="codeWordsLength">The length in bits of the data codewords within the main BitArray.</param>
1624
/// <param name="eccWords">The array of error correction codewords for this block. These codewords help recover the data if the QR code is damaged.</param>
17-
public CodewordBlock(int codeWordsOffset, int codeWordsLength, byte[] eccWords)
25+
public CodewordBlock(int codeWordsOffset, int codeWordsLength, ArraySegment<byte> eccWords)
1826
{
1927
CodeWordsOffset = codeWordsOffset;
2028
CodeWordsLength = codeWordsLength;
@@ -34,6 +42,23 @@ public CodewordBlock(int codeWordsOffset, int codeWordsLength, byte[] eccWords)
3442
/// <summary>
3543
/// Gets the error correction codewords associated with this block.
3644
/// </summary>
37-
public byte[] ECCWords { get; }
45+
public ArraySegment<byte> ECCWords { get; }
46+
47+
private static List<CodewordBlock>? _codewordBlocks;
48+
49+
public static List<CodewordBlock> GetList(int capacity)
50+
=> Interlocked.Exchange(ref _codewordBlocks, null) ?? new List<CodewordBlock>(capacity);
51+
52+
public static void ReturnList(List<CodewordBlock> list)
53+
{
54+
#if NETCOREAPP
55+
foreach (var item in list)
56+
{
57+
ArrayPool<byte>.Shared.Return(item.ECCWords.Array!);
58+
}
59+
#endif
60+
list.Clear();
61+
Interlocked.CompareExchange(ref _codewordBlocks, list, null);
62+
}
3863
}
3964
}

QRCoder/QRCodeGenerator/GaloisField.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics;
23

34
namespace QRCoder;
45

@@ -43,6 +44,9 @@ public static int GetAlphaExpFromIntVal(int intVal)
4344
/// This is particularly necessary when performing multiplications in the field which can result in exponents exceeding the field's maximum.
4445
/// </summary>
4546
public static int ShrinkAlphaExp(int alphaExp)
46-
=> (alphaExp % 256) + (alphaExp / 256);
47+
{
48+
Debug.Assert(alphaExp >= 0);
49+
return (int)((uint)alphaExp % 256 + (uint)alphaExp / 256);
50+
}
4751
}
4852
}

0 commit comments

Comments
 (0)