Line data Source code
1 0 : // Distributed under the MIT License.
2 : // See LICENSE.txt for details.
3 :
4 : #pragma once
5 :
6 : #include <array>
7 : #include <cstddef>
8 : #include <utility>
9 :
10 : #include "Utilities/ErrorHandling/Assert.hpp"
11 : #include "Utilities/Gsl.hpp"
12 : #include "Utilities/Requires.hpp"
13 : #include "Utilities/TMPL.hpp"
14 : #include "Utilities/TypeTraits/IsInteger.hpp"
15 :
16 : /// \ingroup UtilitiesGroup
17 : /// Range of integral values for StaticCache indices. The `Start` is inclusive
18 : /// and the `End` is exclusive. The range must not be empty.
19 : template <auto Start, auto End>
20 1 : struct CacheRange {
21 : static_assert(std::is_same_v<decltype(Start), decltype(End)>);
22 : static_assert(Start < End, "CacheRange must include at least one value");
23 0 : constexpr static auto start = Start;
24 0 : constexpr static auto end = End;
25 0 : constexpr static auto size = end - start;
26 0 : using value_type = std::remove_cv_t<decltype(start)>;
27 : };
28 :
29 : /// \ingroup UtilitiesGroup
30 : /// Possible enumeration values for the StaticCache. Only values specified here
31 : /// are retrievable.
32 : ///
33 : /// \note The `EnumerationType` must be streamable.
34 : template <typename EnumerationType, EnumerationType... Enums>
35 1 : struct CacheEnumeration {
36 0 : constexpr static size_t size = sizeof...(Enums);
37 0 : using value_type = EnumerationType;
38 0 : static constexpr std::array<value_type, size> values{Enums...};
39 0 : using value_list = tmpl::integral_list<EnumerationType, Enums...>;
40 : };
41 :
42 : /// \ingroup UtilitiesGroup
43 : /// A cache of objects intended to be stored in a static variable.
44 : ///
45 : /// Objects can be accessed via a combination of several `size_t` and `enum`
46 : /// arguments. The range of each integral argument is specified via a template
47 : /// parameter of type `CacheRange<start, end>`, giving the first and
48 : /// one-past-last values for the range. Each `enum` argument is specified by a
49 : /// template parameter of type `CacheEnumeration<EnumerationType, Members...>`
50 : /// giving the enumeration type and an explicit set of every enum member to be
51 : /// cached.
52 : ///
53 : /// \example
54 : /// A cache with only numeric indices:
55 : /// \snippet Test_StaticCache.cpp static_cache
56 : ///
57 : /// \example
58 : /// A cache with enumeration indices:
59 : /// \snippet Test_StaticCache.cpp static_cache_with_enum
60 : ///
61 : /// \example
62 : /// A cache with mixed numeric and enumeration indices:
63 : /// \snippet Test_StaticCache.cpp static_cache_with_enum_and_numeric
64 : ///
65 : /// \example
66 : /// A cache with no arguments at all (caching only a single object)
67 : /// \snippet Test_StaticCache.cpp static_cache_no_args
68 : ///
69 : /// \see make_static_cache
70 : ///
71 : /// \tparam T type held in the cache
72 : /// \tparam Ranges ranges of valid indices
73 : template <typename Generator, typename T, typename... Ranges>
74 1 : class StaticCache {
75 : public:
76 : template <typename Gen>
77 : // NOLINTNEXTLINE(bugprone-forwarding-reference-overload)
78 0 : explicit StaticCache(Gen&& generator)
79 : : generator_{std::forward<Gen>(generator)} {}
80 :
81 : template <typename... Args>
82 0 : const T& operator()(const Args... parameters) const {
83 : static_assert(sizeof...(parameters) == sizeof...(Ranges),
84 : "Number of arguments must match number of ranges.");
85 : return unwrap_cache_combined(generate_tuple<Ranges>(parameters)...);
86 : }
87 :
88 : private:
89 : template <typename Range, typename T1>
90 0 : auto generate_tuple(const T1 parameter) const {
91 : if constexpr (std::is_enum<T1>::value) {
92 : static_assert(
93 : std::is_same<typename Range::value_type, std::remove_cv_t<T1>>::value,
94 : "Mismatched enum parameter type and cached type.");
95 : size_t array_location = std::numeric_limits<size_t>::max();
96 : static const std::array<typename Range::value_type, Range::size> values{
97 : Range::values};
98 : for (size_t i = 0; i < Range::size; ++i) {
99 : if (parameter == gsl::at(values, i)) {
100 : array_location = i;
101 : break;
102 : }
103 : }
104 : if (UNLIKELY(array_location == std::numeric_limits<size_t>::max())) {
105 : ERROR("Uncached enumeration value: " << parameter);
106 : }
107 : return std::tuple{array_location, typename Range::value_list{}};
108 : } else {
109 : static_assert(
110 : tt::is_integer_v<std::remove_cv_t<T1>>,
111 : "The parameter passed for a CacheRange must be an integer type.");
112 :
113 : // Check range here because the nested range checks in the unwrap_cache
114 : // function cause significant compile time overhead.
115 : if (UNLIKELY(Range::start >
116 : static_cast<decltype(Range::start)>(parameter) or
117 : static_cast<decltype(Range::start)>(parameter) >=
118 : Range::start +
119 : static_cast<decltype(Range::start)>(Range::size))) {
120 : ERROR("Index out of range: "
121 : << Range::start << " <= " << parameter << " < "
122 : << Range::start +
123 : static_cast<decltype(Range::start)>(Range::size));
124 : }
125 : return std::tuple{
126 : // unsigned cast is safe since this is an index into an array
127 : static_cast<size_t>(
128 : static_cast<typename Range::value_type>(parameter) -
129 : Range::start),
130 : tmpl::make_sequence<
131 : tmpl::integral_constant<typename Range::value_type, Range::start>,
132 : Range::size>{}};
133 : }
134 : }
135 :
136 : // Compilation time notes:
137 : //
138 : // - The separate peeling of different number of arguments is the
139 : // fastest implementation Nils Deppe has found so far.
140 : // - The second fastest is using a Cartesian product on the lists of
141 : // possible values, followed by a for_each over that list to set the
142 : // function pointers in the array. Note that having the for_each function
143 : // be marked `constexpr` resulted in a 6x reduction in compilation time
144 : // for clang 17 compared to the not constexpr version, but still 40%
145 : // slower compilation compared to the pattern matching below.
146 : template <typename... IntegralConstantValues>
147 0 : const T& unwrap_cache_combined() const {
148 : static const T cached_object = generator_(IntegralConstantValues::value...);
149 : return cached_object;
150 : }
151 :
152 : // NVCC v12.6 can't handle the unwrapping code below, so we use a simpler
153 : // version for CUDA. Note that we don't actually need to use StaticCache on
154 : // device at all, so the main issue with NVCC is that it tries to compile this
155 : // code for CUDA at all.
156 : #if defined(__NVCC__) && defined(__CUDA_ARCH__)
157 : template <typename... IntegralConstantValues, typename... IntegralConstants,
158 : typename... Args>
159 : const T& unwrap_cache_combined(
160 : std::tuple<size_t, tmpl::list<IntegralConstants...>> parameter0,
161 : Args... parameters) const {
162 : // note that the act of assigning to the specified function pointer type
163 : // fixes the template arguments that need to be inferred.
164 : static const std::array<
165 : const T& (StaticCache<Generator, T, Ranges...>::*)(Args...) const,
166 : sizeof...(IntegralConstants)>
167 : cache{{&StaticCache<Generator, T, Ranges...>::unwrap_cache_combined<
168 : IntegralConstantValues..., IntegralConstants>...}};
169 : // The array `cache` holds pointers to member functions, so we dereference
170 : // the pointer and invoke it on `this`.
171 : return (this->*gsl::at(cache, std::get<0>(parameter0)))(parameters...);
172 : }
173 : #else // defined(__NVCC__) && defined(__CUDA_ARCH__)
174 : template <typename... IntegralConstantValues, typename... IntegralConstants>
175 0 : const T& unwrap_cache_combined(
176 : std::tuple<size_t, tmpl::list<IntegralConstants...>> parameter0) const {
177 : // note that the act of assigning to the specified function pointer type
178 : // fixes the template arguments that need to be inferred.
179 : static const std::array<const T& (StaticCache::*)() const,
180 : sizeof...(IntegralConstants)>
181 : cache{{&StaticCache::unwrap_cache_combined<IntegralConstantValues...,
182 : IntegralConstants>...}};
183 : // The array `cache` holds pointers to member functions, so we dereference
184 : // the pointer and invoke it on `this`.
185 : return (this->*gsl::at(cache, std::get<0>(parameter0)))();
186 : }
187 :
188 : template <typename... IntegralConstantValues, typename... IntegralConstants0,
189 : typename... IntegralConstants1>
190 0 : const T& unwrap_cache_combined(
191 : std::tuple<size_t, tmpl::list<IntegralConstants0...>> parameter0,
192 : std::tuple<size_t, tmpl::list<IntegralConstants1...>> parameter1) const {
193 : constexpr size_t num0 = sizeof...(IntegralConstants0);
194 : constexpr size_t num1 = sizeof...(IntegralConstants1);
195 : constexpr size_t total_size = num0 * num1;
196 : // note that the act of assigning to the specified function pointer type
197 : // fixes the template arguments that need to be inferred.
198 : static const std::array<const T& (StaticCache::*)() const, total_size>
199 : cache = []() {
200 : std::array<const T& (StaticCache::*)() const, total_size> result;
201 : size_t counter1 = 0;
202 : const auto helper1 = [&counter1,
203 : &result]<typename IntegralConstant1>() {
204 : size_t counter0 = 0;
205 : const auto helper0 = [&counter0, &counter1,
206 : &result]<typename IntegralConstant0>() {
207 : result[counter0 + num0 * counter1] =
208 : &StaticCache::unwrap_cache_combined<IntegralConstantValues...,
209 : IntegralConstant0,
210 : IntegralConstant1>;
211 : ++counter0;
212 : };
213 : EXPAND_PACK_LEFT_TO_RIGHT(
214 : helper0.template operator()<IntegralConstants0>());
215 : ++counter1;
216 : };
217 : EXPAND_PACK_LEFT_TO_RIGHT(
218 : helper1.template operator()<IntegralConstants1>());
219 : return result;
220 : }();
221 :
222 : // The array `cache` holds pointers to member functions, so we dereference
223 : // the pointer and invoke it on `this`.
224 : return (this->*gsl::at(cache, std::get<0>(parameter0) +
225 : num0 * std::get<0>(parameter1)))();
226 : }
227 :
228 : template <typename... IntegralConstantValues, typename... IntegralConstants0,
229 : typename... IntegralConstants1, typename... IntegralConstants2>
230 0 : const T& unwrap_cache_combined(
231 : std::tuple<size_t, tmpl::list<IntegralConstants0...>> parameter0,
232 : std::tuple<size_t, tmpl::list<IntegralConstants1...>> parameter1,
233 : std::tuple<size_t, tmpl::list<IntegralConstants2...>> parameter2) const {
234 : constexpr size_t num0 = sizeof...(IntegralConstants0);
235 : constexpr size_t num1 = sizeof...(IntegralConstants1);
236 : constexpr size_t num2 = sizeof...(IntegralConstants2);
237 : constexpr size_t total_size = num0 * num1 * num2;
238 : // note that the act of assigning to the specified function pointer type
239 : // fixes the template arguments that need to be inferred.
240 : static const std::array<const T& (StaticCache::*)() const, total_size>
241 : cache = []() {
242 : std::array<const T& (StaticCache::*)() const, total_size> result;
243 : size_t counter2 = 0;
244 : const auto helper2 = [&counter2,
245 : &result]<typename IntegralConstant2>() {
246 : size_t counter1 = 0;
247 : const auto helper1 = [&counter1, &counter2,
248 : &result]<typename IntegralConstant1>() {
249 : size_t counter0 = 0;
250 : const auto helper0 = [&counter0, &counter1, &counter2,
251 : &result]<typename IntegralConstant0>() {
252 : result[counter0 + num0 * (counter1 + num1 * counter2)] =
253 : &StaticCache::unwrap_cache_combined<
254 : IntegralConstantValues..., IntegralConstant0,
255 : IntegralConstant1, IntegralConstant2>;
256 : ++counter0;
257 : };
258 : EXPAND_PACK_LEFT_TO_RIGHT(
259 : helper0.template operator()<IntegralConstants0>());
260 : ++counter1;
261 : };
262 : EXPAND_PACK_LEFT_TO_RIGHT(
263 : helper1.template operator()<IntegralConstants1>());
264 : ++counter2;
265 : };
266 : EXPAND_PACK_LEFT_TO_RIGHT(
267 : helper2.template operator()<IntegralConstants2>());
268 : return result;
269 : }();
270 :
271 : // The array `cache` holds pointers to member functions, so we dereference
272 : // the pointer and invoke it on `this`.
273 : return (
274 : this->*gsl::at(cache, std::get<0>(parameter0) +
275 : num0 * (std::get<0>(parameter1) +
276 : num1 * std::get<0>(parameter2))))();
277 : }
278 :
279 : template <typename... IntegralConstantValues, typename... IntegralConstants0,
280 : typename... IntegralConstants1, typename... IntegralConstants2,
281 : typename... IntegralConstants3>
282 0 : const T& unwrap_cache_combined(
283 : std::tuple<size_t, tmpl::list<IntegralConstants0...>> parameter0,
284 : std::tuple<size_t, tmpl::list<IntegralConstants1...>> parameter1,
285 : std::tuple<size_t, tmpl::list<IntegralConstants2...>> parameter2,
286 : std::tuple<size_t, tmpl::list<IntegralConstants3...>> parameter3) const {
287 : constexpr size_t num0 = sizeof...(IntegralConstants0);
288 : constexpr size_t num1 = sizeof...(IntegralConstants1);
289 : constexpr size_t num2 = sizeof...(IntegralConstants2);
290 : constexpr size_t num3 = sizeof...(IntegralConstants3);
291 : constexpr size_t total_size = num0 * num1 * num2 * num3;
292 : // note that the act of assigning to the specified function pointer type
293 : // fixes the template arguments that need to be inferred.
294 : static const std::array<const T& (StaticCache::*)() const, total_size>
295 : cache = []() {
296 : std::array<const T& (StaticCache::*)() const, total_size> result;
297 : size_t counter3 = 0;
298 : const auto helper3 = [&counter3,
299 : &result]<typename IntegralConstant3>() {
300 : size_t counter2 = 0;
301 : const auto helper2 = [&counter2, &counter3,
302 : &result]<typename IntegralConstant2>() {
303 : size_t counter1 = 0;
304 : const auto helper1 = [&counter1, &counter2, &counter3,
305 : &result]<typename IntegralConstant1>() {
306 : size_t counter0 = 0;
307 : const auto helper0 = [&counter0, &counter1, &counter2,
308 : &counter3,
309 : &result]<typename IntegralConstant0>() {
310 : result[counter0 +
311 : num0 *
312 : (counter1 + num1 * (counter2 + num2 * counter3))] =
313 : &StaticCache::unwrap_cache_combined<
314 : IntegralConstantValues..., IntegralConstant0,
315 : IntegralConstant1, IntegralConstant2,
316 : IntegralConstant3>;
317 : ++counter0;
318 : };
319 : EXPAND_PACK_LEFT_TO_RIGHT(
320 : helper0.template operator()<IntegralConstants0>());
321 : ++counter1;
322 : };
323 : EXPAND_PACK_LEFT_TO_RIGHT(
324 : helper1.template operator()<IntegralConstants1>());
325 : ++counter2;
326 : };
327 : EXPAND_PACK_LEFT_TO_RIGHT(
328 : helper2.template operator()<IntegralConstants2>());
329 : ++counter3;
330 : };
331 : EXPAND_PACK_LEFT_TO_RIGHT(
332 : helper3.template operator()<IntegralConstants3>());
333 : return result;
334 : }();
335 :
336 : // The array `cache` holds pointers to member functions, so we
337 : // dereference the pointer and invoke it on `this`.
338 : return (
339 : this->*gsl::at(cache,
340 : std::get<0>(parameter0) +
341 : num0 * (std::get<0>(parameter1) +
342 : num1 * (std::get<0>(parameter2) +
343 : num2 * std::get<0>(parameter3)))))();
344 : }
345 :
346 : template <typename... IntegralConstantValues, typename... IntegralConstants0,
347 : typename... IntegralConstants1, typename... IntegralConstants2,
348 : typename... IntegralConstants3, typename... IntegralConstants4,
349 : typename... Args>
350 0 : const T& unwrap_cache_combined(
351 : std::tuple<size_t, tmpl::list<IntegralConstants0...>> parameter0,
352 : std::tuple<size_t, tmpl::list<IntegralConstants1...>> parameter1,
353 : std::tuple<size_t, tmpl::list<IntegralConstants2...>> parameter2,
354 : std::tuple<size_t, tmpl::list<IntegralConstants3...>> parameter3,
355 : std::tuple<size_t, tmpl::list<IntegralConstants4...>> parameter4,
356 : const Args&... parameters) const {
357 : constexpr size_t num0 = sizeof...(IntegralConstants0);
358 : constexpr size_t num1 = sizeof...(IntegralConstants1);
359 : constexpr size_t num2 = sizeof...(IntegralConstants2);
360 : constexpr size_t num3 = sizeof...(IntegralConstants3);
361 : constexpr size_t num4 = sizeof...(IntegralConstants4);
362 : constexpr size_t total_size = num0 * num1 * num2 * num3 * num4;
363 : // note that the act of assigning to the specified function pointer type
364 : // fixes the template arguments that need to be inferred.
365 : static const std::array<const T& (StaticCache::*)(Args...) const,
366 : total_size>
367 : cache = []() {
368 : std::array<const T& (StaticCache::*)(Args...) const, total_size>
369 : result;
370 : size_t counter4 = 0;
371 : const auto helper4 = [&counter4,
372 : &result]<typename IntegralConstant4>() {
373 : size_t counter3 = 0;
374 : const auto helper3 = [&counter3, &counter4,
375 : &result]<typename IntegralConstant3>() {
376 : size_t counter2 = 0;
377 : const auto helper2 = [&counter2, &counter3, &counter4,
378 : &result]<typename IntegralConstant2>() {
379 : size_t counter1 = 0;
380 : const auto helper1 = [&counter1, &counter2, &counter3,
381 : &counter4,
382 : &result]<typename IntegralConstant1>() {
383 : size_t counter0 = 0;
384 : const auto helper0 = [&counter0, &counter1, &counter2,
385 : &counter3, &counter4,
386 : &result]<typename IntegralConstant0>() {
387 : result[counter0 +
388 : num0 *
389 : (counter1 +
390 : num1 * (counter2 +
391 : num2 * (counter3 + num3 * counter4)))] =
392 : &StaticCache::unwrap_cache_combined<
393 : IntegralConstantValues..., IntegralConstant0,
394 : IntegralConstant1, IntegralConstant2,
395 : IntegralConstant3, IntegralConstant4>;
396 : ++counter0;
397 : };
398 : EXPAND_PACK_LEFT_TO_RIGHT(
399 : helper0.template operator()<IntegralConstants0>());
400 : ++counter1;
401 : };
402 : EXPAND_PACK_LEFT_TO_RIGHT(
403 : helper1.template operator()<IntegralConstants1>());
404 : ++counter2;
405 : };
406 : EXPAND_PACK_LEFT_TO_RIGHT(
407 : helper2.template operator()<IntegralConstants2>());
408 : ++counter3;
409 : };
410 : EXPAND_PACK_LEFT_TO_RIGHT(
411 : helper3.template operator()<IntegralConstants3>());
412 : ++counter4;
413 : };
414 : EXPAND_PACK_LEFT_TO_RIGHT(
415 : helper4.template operator()<IntegralConstants4>());
416 : return result;
417 : }();
418 :
419 : // The array `cache` holds pointers to member functions, so we dereference
420 : // the pointer and invoke it on `this`.
421 : return (
422 : this->*gsl::at(
423 : cache,
424 : std::get<0>(parameter0) +
425 : num0 *
426 : (std::get<0>(parameter1) +
427 : num1 * (std::get<0>(parameter2) +
428 : num2 * (std::get<0>(parameter3) +
429 : num3 * std::get<0>(parameter4))))))(
430 : parameters...);
431 : }
432 : #endif // defined(__NVCC__) && defined(__CUDA_ARCH__)
433 :
434 0 : const Generator generator_;
435 : };
436 :
437 : /// \ingroup UtilitiesGroup
438 : /// Create a StaticCache, inferring the cached type from the generator.
439 : template <typename... Ranges, typename Generator>
440 1 : auto make_static_cache(Generator&& generator) {
441 : using CachedType = std::remove_cv_t<decltype(generator(
442 : std::declval<typename Ranges::value_type>()...))>;
443 : return StaticCache<std::remove_cv_t<Generator>, CachedType, Ranges...>(
444 : std::forward<Generator>(generator));
445 : }
|