PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
id_pair_slab_fold.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <array>
8#include <cstddef>
9#include <cstdint>
10#include <limits>
11#include <stdexcept>
12#include <string_view>
13#include <type_traits>
14
15namespace pineforge::detail {
16
17// The folded single-key slab already budgets 16*Q index bytes, Q >= 2*K.
18// Store just the record offset in each hash slot (the Header owns its full
19// hash), then use the remainder for sorted offsets and bucket transforms.
20// No change to ScratchSlab's allocation, capacities or data-record format.
22 struct Bucket {
23 std::uint64_t count = 0; // Includes dead rows, which fold as identity.
25 };
26 struct Workspace {
27 Bucket bucket;
29 std::array<unsigned char, 512> buffer{};
30 };
31 static_assert(sizeof(Bucket) == 2064, "slab bucket has one count and the exact seed map");
32 static_assert(std::is_trivially_copyable_v<Bucket>, "slab copies complete bucket representations");
33
34public:
35 void configure(std::uint64_t data_capacity, std::uint64_t keys,
36 std::uint64_t slots) noexcept {
37 width_ = data_capacity <= std::numeric_limits<std::uint32_t>::max() ? 4 : 8;
38 bucket_capacity_ = width_ == 4 ? 256 : 1024;
39 rows_at_ = width_ * slots;
40 buckets_at_ = rows_at_ + width_ * keys;
41 // Small manifests need no stored bucket map: root_ is their one map.
42 cached_buckets_ = keys > bucket_capacity_;
43 const auto minimum = bucket_capacity_ / 2;
44 bucket_limit_ = cached_buckets_ ? keys / minimum + (keys % minimum != 0) : 0;
45 reset();
46 }
47 void reset() noexcept {
48 root_.reset();
49 bucket_count_ = 0;
50 }
51 std::uint64_t apply(std::uint64_t seed) const noexcept { return root_.apply(seed); }
52 std::uint64_t storage_bytes() const noexcept {
53 return buckets_at_ + bucket_limit_ * sizeof(Bucket);
54 }
55 std::uint64_t offset_width() const noexcept { return width_; }
56 std::uint64_t bucket_capacity() const noexcept { return bucket_capacity_; }
57 std::uint64_t read_slot(const ScratchSlab& slab, ScratchResource region,
58 std::uint64_t at) const {
59 return read_offset(slab, region, width_ * at);
60 }
62 std::uint64_t at, std::uint64_t offset_plus_one) const {
63 write_offset(slab, region, width_ * at, offset_plus_one);
64 }
65
66 // count is the table's ever-key count BEFORE this mutation; append says
67 // the record is genuinely new. Dead records keep their sorted position.
68 // Admission of all data/key spans precedes this call. The entire index
69 // region is already readable, and every write below fits its fixed span.
70 template<class Reader>
71 void update(ScratchSlab& slab, ScratchResource region, std::uint64_t count,
72 bool append, std::string_view key, std::uint64_t row, Reader& reader) {
73 Workspace work;
74 std::uint64_t low = 0, high = count;
75 while (low < high) {
76 const auto middle = low + (high - low) / 2;
77 if (reader.compare(read_row(slab, region, middle), key) < 0) low = middle + 1;
78 else high = middle;
79 }
80 if (!append && (low == count || read_row(slab, region, low) != row))
81 throw std::logic_error("id slab fold missing sorted row");
82
83 // Locate a bucket using its count, without comparing keys again.
84 // An insertion at a boundary belongs to the following bucket.
85 std::uint64_t bucket = 0, start = 0;
86 if (cached_buckets_ && bucket_count_ != 0) {
87 for (;;) {
88 read_bucket(slab, region, bucket, work.bucket);
89 if (low < start + work.bucket.count || bucket + 1 == bucket_count_) break;
90 start += work.bucket.count;
91 ++bucket;
92 }
93 }
94 if (append) {
95 // Backward, bounded-buffer copies preserve all offsets on an
96 // overlapping shift. Existing buckets retain exactly their rows;
97 // only the receiving bucket gains a row.
98 const auto per_copy = work.buffer.size() / width_;
99 for (auto end = count; end > low;) {
100 const auto n = std::min<std::uint64_t>(per_copy, end - low);
101 const auto bytes = static_cast<std::size_t>(width_ * n);
102 slab.read_at(region, rows_at_ + width_ * (end - n), work.buffer.data(), bytes);
103 slab.write_at(region, rows_at_ + width_ * (end - n + 1), work.buffer.data(), bytes);
104 end -= n;
105 }
106 write_offset(slab, region, rows_at_ + width_ * low, row);
107 ++count;
108 ++work.bucket.count;
109 }
110 if (!cached_buckets_) {
111 work.bucket.count = count;
112 fold_bucket(slab, region, 0, work.bucket, reader);
113 root_ = work.bucket.fold;
114 return;
115 }
116 if (bucket_count_ == 0) bucket_count_ = 1;
117 if (work.bucket.count <= bucket_capacity_) {
118 fold_bucket(slab, region, start, work.bucket, reader);
119 write_bucket(slab, region, bucket, work.bucket);
120 } else {
121 const auto total = work.bucket.count; // Exactly capacity + 1.
122 // Make room for the right half, preserving every unchanged map.
123 for (auto at = bucket_count_; at > bucket + 1; --at) {
124 read_bucket(slab, region, at - 1, work.bucket);
125 write_bucket(slab, region, at, work.bucket);
126 }
127 ++bucket_count_;
128 work.bucket.count = total / 2;
129 fold_bucket(slab, region, start, work.bucket, reader);
130 write_bucket(slab, region, bucket, work.bucket);
131 start += work.bucket.count;
132 work.bucket.count = total - work.bucket.count;
133 fold_bucket(slab, region, start, work.bucket, reader);
134 write_bucket(slab, region, bucket + 1, work.bucket);
135 }
136 // The disk fold's exact concatenation algebra, in lexical order.
137 // Reuse cached maps; no unchanged bucket's bytes are folded here.
138 for (std::uint64_t at = 0; at < bucket_count_; ++at) {
139 read_bucket(slab, region, at, work.bucket);
140 reader.compose(work.next, work.bucket.fold);
141 }
142 root_ = work.next;
143 }
144 static constexpr std::size_t workspace_bytes() noexcept { return sizeof(Workspace); }
145
146private:
147 std::uint64_t read_offset(const ScratchSlab& slab, ScratchResource region,
148 std::uint64_t at) const {
149 if (width_ == 4) {
150 std::uint32_t value = 0;
151 slab.read_at(region, at, &value, sizeof(value));
152 return value;
153 }
154 std::uint64_t value = 0;
155 slab.read_at(region, at, &value, sizeof(value));
156 return value;
157 }
158 void write_offset(ScratchSlab& slab, ScratchResource region, std::uint64_t at,
159 std::uint64_t value) const {
160 if (width_ == 4) {
161 const auto narrow = static_cast<std::uint32_t>(value);
162 slab.write_at(region, at, &narrow, sizeof(narrow));
163 } else slab.write_at(region, at, &value, sizeof(value));
164 }
165 std::uint64_t read_row(const ScratchSlab& slab, ScratchResource region,
166 std::uint64_t at) const {
167 return read_offset(slab, region, rows_at_ + width_ * at);
168 }
169 void read_bucket(const ScratchSlab& slab, ScratchResource region,
170 std::uint64_t at, Bucket& bucket) const {
171 slab.read_at(region, buckets_at_ + at * sizeof(Bucket), &bucket, sizeof(bucket));
172 }
173 void write_bucket(ScratchSlab& slab, ScratchResource region,
174 std::uint64_t at, const Bucket& bucket) const {
175 slab.write_at(region, buckets_at_ + at * sizeof(Bucket), &bucket, sizeof(bucket));
176 }
177 template<class Reader>
178 void fold_bucket(const ScratchSlab& slab, ScratchResource region, std::uint64_t start,
179 Bucket& bucket, Reader& reader) const {
180 bucket.fold.reset();
181 for (std::uint64_t at = 0; at < bucket.count; ++at)
182 reader.append(read_row(slab, region, start + at), bucket.fold);
183 }
184
185 BrokerStateByteFold root_;
186 std::uint64_t width_ = 4, bucket_capacity_ = 256;
187 std::uint64_t rows_at_ = 0, buckets_at_ = 0;
188 std::uint64_t bucket_count_ = 0, bucket_limit_ = 0;
189 bool cached_buckets_ = false;
190};
192 "slab update fits the existing disk fold workspace allowance");
193
194} // namespace pineforge::detail
static constexpr std::size_t workspace_bytes() noexcept
std::uint64_t storage_bytes() const noexcept
std::uint64_t apply(std::uint64_t seed) const noexcept
static constexpr std::size_t workspace_bytes() noexcept
void write_slot(ScratchSlab &slab, ScratchResource region, std::uint64_t at, std::uint64_t offset_plus_one) const
void configure(std::uint64_t data_capacity, std::uint64_t keys, std::uint64_t slots) noexcept
std::uint64_t read_slot(const ScratchSlab &slab, ScratchResource region, std::uint64_t at) const
std::uint64_t offset_width() const noexcept
std::uint64_t bucket_capacity() const noexcept
void update(ScratchSlab &slab, ScratchResource region, std::uint64_t count, bool append, std::string_view key, std::uint64_t row, Reader &reader)
void read_at(ScratchResource resource, std::uint64_t offset, void *destination, std::size_t length) const
void write_at(ScratchResource resource, std::uint64_t offset, const void *source, std::size_t length)