dasmig-core 1.2.0
Shared random engine, distributions and data tiers for the dasmig generator libraries
Loading...
Searching...
No Matches
batch.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_CORE_BATCH_HPP
5#define DASMIG_CORE_BATCH_HPP
6
7#include "rng.hpp"
8
9#include <algorithm>
10#include <concepts>
11#include <cstddef>
12#include <cstdint>
13#include <exception>
14#include <functional>
15#include <limits>
16#include <optional>
17#include <stdexcept>
18#include <string>
19#include <system_error>
20#include <thread>
21#include <type_traits>
22#include <typeindex>
23#include <typeinfo>
24#include <unordered_set>
25#include <utility>
26#include <vector>
27
28/// @file batch.hpp
29/// @brief Batch generation: item seeds, parallel and streaming runs, and
30/// uniqueness, shared by every generator library.
31/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
32///
33/// A batch is `count` results generated from one batch seed. Item `i` is
34/// exactly what the single-call getter returns for `item_seed(batch_seed, i)`,
35/// so any item replays on its own, any range is computed directly, and the
36/// result does not depend on how the work is divided between threads.
37
38namespace dasmig::core
39{
40
41/// @brief The seed of item @p index in a batch.
42///
43/// `rng{batch_seed}.split(index).seed()`, computed without building an engine.
44///
45/// @param batch_seed Seed of the whole batch.
46/// @param index Index of the item (including `batch_options::first`).
47/// @return The seed to give the single-call getter for that item.
48[[nodiscard]] constexpr std::uint64_t item_seed(std::uint64_t batch_seed,
49 std::uint64_t index) noexcept
50{
51 return rng::child_seed_of(batch_seed, index);
52}
53
54/// @brief The seed of attempt @p attempt of item @p index, for a batch that
55/// must be unique.
56///
57/// Attempt 0 is the plain item_seed(), so items that do not collide equal the
58/// single call. Attempt `k > 0` is `item_seed(item_seed(batch_seed, index), k)`.
59///
60/// @param batch_seed Seed of the whole batch.
61/// @param index Index of the item.
62/// @param attempt Attempt number, starting at 0.
63/// @return The seed to generate that attempt with.
64[[nodiscard]] constexpr std::uint64_t attempt_seed(std::uint64_t batch_seed,
65 std::uint64_t index,
66 std::uint64_t attempt) noexcept
67{
68 const std::uint64_t base = item_seed(batch_seed, index);
69 return attempt == 0 ? base : item_seed(base, attempt);
70}
71
72/// @brief Makes a batch unique by a key (an email, a username, ...).
73///
74/// Items are generated, then checked in index order; an item whose key was
75/// already produced earlier in the same call is generated again with the next
76/// attempt seed (see attempt_seed()) until it is new or the attempts run out.
77/// The outcome is the same for any thread count. Uniqueness holds within one
78/// call: two batches that are concatenated are not checked against each other.
80{
81 public:
82 /// @brief Unique by @p key.
83 /// @tparam T The item type the batch produces.
84 /// @tparam Key Callable taking `const T&` and returning something
85 /// convertible to `std::string`.
86 /// @param key Extracts the key.
87 /// @param max_attempts Attempts per item, including the first (at least 1).
88 /// @return The rule.
89 template <typename T, typename Key>
90 [[nodiscard]] static unique_rule by(Key key, std::size_t max_attempts = 100)
91 {
92 unique_rule rule;
93 rule._type = std::type_index{typeid(T)};
94 rule._max_attempts = max_attempts;
95 rule._key = [key = std::move(key)](const void* item)
96 { return std::string{key(*static_cast<const T*>(item))}; };
97 return rule;
98 }
99
100 /// @brief Attempts per item, including the first.
101 /// @return The limit.
102 [[nodiscard]] std::size_t max_attempts() const noexcept { return _max_attempts; }
103
104 /// @brief The item type the rule was made for.
105 /// @return Its type index.
106 [[nodiscard]] std::type_index type() const noexcept { return _type; }
107
108 /// @brief The key of an item.
109 /// @param item Pointer to an item of the rule's type.
110 /// @return The key.
111 [[nodiscard]] std::string key_of(const void* item) const { return _key(item); }
112
113 private:
114 unique_rule() = default;
115
116 std::type_index _type{typeid(void)};
117 std::size_t _max_attempts{1};
118 std::function<std::string(const void*)> _key;
119};
120
121/// @brief How a batch is generated. Requests keep describing one item.
123{
124 std::uint64_t count{1}; ///< Number of items.
125 std::uint64_t first{0}; ///< Index of the first item; item `i` is item_seed(seed, first + i).
126 std::size_t threads{1}; ///< Worker threads; 0 means the hardware count, 1 runs on the caller.
127 std::optional<unique_rule> unique{}; // NOLINT(readability-redundant-member-init): keeps -Wextra quiet for partial designated initializers
128 ///< Make the batch unique by a key.
129};
130
131namespace detail
132{
133
134template <typename Make>
135using batch_item_t = std::remove_cvref_t<std::invoke_result_t<const Make&, std::uint64_t>>;
136
137// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
138[[nodiscard]] inline std::size_t resolve_threads(std::size_t requested, std::size_t work) noexcept
139{
140#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
141 (void)requested;
142 (void)work;
143 return 1;
144#else
145 std::size_t threads = requested;
146 if (threads == 0)
147 {
148 threads = std::max<std::size_t>(1, std::thread::hardware_concurrency());
149 }
150 return std::max<std::size_t>(1, std::min(threads, work));
151#endif
152}
153
154// Fills slots[0, n) with make(item_seed(batch_seed, first + k)), in contiguous
155// chunks, one per thread. The failure of the lowest index is rethrown.
156template <typename Make, typename Item>
157void fill_window(const Make& make, std::uint64_t batch_seed, std::uint64_t first,
158 std::size_t n, std::size_t threads, std::vector<std::optional<Item>>& slots)
159{
160 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
161 const auto run_chunk = [&](std::size_t lo, std::size_t hi, std::exception_ptr& error)
162 {
163 try
164 {
165 for (std::size_t k = lo; k < hi; ++k)
166 {
167 slots[k].emplace(make(item_seed(batch_seed, first + k)));
168 }
169 }
170 catch (...)
171 {
172 error = std::current_exception();
173 }
174 };
175
176 if (threads <= 1)
177 {
178 std::exception_ptr error;
179 run_chunk(0, n, error);
180 if (error) std::rethrow_exception(error);
181 return;
182 }
183
184 std::vector<std::exception_ptr> errors(threads);
185 const auto bound = [&](std::size_t chunk) { return n * chunk / threads; };
186 std::vector<std::thread> workers;
187 workers.reserve(threads - 1);
188 for (std::size_t c = 1; c < threads; ++c)
189 {
190 try
191 {
192 workers.emplace_back(run_chunk, bound(c), bound(c + 1), std::ref(errors[c]));
193 }
194 catch (const std::system_error&)
195 {
196 // No thread available: the caller does this chunk after its own.
197 run_chunk(bound(c), bound(c + 1), errors[c]);
198 }
199 }
200 run_chunk(bound(0), bound(1), errors[0]);
201 for (auto& worker : workers) worker.join();
202 for (const auto& error : errors)
203 {
204 if (error) std::rethrow_exception(error);
205 }
206}
207
208template <typename Make, typename Sink>
209void run_batch_windows(const batch_options& options, std::uint64_t batch_seed,
210 const Make& make, Sink& sink, std::uint64_t window_items)
211{
212 using item = batch_item_t<Make>;
213
214 if (options.count > std::numeric_limits<std::uint64_t>::max() - options.first)
215 {
216 throw std::invalid_argument("batch: first + count overflows the item index");
217 }
218 if (options.unique)
219 {
220 if (options.unique->type() != std::type_index{typeid(item)})
221 {
222 throw std::invalid_argument("batch: the unique rule was made for another item type");
223 }
224 if (options.unique->max_attempts() == 0)
225 {
226 throw std::invalid_argument("batch: a unique rule needs at least one attempt");
227 }
228 }
229
230 const std::size_t threads = resolve_threads(
231 options.threads,
232 static_cast<std::size_t>(std::min<std::uint64_t>(options.count, std::numeric_limits<std::size_t>::max())));
233 std::unordered_set<std::string> seen;
234 std::vector<std::optional<item>> slots;
235
236 for (std::uint64_t done = 0; done < options.count;)
237 {
238 const auto n = static_cast<std::size_t>(std::min(window_items, options.count - done));
239 const std::uint64_t window_first = options.first + done;
240 slots.clear();
241 slots.resize(n);
242 fill_window(make, batch_seed, window_first, n, threads, slots);
243
244 for (std::size_t k = 0; k < n; ++k)
245 {
246 const std::uint64_t index = window_first + k;
247 if (options.unique)
248 {
249 const unique_rule& rule = *options.unique;
250 std::string key = rule.key_of(&*slots[k]);
251 for (std::uint64_t attempt = 1; !seen.insert(key).second; ++attempt)
252 {
253 if (attempt >= rule.max_attempts())
254 {
255 throw std::runtime_error(
256 "batch: item " + std::to_string(index) + " has no unique key after " +
257 std::to_string(rule.max_attempts()) + " attempts");
258 }
259 slots[k].emplace(make(attempt_seed(batch_seed, index, attempt)));
260 key = rule.key_of(&*slots[k]);
261 }
262 }
263 sink(index, *slots[k]);
264 }
265 done += n;
266 }
267}
268
269} // namespace detail
270
271/// @brief Generate a batch and return it.
272///
273/// Item `i` is `make(item_seed(batch_seed, options.first + i))`. @p make runs
274/// on several threads when `options.threads` allows, so it must be callable
275/// concurrently (the libraries' const seeded getters are).
276///
277/// @code
278/// auto names = dasmig::core::run_batch({.count = 1000, .threads = 4}, 7,
279/// [&](std::uint64_t seed) { return gen.get_name(request, seed); });
280/// @endcode
281///
282/// @tparam Make Callable taking a `std::uint64_t` seed and returning an item.
283/// @param options Count, first index, threads and uniqueness.
284/// @param batch_seed Seed of the whole batch.
285/// @param make Generates one item from its seed.
286/// @return The items, in index order.
287/// @throws std::invalid_argument If the options are inconsistent.
288/// @throws std::runtime_error If a unique batch runs out of attempts.
289/// @throws Whatever @p make throws, for the lowest failing index.
290template <typename Make>
291 requires std::invocable<const Make&, std::uint64_t>
292[[nodiscard]] std::vector<detail::batch_item_t<Make>> run_batch(const batch_options& options,
293 std::uint64_t batch_seed,
294 const Make& make)
295{
296 using item = detail::batch_item_t<Make>;
297 std::vector<item> out;
298 out.reserve(static_cast<std::size_t>(options.count));
299 auto sink = [&out](std::uint64_t, item& value) { out.push_back(std::move(value)); };
300 detail::run_batch_windows(options, batch_seed, make, sink,
301 std::max<std::uint64_t>(options.count, 1));
302 return out;
303}
304
305/// @brief Generate a batch and pass each item to @p sink, in index order,
306/// without holding the whole batch.
307///
308/// At most a few thousand items per thread are alive at once (plus the keys of
309/// a unique batch). @p sink runs on the calling thread and receives the
310/// item's index (including `options.first`) and the item.
311///
312/// @tparam Make Callable taking a `std::uint64_t` seed and returning an item.
313/// @tparam Sink Callable taking `(std::uint64_t index, Item&)`.
314/// @param options Count, first index, threads and uniqueness.
315/// @param batch_seed Seed of the whole batch.
316/// @param make Generates one item from its seed.
317/// @param sink Receives the items.
318/// @throws The same errors as run_batch(), and whatever @p sink throws.
319template <typename Make, typename Sink>
320 requires std::invocable<const Make&, std::uint64_t>
321void for_each_batch(const batch_options& options, std::uint64_t batch_seed, const Make& make,
322 Sink&& sink) // NOLINT(cppcoreguidelines-missing-std-forward): called as an lvalue, never moved
323{
324 const std::size_t threads = detail::resolve_threads(options.threads, std::numeric_limits<std::size_t>::max());
325 detail::run_batch_windows(options, batch_seed, make, sink,
326 static_cast<std::uint64_t>(threads) * 4096U);
327}
328
329} // namespace dasmig::core
330
331#endif // DASMIG_CORE_BATCH_HPP
void for_each_batch(const batch_options &options, std::uint64_t batch_seed, const Make &make, Sink &&sink)
Generate a batch and pass each item to sink, in index order, without holding the whole batch.
Definition batch.hpp:321
constexpr std::uint64_t attempt_seed(std::uint64_t batch_seed, std::uint64_t index, std::uint64_t attempt) noexcept
The seed of attempt attempt of item index, for a batch that must be unique.
Definition batch.hpp:64
std::vector< detail::batch_item_t< Make > > run_batch(const batch_options &options, std::uint64_t batch_seed, const Make &make)
Generate a batch and return it.
Definition batch.hpp:292
constexpr std::uint64_t item_seed(std::uint64_t batch_seed, std::uint64_t index) noexcept
The seed of item index in a batch.
Definition batch.hpp:48
static constexpr std::uint64_t child_seed_of(std::uint64_t seed, std::uint64_t index) noexcept
child_seed(index) for a seed, without building an engine.
Definition rng.hpp:204
Makes a batch unique by a key (an email, a username, ...).
Definition batch.hpp:80
static unique_rule by(Key key, std::size_t max_attempts=100)
Unique by key.
Definition batch.hpp:90
std::type_index type() const noexcept
The item type the rule was made for.
Definition batch.hpp:106
std::string key_of(const void *item) const
The key of an item.
Definition batch.hpp:111
std::size_t max_attempts() const noexcept
Attempts per item, including the first.
Definition batch.hpp:102
The random engine every generator library uses: xoshiro256** seeded through SplitMix64,...
How a batch is generated. Requests keep describing one item.
Definition batch.hpp:123
std::size_t threads
Worker threads; 0 means the hardware count, 1 runs on the caller.
Definition batch.hpp:126
std::uint64_t count
Number of items.
Definition batch.hpp:124
std::uint64_t first
Index of the first item; item i is item_seed(seed, first + i).
Definition batch.hpp:125