Skip to content

Commit 04bb7e8

Browse files
committed
feat(segmenter): cyclic hash caching for *massive* performance gain
This makes the segmenter about 70% faster on a typical data set: ``` ./mkdwarfs -i /usr/bin --order=path --file-hash=none -C null -l9 --metadata-compression=null -o /dev/null --force ran 1.70 ± 0.01 times faster than ./mkdwarfs.before -i /usr/bin --order=path --file-hash=none -C null -l9 --metadata-compression=null -o /dev/null --force ```
1 parent fb43541 commit 04bb7e8

2 files changed

Lines changed: 143 additions & 31 deletions

File tree

include/dwarfs/writer/internal/cyclic_hash.h

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,17 @@ namespace dwarfs::writer::internal {
3232

3333
class rsync_hash {
3434
public:
35+
using value_type = std::uint32_t;
36+
3537
rsync_hash() = default;
3638

37-
DWARFS_FORCE_INLINE uint32_t operator()() const {
38-
return a_ | (static_cast<uint32_t>(b_) << 16);
39+
DWARFS_FORCE_INLINE value_type operator()() const {
40+
return a_ | (static_cast<value_type>(b_) << 16);
41+
}
42+
43+
DWARFS_FORCE_INLINE void set(value_type hash) {
44+
a_ = static_cast<uint16_t>(hash & 0xFFFF);
45+
b_ = static_cast<uint16_t>((hash >> 16) & 0xFFFF);
3946
}
4047

4148
DWARFS_FORCE_INLINE void update(uint8_t inbyte) {
@@ -56,12 +63,12 @@ class rsync_hash {
5663
len_ = 0;
5764
}
5865

59-
static DWARFS_FORCE_INLINE constexpr uint32_t
66+
static DWARFS_FORCE_INLINE constexpr value_type
6067
repeating_window(uint8_t byte, size_t length) {
6168
auto v = static_cast<uint16_t>(byte);
6269
auto a = static_cast<uint16_t>(v * length);
6370
auto b = static_cast<uint16_t>(v * (length * (length + 1)) / 2);
64-
return static_cast<uint32_t>(a) | (static_cast<uint32_t>(b) << 16);
71+
return static_cast<value_type>(a) | (static_cast<value_type>(b) << 16);
6572
}
6673

6774
private:

src/writer/segmenter.cpp

Lines changed: 132 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ struct segmenter_stats {
9898
uint64_t bloom_lookups{0};
9999
uint64_t bloom_hits{0};
100100
uint64_t bloom_true_positives{0};
101+
uint64_t total_appends{0};
102+
uint64_t total_appends_with_cached_hash{0};
101103
value_stream_quantile_estimator l2_collision_vec_size{0.5, 0.75, 0.9, 0.95,
102104
0.99};
103105
};
@@ -403,7 +405,9 @@ class ConstantGranularityPolicy : private GranularityPolicyBase {
403405
return kGranularity;
404406
}
405407

406-
static DWARFS_FORCE_INLINE bool compile_time_granularity() { return true; }
408+
static constexpr DWARFS_FORCE_INLINE bool compile_time_granularity() {
409+
return true;
410+
}
407411
};
408412

409413
class VariableGranularityPolicy : private GranularityPolicyBase {
@@ -471,7 +475,9 @@ class VariableGranularityPolicy : private GranularityPolicyBase {
471475
return granularity_;
472476
}
473477

474-
static DWARFS_FORCE_INLINE bool compile_time_granularity() { return false; }
478+
static constexpr DWARFS_FORCE_INLINE bool compile_time_granularity() {
479+
return false;
480+
}
475481

476482
private:
477483
uint_fast32_t const granularity_;
@@ -1183,10 +1189,12 @@ class active_block : private GranularityPolicy {
11831189

11841190
DWARFS_FORCE_INLINE mutable_byte_buffer data() const { return data_; }
11851191

1192+
// returns true if cached_hashval was used to avoid recomputation
11861193
template <bool UseSegmentQueue>
1187-
DWARFS_FORCE_INLINE void append_bytes(
1194+
DWARFS_FORCE_INLINE bool append_bytes(
11881195
granular_extent_adapter<GranularityPolicy, UseSegmentQueue>& data,
1189-
file_off_t offset, file_size_t size, bloom_filter& global_filter);
1196+
file_off_t offset, file_size_t size, bloom_filter& global_filter,
1197+
std::optional<rsync_hash::value_type> cached_hashval);
11901198

11911199
DWARFS_FORCE_INLINE size_t next_hash_distance_in_frames() const {
11921200
return window_step_mask_ + 1 - (size_in_frames() & window_step_mask_);
@@ -1341,10 +1349,13 @@ class segmenter_ final : public segmenter::impl, private SegmentingPolicy {
13411349
template <bool UseSegmentQueue>
13421350
DWARFS_FORCE_INLINE void
13431351
append_to_block(chunkable& chkable, extent_adapter_t<UseSegmentQueue>& data,
1344-
file_off_t offset_in_frames, file_size_t size_in_frames);
1352+
file_off_t offset_in_frames, file_size_t size_in_frames,
1353+
std::optional<rsync_hash::value_type> cached_hashval);
13451354
template <bool UseSegmentQueue>
1346-
void add_data(chunkable& chkable, extent_adapter_t<UseSegmentQueue>& data,
1347-
file_off_t offset_in_frames, file_size_t size_in_frames);
1355+
void
1356+
add_data(chunkable& chkable, extent_adapter_t<UseSegmentQueue>& data,
1357+
file_off_t offset_in_frames, file_size_t size_in_frames,
1358+
std::optional<rsync_hash::value_type> cached_hashval = std::nullopt);
13481359
template <bool UseSegmentQueue>
13491360
DWARFS_FORCE_INLINE void
13501361
segment_and_add_data(chunkable& chkable,
@@ -1483,27 +1494,74 @@ active_block<LoggerPolicy, GranularityPolicy>::is_existing_repeating_sequence(
14831494

14841495
template <typename LoggerPolicy, typename GranularityPolicy>
14851496
template <bool UseSegmentQueue>
1486-
DWARFS_FORCE_INLINE void
1497+
DWARFS_FORCE_INLINE bool
14871498
active_block<LoggerPolicy, GranularityPolicy>::append_bytes(
14881499
granular_extent_adapter<GranularityPolicy, UseSegmentQueue>& data,
1489-
file_off_t data_offset, file_size_t data_size,
1490-
bloom_filter& global_filter) {
1500+
file_off_t data_offset, file_size_t data_size, bloom_filter& global_filter,
1501+
std::optional<rsync_hash::value_type> cached_hashval) {
14911502
auto v = this->template create<granular_buffer_adapter<GranularityPolicy>>(
14921503
data_.raw_buffer());
14931504

14941505
// TODO: this works in theory, but slows down the segmenter by almost 10%
14951506
// auto v = this->template create<
14961507
// granular_byte_buffer_adapter<GranularityPolicy>>(data_);
14971508

1509+
auto const data_frames = bytes_to_frames(data_size);
14981510
auto offset = v.size();
14991511

1500-
DWARFS_CHECK(offset + bytes_to_frames(data_size) <= capacity_in_frames_,
1512+
DWARFS_CHECK(offset + data_frames <= capacity_in_frames_,
15011513
fmt::format("block capacity exceeded: {} + {} > {}", offset,
15021514
data_size, frames_to_bytes(capacity_in_frames_)));
15031515

15041516
v.append(data, data_offset, data_size);
15051517

1518+
auto insert_hashval = [&](size_t offset) {
1519+
auto const hashval = hasher_();
1520+
if (!is_existing_repeating_sequence(hashval, offset - window_size_))
1521+
[[likely]] {
1522+
offsets_.insert(hashval, offset - window_size_);
1523+
if (filter_.size() > 0) {
1524+
filter_.add(hashval);
1525+
}
1526+
global_filter.add(hashval);
1527+
}
1528+
};
1529+
15061530
if (window_size_ > 0) {
1531+
/*
1532+
* If a cached hash value is provided *and* we are already at least
1533+
* window_size_ frames into the block, we can skip the cyclic hash
1534+
* computation and reuse the cached hash value. `segment_and_add_data`
1535+
* guarantees that it will only set a cached hash value for a segment
1536+
* with `window_step` frames and aligned to a `window_step` offset.
1537+
* However, `add_data` could still split that segment if it would
1538+
* cross a block boundary, which is why we need additional checks.
1539+
*
1540+
* For a granularity that is a power of two, these checks can never
1541+
* fail, since the block size is always a multiple of the step size
1542+
* in bytes.
1543+
*/
1544+
1545+
auto is_complete_segment = [&] {
1546+
if constexpr (GranularityPolicy::compile_time_granularity()) {
1547+
if constexpr (std::has_single_bit(GranularityPolicy::kGranularity)) {
1548+
assert(std::cmp_equal(data_frames, window_step_mask_ + 1) &&
1549+
(offset & window_step_mask_) == 0);
1550+
return true;
1551+
}
1552+
}
1553+
return std::cmp_equal(data_frames, window_step_mask_ + 1) &&
1554+
(offset & window_step_mask_) == 0;
1555+
};
1556+
1557+
if (cached_hashval.has_value() && offset >= window_size_ &&
1558+
is_complete_segment()) {
1559+
offset += data_frames;
1560+
hasher_.set(*cached_hashval);
1561+
insert_hashval(offset);
1562+
return true;
1563+
}
1564+
15071565
while (offset < v.size()) {
15081566
if (offset < window_size_) [[unlikely]] {
15091567
v.update_hash(hasher_, offset);
@@ -1512,19 +1570,13 @@ active_block<LoggerPolicy, GranularityPolicy>::append_bytes(
15121570
}
15131571
if (++offset >= window_size_) [[likely]] {
15141572
if ((offset & window_step_mask_) == 0) [[unlikely]] {
1515-
auto hashval = hasher_();
1516-
if (!is_existing_repeating_sequence(hashval, offset - window_size_))
1517-
[[likely]] {
1518-
offsets_.insert(hashval, offset - window_size_);
1519-
if (filter_.size() > 0) {
1520-
filter_.add(hashval);
1521-
}
1522-
global_filter.add(hashval);
1523-
}
1573+
insert_hashval(offset);
15241574
}
15251575
}
15261576
}
15271577
}
1578+
1579+
return false;
15281580
}
15291581

15301582
template <typename LoggerPolicy, typename GranularityPolicy>
@@ -1655,6 +1707,15 @@ void segmenter_<LoggerPolicy, SegmentingPolicy>::finish() {
16551707
"avoided {} collisions in 0x{:02x}-byte sequences", v,
16561708
k);
16571709
}
1710+
1711+
if (stats_.total_appends > 0) {
1712+
LOG_VERBOSE << cfg_.context
1713+
<< fmt::format("appends using cached hashes: {}/{} ({:.3f}%)",
1714+
stats_.total_appends_with_cached_hash,
1715+
stats_.total_appends,
1716+
100.0 * stats_.total_appends_with_cached_hash /
1717+
stats_.total_appends);
1718+
}
16581719
}
16591720

16601721
template <typename LoggerPolicy, typename SegmentingPolicy>
@@ -1671,7 +1732,8 @@ template <bool UseSegmentQueue>
16711732
DWARFS_FORCE_INLINE void
16721733
segmenter_<LoggerPolicy, SegmentingPolicy>::append_to_block(
16731734
chunkable& chkable, extent_adapter_t<UseSegmentQueue>& data,
1674-
file_off_t offset_in_frames, file_size_t size_in_frames) {
1735+
file_off_t offset_in_frames, file_size_t size_in_frames,
1736+
std::optional<rsync_hash::value_type> cached_hashval) {
16751737
if (blocks_.empty() or blocks_.back().full()) [[unlikely]] {
16761738
if (blocks_.size() >= std::max<size_t>(1, cfg_.max_active_blocks)) {
16771739
blocks_.pop_front();
@@ -1702,7 +1764,13 @@ segmenter_<LoggerPolicy, SegmentingPolicy>::append_to_block(
17021764
<< frames_to_bytes(block.size_in_frames())
17031765
<< " from chunkable offset " << offset_in_bytes;
17041766

1705-
block.append_bytes(data, offset_in_bytes, size_in_bytes, global_filter_);
1767+
++stats_.total_appends;
1768+
1769+
if (block.append_bytes(data, offset_in_bytes, size_in_bytes, global_filter_,
1770+
cached_hashval)) {
1771+
++stats_.total_appends_with_cached_hash;
1772+
}
1773+
17061774
chunk_.size_in_frames += size_in_frames;
17071775

17081776
prog_.filesystem_size += size_in_bytes;
@@ -1718,7 +1786,8 @@ template <typename LoggerPolicy, typename SegmentingPolicy>
17181786
template <bool UseSegmentQueue>
17191787
void segmenter_<LoggerPolicy, SegmentingPolicy>::add_data(
17201788
chunkable& chkable, extent_adapter_t<UseSegmentQueue>& data,
1721-
file_off_t offset_in_frames, file_size_t size_in_frames) {
1789+
file_off_t offset_in_frames, file_size_t size_in_frames,
1790+
std::optional<rsync_hash::value_type> cached_hashval) {
17221791
while (size_in_frames > 0) {
17231792
file_off_t block_offset_in_frames = 0;
17241793

@@ -1729,10 +1798,13 @@ void segmenter_<LoggerPolicy, SegmentingPolicy>::add_data(
17291798
auto const chunk_size_in_frames = std::min<file_size_t>(
17301799
size_in_frames, block_size_in_frames_ - block_offset_in_frames);
17311800

1732-
append_to_block(chkable, data, offset_in_frames, chunk_size_in_frames);
1801+
append_to_block(chkable, data, offset_in_frames, chunk_size_in_frames,
1802+
cached_hashval);
17331803

17341804
offset_in_frames += chunk_size_in_frames;
17351805
size_in_frames -= chunk_size_in_frames;
1806+
1807+
cached_hashval.reset(); // only use for first append
17361808
}
17371809
}
17381810

@@ -1763,6 +1835,19 @@ segmenter_<LoggerPolicy, SegmentingPolicy>::segment_and_add_data(
17631835
lookback_size_in_frames +
17641836
(blocks_.empty() ? window_step_
17651837
: blocks_.back().next_hash_distance_in_frames());
1838+
/*
1839+
* The lagging hashvals vector is used to store the hash values every
1840+
* window_step_ frames that reach back up to lookback_size_in_frames.
1841+
* In most cases, the oldest of these lagging hash values can be used
1842+
* to avoid recomputing the cyclic hash when copying data to the current
1843+
* block.
1844+
*/
1845+
std::vector<rsync_hash::value_type> lagging_hashvals(lookback_size_in_frames /
1846+
window_step_);
1847+
size_t lagging_hashvals_index = 0;
1848+
size_t lagging_hashvals_count = 0;
1849+
1850+
assert(lookback_size_in_frames % window_step_ == 0);
17661851

17671852
DWARFS_CHECK(std::cmp_greater_equal(size_in_frames, window_size_),
17681853
"unexpected call to segment_and_add_data");
@@ -1790,8 +1875,10 @@ segmenter_<LoggerPolicy, SegmentingPolicy>::segment_and_add_data(
17901875
last_offset = offset;
17911876
};
17921877

1793-
auto add_frames = [&](file_size_t num) {
1794-
add_data(chkable, data, frames_written, num);
1878+
auto add_frames = [&](file_size_t num,
1879+
std::optional<rsync_hash::value_type> cached_hv =
1880+
std::nullopt) {
1881+
add_data(chkable, data, frames_written, num, cached_hv);
17951882
};
17961883

17971884
while (offset_in_frames < size_in_frames) {
@@ -1882,6 +1969,9 @@ segmenter_<LoggerPolicy, SegmentingPolicy>::segment_and_add_data(
18821969
next_hash_offset_in_frames =
18831970
frames_written + lookback_size_in_frames +
18841971
blocks_.back().next_hash_distance_in_frames();
1972+
1973+
// must reset lagging hashvals after a successful match
1974+
lagging_hashvals_count = 0;
18851975
}
18861976

18871977
matches.clear();
@@ -1899,11 +1989,26 @@ segmenter_<LoggerPolicy, SegmentingPolicy>::segment_and_add_data(
18991989
[[unlikely]] {
19001990
auto num_to_write =
19011991
offset_in_frames - lookback_size_in_frames - frames_written;
1902-
add_frames(num_to_write);
1992+
std::optional<rsync_hash::value_type> cached_hv;
1993+
1994+
// only if we have enough lagging hash values to cover the lookback size
1995+
// can we use the oldest one as a cached value
1996+
if (lagging_hashvals_count >= lagging_hashvals.size()) {
1997+
cached_hv = lagging_hashvals[lagging_hashvals_index];
1998+
}
1999+
2000+
add_frames(num_to_write, cached_hv);
19032001
frames_written += num_to_write;
19042002
next_hash_offset_in_frames += window_step_;
19052003

19062004
update_progress(offset_in_frames);
2005+
2006+
lagging_hashvals[lagging_hashvals_index] = hashval;
2007+
++lagging_hashvals_count;
2008+
2009+
if (++lagging_hashvals_index == lagging_hashvals.size()) {
2010+
lagging_hashvals_index = 0;
2011+
}
19072012
}
19082013

19092014
offset_in_frames = hashwin.slide(hasher, data, offset_in_frames);

0 commit comments

Comments
 (0)