OXYGENHUB
◆ main openmsx / src / utils / DeltaBlock.cc
10.9 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "DeltaBlock.hh"
2
3
#include "lz4.hh"
4
#include "ranges.hh"
5
6
#include <algorithm>
7
#include <bit>
8
#include <cassert>
9
#include <tuple>
10
#include <utility>
11
#if STATISTICS
12
#include <iostream>
13
#endif
14
#ifdef __SSE2__
15
#include <emmintrin.h>
16
#endif
17
18
namespace openmsx {
19
20
// --- Compressed integers ---
21
22
// See https://en.wikipedia.org/wiki/LEB128 for a description of the
23
// 'unsigned LEB' format.
24
static void storeUleb(std::vector<uint8_t>& result, size_t value)
25
{
26
	do {
27
		uint8_t b = value & 0x7F;
28
		value >>= 7;
29
		if (value) b |= 0x80;
30
		result.push_back(b);
31
	} while (value);
32
}
33
34
[[nodiscard]] static constexpr size_t loadUleb(std::span<const uint8_t>& data)
35
{
36
	size_t result = 0;
37
	int shift = 0;
38
	while (true) {
39
		assert(!data.empty());
40
		uint8_t b = data.front();
41
		data = data.subspan(1);
42
		result |= size_t(b & 0x7F) << shift;
43
		if ((b & 0x80) == 0) return result;
44
		shift += 7;
45
	}
46
}
47
48
49
// --- Helper functions to compare {4,8,16} bytes at aligned memory locations ---
50
51
template<int N> bool comp(const uint8_t* p, const uint8_t* q);
52
53
template<> bool comp<4>(const uint8_t* p, const uint8_t* q)
54
{
55
	return *std::bit_cast<const uint32_t*>(p) ==
56
	       *std::bit_cast<const uint32_t*>(q);
57
}
58
59
template<> bool comp<8>(const uint8_t* p, const uint8_t* q)
60
{
61
	return *std::bit_cast<const uint64_t*>(p) ==
62
	       *std::bit_cast<const uint64_t*>(q);
63
}
64
65
#ifdef __SSE2__
66
template<> bool comp<16>(const uint8_t* p, const uint8_t* q)
67
{
68
	// Tests show that (on my machine) using 1 128-bit load is faster than
69
	// 2 64-bit loads. Even though the actual comparison is slightly more
70
	// complicated with SSE instructions.
71
	__m128i a = _mm_loadu_si128(std::bit_cast<const __m128i*>(p));
72
	__m128i b = _mm_loadu_si128(std::bit_cast<const __m128i*>(q));
73
	__m128i d = _mm_cmpeq_epi8(a, b);
74
	return _mm_movemask_epi8(d) == 0xffff;
75
}
76
#endif
77
78
79
// --- Optimized mismatch function ---
80
81
// This is much like the function std::mismatch(). You pass in two buffers,
82
// the corresponding elements of both buffers are compared and the first
83
// position where the elements no longer match is returned.
84
//
85
// Compared to the std::mismatch() this implementation is faster because:
86
// - We make use of sentinels. This requires to temporarily change the content
87
//   of the buffer. So it won't work with read-only-memory.
88
// - We compare words-at-a-time instead of byte-at-a-time.
89
static std::pair<const uint8_t*, const uint8_t*> scan_mismatch(
90
	const uint8_t* p, const uint8_t* p_end, const uint8_t* q, const uint8_t* q_end)
91
{
92
	assert((p_end - p) == (q_end - q));
93
94
	// When SSE is available, work with 16-byte words, otherwise 4 or 8
95
	// bytes. This routine also benefits from AVX2 instructions. Though not
96
	// all x86_64 CPUs have AVX2 (all have SSE2), so using them requires
97
	// extra run-time checks and that's not worth it at this point.
98
	constexpr ptrdiff_t WORD_SIZE =
99
#ifdef __SSE2__
100
		sizeof(__m128i);
101
#else
102
		sizeof(void*);
103
#endif
104
105
	// Region too small or
106
	// both buffers are differently aligned.
107
	if (((p_end - p) < (2 * WORD_SIZE)) ||
108
	    ((std::bit_cast<uintptr_t>(p) & (WORD_SIZE - 1)) !=
109
	     (std::bit_cast<uintptr_t>(q) & (WORD_SIZE - 1)))) [[unlikely]] {
110
		goto end;
111
	}
112
113
	// Align to WORD_SIZE boundary. No need for end-of-buffer checks.
114
	if (std::bit_cast<uintptr_t>(p) & (WORD_SIZE - 1)) [[unlikely]] {
115
		do {
116
			if (*p != *q) return {p, q};
117
			p += 1; q += 1;
118
		} while (std::bit_cast<uintptr_t>(p) & (WORD_SIZE - 1));
119
	}
120
121
	// Fast path. Compare words-at-a-time.
122
	{
123
		// Place a sentinel in the last full word. This ensures we'll
124
		// find a mismatch within the buffer, and so we can omit the
125
		// end-of-buffer checks.
126
		auto* sentinel = &const_cast<uint8_t*>(p_end)[-WORD_SIZE];
127
		auto save = *sentinel;
128
		*sentinel = ~q_end[-WORD_SIZE];
129
130
		while (comp<WORD_SIZE>(p, q)) {
131
			p += WORD_SIZE; q += WORD_SIZE;
132
		}
133
134
		// Restore sentinel.
135
		*sentinel = save;
136
	}
137
138
	// Slow path. This handles:
139
	// - Small or differently aligned buffers.
140
	// - The bytes at and after the (restored) sentinel.
141
end:	return std::mismatch(p, p_end, q);
142
}
143
144
145
// --- Optimized scan_match function ---
146
147
// Like scan_mismatch() above, but searches two buffers for the first
148
// corresponding equal (instead of not-equal) bytes.
149
//
150
// Like scan_mismatch(), this places a temporary sentinel in the buffer, so the
151
// buffer cannot be read-only memory.
152
//
153
// Unlike scan_mismatch() it's less obvious how to perform this function
154
// word-at-a-time (it's possible with some bit hacks). Though luckily this
155
// function is also less performance critical.
156
[[nodiscard]] static constexpr std::pair<const uint8_t*, const uint8_t*> scan_match(
157
	const uint8_t* p, const uint8_t* p_end, const uint8_t* q, const uint8_t* q_end)
158
{
159
	assert((p_end - p) == (q_end - q));
160
161
	// Code below is functionally equivalent to:
162
	//   while ((p != p_end) && (*p != *q)) { ++p; ++q; }
163
	//   return {p, q};
164
165
	if (p == p_end) return {p, q};
166
167
	auto* p_last = const_cast<uint8_t*>(p_end - 1);
168
	auto save = *p_last;
169
	*p_last = q_end[-1]; // make p_end[-1] == q_end[-1]
170
171
	while (*p != *q) { ++p; ++q; }
172
173
	*p_last = save;
174
	if ((p == p_last) && (*p != *q)) { ++p; ++q; }
175
	return {p, q};
176
}
177
178
179
// --- delta (de)compression routines ---
180
181
// Calculate a 'delta' between two binary buffers of equal size.
182
// The result is a stream of:
183
//   n1 number of bytes are equal
184
//   n2 number of bytes are different, and here are the bytes
185
//   n3 number of bytes are equal
186
//   ...
187
[[nodiscard]] static std::vector<uint8_t> calcDelta(
188
	const uint8_t* oldBuf, std::span<const uint8_t> newBuf)
189
{
190
	std::vector<uint8_t> result;
191
192
	const auto* p = oldBuf;
193
	const auto* q = newBuf.data();
194
	auto size = newBuf.size();
195
	const auto* p_end = p + size;
196
	const auto* q_end = q + size;
197
198
	// scan equal bytes (possibly zero)
199
	const auto* q1 = q;
200
	std::tie(p, q) = scan_mismatch(p, p_end, q, q_end);
201
	auto n1 = q - q1;
202
	storeUleb(result, n1);
203
204
	while (q != q_end) {
205
		assert(*p != *q);
206
207
		const auto* q2 = q;
208
	different:
209
		std::tie(p, q) = scan_match(p + 1, p_end, q + 1, q_end);
210
		auto n2 = q - q2;
211
212
		const auto* q3 = q;
213
		std::tie(p, q) = scan_mismatch(p, p_end, q, q_end);
214
		auto n3 = q - q3;
215
		if ((q != q_end) && (n3 <= 2)) goto different;
216
217
		storeUleb(result, n2);
218
		result.insert(result.end(), q2, q3);
219
220
		if (n3 != 0) storeUleb(result, n3);
221
	}
222
223
	result.shrink_to_fit();
224
	return result;
225
}
226
227
// Apply a previously calculated 'delta' to 'oldBuf' to get 'newbuf'.
228
static void applyDeltaInPlace(std::span<uint8_t> buf, std::span<const uint8_t> delta)
229
{
230
	while (!buf.empty()) {
231
		auto n1 = loadUleb(delta);
232
		buf = buf.subspan(n1);
233
		if (buf.empty()) break;
234
235
		auto n2 = loadUleb(delta);
236
		copy_to_range(delta.subspan(0, n2), buf);
237
		buf   = buf  .subspan(n2);
238
		delta = delta.subspan(n2);
239
	}
240
}
241
242
#if STATISTICS
243
244
// class DeltaBlock
245
246
DeltaBlock::~DeltaBlock()
247
{
248
	globalAllocSize -= allocSize;
249
	std::cout << "stat: ~DeltaBlock " << globalAllocSize
250
	          << " (-" << allocSize << ")\n";
251
}
252
253
#endif
254
255
// class DeltaBlockCopy
256
257
DeltaBlockCopy::DeltaBlockCopy(std::span<const uint8_t> data)
258
	: block(data.size())
259
{
260
#ifdef DEBUG
261
	sha1 = SHA1::calc(data);
262
#endif
263
	copy_to_range(data, std::span{block});
264
	assert(!compressed());
265
#if STATISTICS
266
	allocSize = size;
267
	globalAllocSize += allocSize;
268
	std::cout << "stat: DeltaBlockCopy " << globalAllocSize
269
	          << " (+" << allocSize << ")\n";
270
#endif
271
}
272
273
void DeltaBlockCopy::apply(std::span<uint8_t> dst) const
274
{
275
	if (compressed()) {
276
		LZ4::decompress(block.data(), dst.data(), int(compressedSize), int(dst.size()));
277
	} else {
278
		copy_to_range(std::span{block.data(), dst.size()}, dst);
279
	}
280
#ifdef DEBUG
281
	assert(SHA1::calc(dst) == sha1);
282
#endif
283
}
284
285
void DeltaBlockCopy::compress(size_t size)
286
{
287
	if (compressed()) return;
288
289
	size_t dstLen = LZ4::compressBound(int(size));
290
	MemBuffer<uint8_t> buf2(dstLen);
291
	dstLen = LZ4::compress(block.data(), buf2.data(), int(size));
292
293
	if (dstLen >= size) {
294
		// compression isn't beneficial
295
		return;
296
	}
297
	compressedSize = dstLen;
298
	std::swap(block, buf2);
299
	block.resize(compressedSize); // shrink to fit
300
	assert(compressed());
301
#ifdef DEBUG
302
	MemBuffer<uint8_t> buf3(size);
303
	apply({buf3.data(), size});
304
	assert(std::ranges::equal(std::span{buf3.data(), size}, std::span{buf2.data(), size}));
305
#endif
306
#if STATISTICS
307
	int delta = compressedSize - allocSize;
308
	allocSize = compressedSize;
309
	globalAllocSize += delta;
310
	std::cout << "stat: compress " << globalAllocSize
311
	          << " (" << delta << ")\n";
312
#endif
313
}
314
315
const uint8_t* DeltaBlockCopy::getData()
316
{
317
	assert(!compressed());
318
	return block.data();
319
}
320
321
322
// class DeltaBlockDiff
323
324
DeltaBlockDiff::DeltaBlockDiff(
325
		std::shared_ptr<DeltaBlockCopy> prev_,
326
		std::span<const uint8_t> data)
327
	: prev(std::move(prev_))
328
	, delta(calcDelta(prev->getData(), data))
329
{
330
#ifdef DEBUG
331
	sha1 = SHA1::calc(data);
332
333
	MemBuffer<uint8_t> buf(data.size());
334
	apply({buf.data(), data.size()});
335
	assert(std::ranges::equal(std::span{buf.data(), data.size()}, data));
336
#endif
337
#if STATISTICS
338
	allocSize = delta.size();
339
	globalAllocSize += allocSize;
340
	std::cout << "stat: DeltaBlockDiff " << globalAllocSize
341
	          << " (+" << allocSize << ")\n";
342
#endif
343
}
344
345
void DeltaBlockDiff::apply(std::span<uint8_t> dst) const
346
{
347
	prev->apply(dst);
348
	applyDeltaInPlace(dst, delta);
349
#ifdef DEBUG
350
	assert(SHA1::calc(dst) == sha1);
351
#endif
352
}
353
354
size_t DeltaBlockDiff::getDeltaSize() const
355
{
356
	return delta.size();
357
}
358
359
360
// class LastDeltaBlocks
361
362
std::shared_ptr<DeltaBlock> LastDeltaBlocks::createNew(
363
		const void* id, std::span<const uint8_t> data)
364
{
365
	auto size = data.size();
366
	auto it = std::ranges::lower_bound(infos, std::tuple(id, size), {},
367
		[](const Info& info) { return std::tuple(info.id, info.size); });
368
	if ((it == end(infos)) || (it->id != id) || (it->size != size)) {
369
		// no previous info yet
370
		it = infos.emplace(it, id, size);
371
	}
372
	assert(it->id   == id);
373
	assert(it->size == size);
374
375
	auto ref = it->ref.lock();
376
	if (it->accSize >= size || !ref) {
377
		if (ref) {
378
			// We will switch to a new DeltaBlockCopy object. So
379
			// now is a good time to compress the old one.
380
			ref->compress(size);
381
		}
382
		// Heuristic: create a new block when too many small
383
		// differences have accumulated.
384
		auto b = std::make_shared<DeltaBlockCopy>(data);
385
		it->ref = b;
386
		it->last = b;
387
		it->accSize = 0;
388
		return b;
389
	} else {
390
		// Create diff based on earlier reference block.
391
		// Reference remains unchanged.
392
		auto b = std::make_shared<DeltaBlockDiff>(ref, data);
393
		it->last = b;
394
		it->accSize += b->getDeltaSize();
395
		return b;
396
	}
397
}
398
399
std::shared_ptr<DeltaBlock> LastDeltaBlocks::createNullDiff(
400
		const void* id, std::span<const uint8_t> data)
401
{
402
	auto size = data.size();
403
	auto it = std::ranges::lower_bound(infos, id, {}, &Info::id);
404
	if ((it == end(infos)) || (it->id != id)) {
405
		// no previous block yet
406
		it = infos.emplace(it, id, size);
407
	}
408
	assert(it->size == size);
409
410
	auto last = it->last.lock();
411
	if (!last) {
412
		auto b = std::make_shared<DeltaBlockCopy>(data);
413
		it->ref = b;
414
		it->last = b;
415
		it->accSize = 0;
416
		return b;
417
	} else {
418
#ifdef DEBUG
419
		assert(SHA1::calc(data) == last->sha1);
420
#endif
421
		return last;
422
	}
423
}
424
425
void LastDeltaBlocks::clear()
426
{
427
	for (const Info& info : infos) {
428
		if (auto ref = info.ref.lock()) {
429
			ref->compress(info.size);
430
		}
431
	}
432
	infos.clear();
433
}
434
435
} // namespace openmsx