Skip to content

Commit 23bb2da

Browse files
committed
Reduce memory allocation
1 parent c8c74b5 commit 23bb2da

2 files changed

Lines changed: 108 additions & 51 deletions

File tree

QrCodeGenerator/SegmentCompaction.cs

Lines changed: 100 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
using System;
99
using System.Collections.Generic;
10+
using System.Diagnostics;
1011
using System.Diagnostics.CodeAnalysis;
1112

1213
namespace Net.Codecrete.QrCodeGenerator
@@ -43,97 +44,114 @@ internal static List<DataSegment> BuildSegments(ArraySegment<byte> bytes, int ve
4344
// The algorithm first determines the best encoding mode for each byte
4445
// and builds blocks of bytes with the same encoding mode.
4546
var blocks = BuildBlocks(bytes, considerKanjiMode);
47+
var blockCount = blocks.Length;
4648

4749
// Since switching from one mode to another and back requires additional bits,
4850
// the additional cost of switching can be higher than the savings
4951
// from using a more efficient mode. If this is the case, two or three blocks are merged.
5052
// In the first step, short numeric blocks are merged with alphanumeric blocks.
5153
// In the second step, all types of blocks are merged into byte blocks.
52-
MergeBlocks(blocks, version, DataSegmentMode.Alphanumeric,
54+
blockCount = MergeBlocks(blocks, blockCount, version, DataSegmentMode.Alphanumeric,
5355
(mode0, mode1, mode2) => mode0 == DataSegmentMode.Alphanumeric
5456
&& mode1 == DataSegmentMode.Numeric && mode2 == mode0,
5557
(mode0, mode1) => (mode0 == DataSegmentMode.Alphanumeric && mode1 == DataSegmentMode.Numeric)
5658
|| (mode0 == DataSegmentMode.Numeric && mode1 == DataSegmentMode.Alphanumeric)
5759
);
58-
MergeBlocks(blocks, version, DataSegmentMode.Binary,
60+
blockCount = MergeBlocks(blocks, blockCount, version, DataSegmentMode.Binary,
5961
(mode0, mode1, mode2) => mode1 != DataSegmentMode.Binary && mode2 == mode0,
6062
(mode0, mode1) => (mode0 == DataSegmentMode.Binary && mode1 != DataSegmentMode.Binary)
6163
|| (mode0 != DataSegmentMode.Binary && mode1 == DataSegmentMode.Binary)
6264
);
6365

66+
var segments = new List<DataSegment>(blockCount);
6467
var offset = 0;
65-
return blocks.ConvertAll(block =>
68+
for (var i = 0; i < blockCount; i += 1)
6669
{
70+
var block = blocks[i];
6771
var blockBytes = bytes.MakeSlice(offset, block.Length);
6872
offset += block.Length;
69-
return DataSegment.MakeSegment(block.Mode, blockBytes);
70-
});
73+
segments.Add(DataSegment.MakeSegment(block.Mode, blockBytes));
74+
}
75+
76+
return segments;
7177
}
7278

7379
/// <summary>
7480
/// Merges blocks if the length can be reduced.
7581
/// </summary>
76-
/// <param name="blocks">The list of blocks to process.</param>
82+
/// <param name="blocks">The array of blocks to process.</param>
83+
/// <param name="blockCount">The number of active blocks in the array.</param>
7784
/// <param name="version">The QR code version.</param>
7885
/// <param name="mergedMode">The data segment mode of the merged block.</param>
7986
/// <param name="merge3Condition">Condition for testing 3 consecutive blocks.</param>
8087
/// <param name="merge2Condition">Condition for testing 2 consecutive blocks.</param>
88+
/// <returns>Number of remaining blocks in array</returns>
8189
[SuppressMessage("csharpsquid", "S3776")]
82-
private static void MergeBlocks(List<Block> blocks, int version, DataSegmentMode mergedMode,
90+
private static int MergeBlocks(Block[] blocks, int blockCount, int version, DataSegmentMode mergedMode,
8391
Func<DataSegmentMode, DataSegmentMode, DataSegmentMode, bool> merge3Condition,
8492
Func<DataSegmentMode, DataSegmentMode, bool> merge2Condition)
8593
{
8694
var previousCount = -1;
87-
while (blocks.Count > 1 && previousCount != blocks.Count)
95+
while (blockCount > 1 && previousCount != blockCount)
8896
{
89-
previousCount = blocks.Count;
90-
91-
// Work back to front to reduce the amount of copying in `blocks`
92-
var index = blocks.Count - 1;
93-
while (index > 0)
97+
previousCount = blockCount;
98+
99+
var targetIndex = 1;
100+
var sourceIndex = 1;
101+
while (sourceIndex < blockCount)
94102
{
95-
var mode0 = blocks[index].Mode;
96-
var mode1 = blocks[index - 1].Mode;
97-
DataSegmentMode? mode2 = index >= 2 ? blocks[index - 2].Mode : (DataSegmentMode?) null;
103+
var merged = false;
104+
var mode0 = blocks[targetIndex - 1].Mode;
105+
var mode1 = blocks[sourceIndex].Mode;
98106

99-
// Case 1: merge 3 blocks
107+
// Case 1: merge 3 blocks (last processed one plus 2 unprocessed ones)
100108
// Test if the bit stream is shorter if all 3 blocks are merged (using the specified merged mode).
101-
if (mode2 != null && merge3Condition(mode0, mode1, mode2.Value))
109+
if (sourceIndex + 1 < blockCount && merge3Condition(mode0, mode1, blocks[sourceIndex + 1].Mode))
102110
{
103111
var mergedPayloadLength =
104-
blocks[index - 2].Length + blocks[index - 1].Length + blocks[index].Length;
112+
blocks[targetIndex - 1].Length + blocks[sourceIndex].Length + blocks[sourceIndex + 1].Length;
105113
var mergedBlock = new Block { Mode = mergedMode, Length = mergedPayloadLength };
106114
var mergedLength = mergedBlock.GetSegmentLength(version);
107-
var separateLength = blocks[index - 2].GetSegmentLength(version)
108-
+ blocks[index - 1].GetSegmentLength(version)
109-
+ blocks[index].GetSegmentLength(version);
115+
var separateLength = blocks[targetIndex - 1].GetSegmentLength(version)
116+
+ blocks[sourceIndex].GetSegmentLength(version)
117+
+ blocks[sourceIndex + 1].GetSegmentLength(version);
110118
if (mergedLength <= separateLength)
111119
{
112-
blocks[index - 2] = mergedBlock;
113-
blocks.RemoveRange(index - 1, 2);
114-
index -= 1;
120+
blocks[targetIndex - 1] = mergedBlock;
121+
sourceIndex += 2;
122+
merged = true;
115123
}
116124
}
117125

118-
// Case 2: merge 2 blocks
126+
// Case 2: merge 2 blocks (last processed one and the current unprocessed one)
119127
// Test if the bit stream is shorter if the 2 blocks are merged (using the specified merged mode).
120128
else if (merge2Condition(mode0, mode1))
121129
{
122-
var mergedBlock = new Block
123-
{ Mode = mergedMode, Length = blocks[index - 1].Length + blocks[index].Length };
130+
var mergedPayloadLength = blocks[targetIndex - 1].Length + blocks[sourceIndex].Length;
131+
var mergedBlock = new Block { Mode = mergedMode, Length = mergedPayloadLength };
124132
var mergedLength = mergedBlock.GetSegmentLength(version);
125-
var separateLength = blocks[index - 1].GetSegmentLength(version) +
126-
blocks[index].GetSegmentLength(version);
133+
var separateLength = blocks[targetIndex - 1].GetSegmentLength(version) +
134+
blocks[sourceIndex].GetSegmentLength(version);
127135
if (mergedLength <= separateLength)
128136
{
129-
blocks[index - 1] = mergedBlock;
130-
blocks.RemoveAt(index);
137+
blocks[targetIndex - 1] = mergedBlock;
138+
sourceIndex += 1;
139+
merged = true;
131140
}
132141
}
133-
134-
index -= 1;
142+
143+
if (!merged)
144+
{
145+
blocks[targetIndex] = blocks[sourceIndex];
146+
targetIndex += 1;
147+
sourceIndex += 1;
148+
}
135149
}
150+
151+
blockCount = targetIndex;
136152
}
153+
154+
return blockCount;
137155
}
138156

139157
/// <summary>
@@ -146,17 +164,19 @@ private static void MergeBlocks(List<Block> blocks, int version, DataSegmentMode
146164
/// <param name="bytes">Bytes to process</param>
147165
/// <param name="useKanji">If <c>true</c>, Kanji encoding is considered; if <c>false</c>, Kanji encoding is not used.</param>
148166
/// <returns>List of blocks</returns>
149-
private static List<Block> BuildBlocks(ArraySegment<byte> bytes, bool useKanji)
167+
private static Block[] BuildBlocks(ArraySegment<byte> bytes, bool useKanji)
150168
{
151169
if (bytes.Count == 0)
152170
{
153-
return new List<Block>();
171+
return Array.Empty<Block>();
154172
}
155173

156174
var modes = CalcCompactionMode(bytes, useKanji);
157-
175+
158176
// create blocks
159-
var blockList = new List<Block>();
177+
var modeChanges = CountModeChanges(modes);
178+
var blocks = new Block[modeChanges];
179+
var blockCount = 0;
160180
var blockStartIndex = 0;
161181
var previousMode = modes[0];
162182
for (var i = 0; i < modes.Length; i += 1)
@@ -167,14 +187,30 @@ private static List<Block> BuildBlocks(ArraySegment<byte> bytes, bool useKanji)
167187
continue;
168188
}
169189

170-
blockList.Add(new Block { Mode = previousMode, Length = i - blockStartIndex });
190+
blocks[blockCount] = new Block { Mode = (DataSegmentMode) previousMode, Length = i - blockStartIndex };
191+
blockCount += 1;
171192
previousMode = currentMode;
172193
blockStartIndex = i;
173194
}
174195

175-
blockList.Add(new Block { Mode = previousMode, Length = modes.Length - blockStartIndex });
196+
blocks[blockCount] = new Block { Mode = (DataSegmentMode) previousMode, Length = modes.Length - blockStartIndex };
197+
return blocks;
198+
}
176199

177-
return blockList;
200+
private static int CountModeChanges(byte[] modes)
201+
{
202+
var count = 1;
203+
var previousMode = modes[0];
204+
for (var i = 0; i < modes.Length; i += 1)
205+
{
206+
var currentMode = modes[i];
207+
if (currentMode != previousMode)
208+
{
209+
count += 1;
210+
previousMode = currentMode;
211+
}
212+
}
213+
return count;
178214
}
179215

180216
/// <summary>
@@ -190,31 +226,31 @@ private static List<Block> BuildBlocks(ArraySegment<byte> bytes, bool useKanji)
190226
/// <param name="bytes">The bytes to encode.</param>
191227
/// <param name="useKanji">If <c>true</c>, Kanji encoding is considered; if <c>false</c>, Kanji encoding is not used.</param>
192228
/// <returns>An array of the best encoding mode for each byte.</returns>
193-
private static DataSegmentMode[] CalcCompactionMode(ArraySegment<byte> bytes, bool useKanji)
229+
private static byte[] CalcCompactionMode(ArraySegment<byte> bytes, bool useKanji)
194230
{
195231
var len = bytes.Count;
196-
var modes = new DataSegmentMode[len];
232+
var modes = new byte[len];
197233
var index = 0;
198234
while (index < len)
199235
{
200236
var b1 = bytes.At(index);
201237
if (DataSegmentNumeric.IsNumeric(b1))
202238
{
203-
modes[index] = DataSegmentMode.Numeric;
239+
modes[index] = (byte) DataSegmentMode.Numeric;
204240
}
205241
else if (DataSegmentAlphanumeric.IsAlphanumeric(b1))
206242
{
207-
modes[index] = DataSegmentMode.Alphanumeric;
243+
modes[index] = (byte) DataSegmentMode.Alphanumeric;
208244
}
209245
else if (useKanji && index < len - 1 && DataSegmentKanji.IsShiftJisDoubleByte(b1, bytes.At(index + 1)))
210246
{
211-
modes[index] = DataSegmentMode.Kanji;
247+
modes[index] = (byte) DataSegmentMode.Kanji;
212248
index += 1;
213-
modes[index] = DataSegmentMode.Kanji;
249+
modes[index] = (byte) DataSegmentMode.Kanji;
214250
}
215251
else
216252
{
217-
modes[index] = DataSegmentMode.Binary;
253+
modes[index] = (byte) DataSegmentMode.Binary;
218254
}
219255

220256
index += 1;
@@ -230,7 +266,7 @@ private static DataSegmentMode[] CalcCompactionMode(ArraySegment<byte> bytes, bo
230266
/// <summary>
231267
/// Block of bytes with an associated encoding mode.
232268
/// </summary>
233-
private struct Block
269+
internal struct Block
234270
{
235271
/// <summary>
236272
/// Encoding mode.
@@ -249,7 +285,21 @@ private struct Block
249285
/// <returns>Resulting segment length, in bits.</returns>
250286
internal int GetSegmentLength(int version)
251287
{
252-
return DataSegment.GetBitLength(Mode, Length, version);
288+
// Duplicated code for performance
289+
switch (Mode)
290+
{
291+
case DataSegmentMode.Binary:
292+
return 12 + (version <= 9 ? 0 : 8) + Length * 8;
293+
case DataSegmentMode.Numeric:
294+
return 14 + (version + 7) / 17 * 2 + (Length * 10 + 2) / 3;
295+
case DataSegmentMode.Alphanumeric:
296+
return 13 + (version + 7) / 17 * 2 + (Length * 11 + 1) / 2;
297+
case DataSegmentMode.Kanji:
298+
return 12 + (version + 7) / 17 * 2 + Length * 13 / 2;
299+
}
300+
301+
Debug.Assert(false);
302+
return 0;
253303
}
254304

255305
public override string ToString()

QrCodeGeneratorTest/SegmentCompactionTest.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Runtime.Serialization.Formatters;
43
using System.Text;
54
using Xunit;
65
using Xunit.Internal;
@@ -27,6 +26,14 @@ public static TheoryData<string, int, ECI, int> TestCases()
2726
return data;
2827
}
2928

29+
[Theory, CombinatorialData]
30+
public static void TestBlockGetSegmentLength([CombinatorialRange(1, 4)] int modeIndex, [CombinatorialRange(0, 7)] int length, [CombinatorialRange(1, 40)] int version)
31+
{
32+
var mode = (DataSegmentMode)modeIndex;
33+
var block = new SegmentCompaction.Block { Mode = mode, Length = length };
34+
Assert.Equal(DataSegment.GetBitLength(mode, length, version), block.GetSegmentLength(version));
35+
}
36+
3037
private static IEnumerable<TestCase> GenerateTestCases()
3138
{
3239
#if NET

0 commit comments

Comments
 (0)