Skip to content

Commit 7a7913c

Browse files
committed
Improve group index generation
See dotnet/runtime#115033 and GroupIndexBlockBench for why we're using a span here now. Thanks Warpten for suggesting the span solution!
1 parent ed6fb1b commit 7a7913c

File tree

2 files changed

+119
-9
lines changed

2 files changed

+119
-9
lines changed

TACTBench/GroupIndexBlockBench.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using BenchmarkDotNet.Attributes;
2+
using System.Buffers.Binary;
3+
using System.Runtime.InteropServices;
4+
5+
namespace TACTBench
6+
{
7+
[MemoryDiagnoser]
8+
public class GroupIndexBlockBench
9+
{
10+
private List<Entry> Entries = [];
11+
private const int outputEntriesPerBlock = 157;
12+
13+
/* On .NET 9:
14+
| Method | Mean | Error | StdDev | Gen0 | Allocated |
15+
|---------------------- |---------:|----------:|----------:|---------:|----------:|
16+
| LINQ | 4.294 ms | 0.0396 ms | 0.0370 ms | 7.8125 | 611520 B |
17+
| EnumerableChunk | 6.672 ms | 0.0760 ms | 0.0674 ms | 156.2500 | 8156433 B |
18+
| CollectionMarshalSpan | 2.199 ms | 0.0224 ms | 0.0199 ms | - | - |
19+
*/
20+
21+
// Someone figure out how to run this on .NET 10 :)
22+
23+
[GlobalSetup]
24+
public void Setup()
25+
{
26+
Entries = [];
27+
var random = new Random();
28+
for (var i = 0; i < 1_000_000; i++)
29+
{
30+
var entry = new Entry()
31+
{
32+
Size = i,
33+
ArchiveIndex = (short)(i / 10000),
34+
Offset = i
35+
};
36+
37+
random.NextBytes(entry.EKey);
38+
39+
Entries.Add(entry);
40+
}
41+
}
42+
43+
[Benchmark]
44+
public void LINQ()
45+
{
46+
var totalBlocks = (Entries.Count + outputEntriesPerBlock - 1) / outputEntriesPerBlock;
47+
for (int i = 0; i < totalBlocks; i++)
48+
{
49+
var block = Entries.Skip(i * outputEntriesPerBlock).Take(outputEntriesPerBlock);
50+
foreach (var entry in block)
51+
{
52+
Process(entry);
53+
}
54+
}
55+
}
56+
57+
[Benchmark]
58+
public void EnumerableChunk()
59+
{
60+
foreach (var block in Entries.Chunk(outputEntriesPerBlock))
61+
{
62+
foreach (var entry in block)
63+
{
64+
Process(entry);
65+
}
66+
}
67+
}
68+
69+
[Benchmark]
70+
public void CollectionMarshalSpan()
71+
{
72+
var span = CollectionsMarshal.AsSpan(Entries);
73+
int totalBlocks = (span.Length + outputEntriesPerBlock - 1) / outputEntriesPerBlock;
74+
75+
for (int i = 0; i < totalBlocks; i++)
76+
{
77+
int start = i * outputEntriesPerBlock;
78+
int length = Math.Min(outputEntriesPerBlock, span.Length - start);
79+
var block = span.Slice(start, length);
80+
81+
foreach (var entry in block)
82+
{
83+
Process(entry);
84+
}
85+
}
86+
}
87+
88+
private static void Process(Entry entry)
89+
{
90+
// Close enough
91+
BinaryPrimitives.ReverseEndianness(entry.Size);
92+
BinaryPrimitives.ReverseEndianness((short)entry.ArchiveIndex);
93+
BinaryPrimitives.ReverseEndianness(entry.Offset);
94+
}
95+
96+
public class Entry
97+
{
98+
public byte[] EKey = new byte[16];
99+
public int Size;
100+
public short ArchiveIndex;
101+
public int Offset;
102+
}
103+
}
104+
}

TACTSharp/GroupIndex.cs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Buffers.Binary;
2+
using System.Runtime.InteropServices;
23
using System.Security.Cryptography;
34

45
namespace TACTSharp
@@ -90,23 +91,28 @@ public string Generate(CDN CDN, Settings Settings, string? hash, string[] archiv
9091
var ofsStartOfTocEkeys = outputNumBlocks * outputBlockSizeBytes;
9192
var ofsStartOfTocBlockHashes = ofsStartOfTocEkeys + outputFooter.keyBytes * outputNumBlocks;
9293

93-
for (var i = 0; i < outputNumBlocks; i++)
94+
// See https://github.com/dotnet/runtime/issues/115033 and GroupIndexBlockBench for why we're using a span over entries here.
95+
// Not that it matters a ton since groupindex gen should only run once per cdnconfig, but still.
96+
var entriesSpan = CollectionsMarshal.AsSpan(Entries);
97+
int totalBlocks = (entriesSpan.Length + outputEntriesPerBlock - 1) / outputEntriesPerBlock;
98+
99+
for (int blockIndex = 0; blockIndex < totalBlocks; blockIndex++)
94100
{
95-
var startOfBlock = i * outputBlockSizeBytes;
96-
bin.BaseStream.Position = startOfBlock;
101+
int start = blockIndex * outputEntriesPerBlock;
102+
int length = Math.Min(outputEntriesPerBlock, entriesSpan.Length - start);
103+
var blockSpan = entriesSpan.Slice(start, length);
104+
bin.BaseStream.Position = blockIndex * outputBlockSizeBytes;
97105

98-
var blockEntries = Entries.Skip(i * outputEntriesPerBlock).Take(outputEntriesPerBlock).ToArray();
99-
for (var j = 0; j < blockEntries.Length; j++)
106+
foreach (var entry in blockSpan)
100107
{
101-
var entry = blockEntries[j];
102108
bin.Write(entry.EKey);
103109
bin.Write(BinaryPrimitives.ReverseEndianness(entry.Size));
104110
bin.Write(BinaryPrimitives.ReverseEndianness((short)entry.ArchiveIndex));
105111
bin.Write(BinaryPrimitives.ReverseEndianness(entry.Offset));
106112
}
107-
bin.BaseStream.Position = ofsStartOfTocEkeys + i * outputFooter.keyBytes;
108-
bin.Write(blockEntries.Last().EKey);
109-
bin.BaseStream.Position = ofsStartOfTocBlockHashes + i * outputFooter.hashBytes;
113+
bin.BaseStream.Position = ofsStartOfTocEkeys + blockIndex * outputFooter.keyBytes;
114+
bin.Write(blockSpan[^1].EKey);
115+
bin.BaseStream.Position = ofsStartOfTocBlockHashes + blockIndex * outputFooter.hashBytes;
110116
bin.Write(new byte[outputFooter.hashBytes]);
111117
}
112118

0 commit comments

Comments
 (0)