|
5 | 5 | #include <algorithm> |
6 | 6 | #include <cassert> |
7 | 7 | #include <cmath> |
8 | | -#include <functional> |
9 | | -#include <memory> |
| 8 | +#include <limits> |
10 | 9 | #include <span> |
11 | 10 | #include <type_traits> |
12 | 11 | #include <variant> |
@@ -214,150 +213,118 @@ inline void StdTransform(std::span<const T> data, std::span<T> out, |
214 | 213 | StdTransformWithStats(data, out, std::span<T>{}, w); |
215 | 214 | } |
216 | 215 |
|
217 | | -// ============================================================================ |
218 | | -// CompAccumulator - PARTIALLY OPTIMIZED for SkipNA=false |
219 | | -// ============================================================================ |
220 | | -// Cannot fully remove isnan checks because NaN breaks comparison semantics |
221 | | -// (NaN < x and NaN > x are both false), which corrupts the monotonic deque. |
222 | | -// Optimization: Short-circuit all deque operations once has_nan_ is true. |
223 | | -// ============================================================================ |
224 | | -template <typename T, typename Comp, bool SkipNA> class CompAccumulator { |
225 | | -public: |
226 | | - // std::pair's default constructor value-initializes, which would zero the |
227 | | - // whole ring buffer on every allocation; this aggregate's one is trivial |
228 | | - struct Entry { |
229 | | - index_t index; |
230 | | - T value; |
231 | | - }; |
| 216 | +// Rolling min and max are one van Herk / Gil-Werman block scan: each output |
| 217 | +// joins the suffix scan of the previous block with the prefix scan of the |
| 218 | +// current one, three compare-selects per element and no data-dependent |
| 219 | +// branch. The op must stay the plain `a < b ? b : a` (one maxsd on x86-64, |
| 220 | +// fcsel on aarch64): std::fmax is a libm call on x86-64 GCC and an isnan() |
| 221 | +// select becomes a branch. NaN handling therefore lives outside the op. With |
| 222 | +// skipna a NaN turns into the identity before the op and a rolling NaN count |
| 223 | +// makes a window without a valid value NaN, which the identity alone can't |
| 224 | +// since -inf is a legitimate value. Without skipna the caller passes NaN-free |
| 225 | +// data. |
| 226 | +template <typename T> struct MaxOp { |
| 227 | + static constexpr T id = -std::numeric_limits<T>::infinity(); |
| 228 | + T operator()(T a, T b) const noexcept { return a < b ? b : a; } |
| 229 | +}; |
232 | 230 |
|
233 | | - CompAccumulator(index_t window_size) |
234 | | - : buffer_(std::make_unique_for_overwrite<Entry[]>( |
235 | | - static_cast<size_t>(window_size))), |
236 | | - window_size_(window_size) {} |
237 | | - inline bool Empty() const noexcept { return tail_ == -1; } |
238 | | - inline void PushBack(index_t i, T x) noexcept { |
239 | | - if (tail_ == -1) { |
240 | | - head_ = 0; |
241 | | - tail_ = 0; |
242 | | - } else if (tail_ == window_size_ - 1) { |
243 | | - tail_ = 0; |
244 | | - } else { |
245 | | - ++tail_; |
246 | | - } |
247 | | - buffer_[tail_] = {i, x}; |
| 231 | +template <typename T> struct MinOp { |
| 232 | + static constexpr T id = std::numeric_limits<T>::infinity(); |
| 233 | + T operator()(T a, T b) const noexcept { return b < a ? b : a; } |
| 234 | +}; |
| 235 | + |
| 236 | +template <typename T, typename Op, bool SkipNA> |
| 237 | +inline void BlockScan(std::span<const T> data, std::span<T> out, index_t w, |
| 238 | + index_t min_samples) { |
| 239 | + const Op op; |
| 240 | + const index_t n = std::ssize(data); |
| 241 | + auto clean = [](T x) -> T { |
| 242 | + if constexpr (SkipNA) |
| 243 | + return std::isnan(x) ? Op::id : x; |
| 244 | + else |
| 245 | + return x; |
| 246 | + }; |
| 247 | + auto is_nan = [](T x) -> index_t { |
| 248 | + if constexpr (SkipNA) |
| 249 | + return std::isnan(x); |
| 250 | + else |
| 251 | + return 0; |
| 252 | + }; |
| 253 | + index_t nans = 0; |
| 254 | + T run = Op::id; |
| 255 | + const index_t head = std::min(w, n); |
| 256 | + for (index_t i = 0; i < head; ++i) { |
| 257 | + run = op(run, clean(data[i])); |
| 258 | + nans += is_nan(data[i]); |
| 259 | + out[i] = (i + 1 < min_samples || nans == i + 1) ? kNaN<T> : run; |
248 | 260 | } |
249 | | - inline void PopBack() noexcept { |
250 | | - if (head_ == tail_) { |
251 | | - head_ = 0; |
252 | | - tail_ = -1; |
253 | | - } else if (tail_ == 0) { |
254 | | - tail_ = window_size_ - 1; |
255 | | - } else { |
256 | | - --tail_; |
| 261 | + if (n <= w) |
| 262 | + return; |
| 263 | + // slot w holds the identity so the window that is exactly one block needs |
| 264 | + // no special case |
| 265 | + std::vector<T> prev(w + 1, Op::id), cur(w + 1, Op::id); |
| 266 | + auto suffix = [&](std::vector<T> &buf, index_t start) { |
| 267 | + buf[w - 1] = clean(data[start + w - 1]); |
| 268 | + for (index_t j = w - 2; j >= 0; --j) |
| 269 | + buf[j] = op(clean(data[start + j]), buf[j + 1]); |
| 270 | + }; |
| 271 | + suffix(prev, 0); |
| 272 | + for (index_t b = w; b < n; b += w) { |
| 273 | + const index_t len = std::min(w, n - b); |
| 274 | + run = Op::id; |
| 275 | + for (index_t o = 0; o < len; ++o) { |
| 276 | + const index_t i = b + o; |
| 277 | + run = op(run, clean(data[i])); |
| 278 | + nans += is_nan(data[i]) - is_nan(data[i - w]); |
| 279 | + out[i] = nans == w ? kNaN<T> : op(prev[o + 1], run); |
257 | 280 | } |
258 | | - } |
259 | | - inline void PopFront() noexcept { |
260 | | - if (head_ == tail_) { |
261 | | - head_ = 0; |
262 | | - tail_ = -1; |
263 | | - } else if (head_ == window_size_ - 1) { |
264 | | - head_ = 0; |
265 | | - } else { |
266 | | - ++head_; |
| 281 | + if (b + len < n) { |
| 282 | + suffix(cur, b); |
| 283 | + std::swap(prev, cur); |
267 | 284 | } |
268 | 285 | } |
269 | | - inline const Entry &Front() const noexcept { return buffer_[head_]; } |
270 | | - inline const Entry &Back() const noexcept { return buffer_[tail_]; } |
271 | | - |
272 | | - void Insert(T x) noexcept { |
273 | | - if constexpr (!SkipNA) { |
274 | | - // Short-circuit: once NaN seen, skip all deque maintenance |
275 | | - if (has_nan_) { |
276 | | - ++i_; |
277 | | - return; |
278 | | - } |
279 | | - if (std::isnan(x)) { |
280 | | - has_nan_ = true; |
281 | | - ++i_; |
282 | | - return; |
283 | | - } |
284 | | - } |
285 | | - |
286 | | - if constexpr (SkipNA) { |
287 | | - if (std::isnan(x)) { |
288 | | - // the front can expire on this step even though nothing is inserted; |
289 | | - // skipping the check leaves an out-of-window entry to be returned |
290 | | - if (!Empty() && Front().index <= i_) { |
291 | | - PopFront(); |
292 | | - } |
293 | | - ++i_; |
294 | | - return; |
295 | | - } |
296 | | - } |
| 286 | +} |
297 | 287 |
|
298 | | - // Valid value: maintain monotonic deque |
299 | | - while (!Empty() && comp_(Back().value, x)) { |
300 | | - PopBack(); |
301 | | - } |
302 | | - if (!Empty() && Front().index <= i_) { |
303 | | - PopFront(); |
304 | | - } |
305 | | - PushBack(window_size_ + i_, x); |
306 | | - ++i_; |
| 288 | +template <typename T, typename Op> |
| 289 | +inline void CompTransform(std::span<const T> data, std::span<T> out, |
| 290 | + const Window &w) { |
| 291 | + assert(w.window_size > 0 && w.min_samples > 0); |
| 292 | + const index_t n = std::ssize(data); |
| 293 | + if (n < w.min_samples) { |
| 294 | + FillNaN(out); |
| 295 | + return; |
307 | 296 | } |
308 | | - |
309 | | - void Update(T x) noexcept { Insert(x); } |
310 | | - |
311 | | - T Update(T x, index_t) noexcept { |
312 | | - Insert(x); |
313 | | - if constexpr (!SkipNA) { |
314 | | - if (has_nan_) |
315 | | - return kNaN<T>; |
316 | | - } |
317 | | - if (Empty()) |
318 | | - return kNaN<T>; |
319 | | - return Front().value; |
| 297 | + if (w.skipna) { |
| 298 | + const index_t window_size = std::min(w.window_size, n); |
| 299 | + BlockScan<T, Op, true>(data, out, window_size, |
| 300 | + std::min(w.min_samples, window_size)); |
| 301 | + return; |
320 | 302 | } |
321 | | - |
322 | | - T Update(T new_x, T) noexcept { |
323 | | - Insert(new_x); |
324 | | - if constexpr (!SkipNA) { |
325 | | - if (has_nan_) |
326 | | - return kNaN<T>; |
327 | | - } |
328 | | - if (Empty()) |
329 | | - return kNaN<T>; |
330 | | - return Front().value; |
| 303 | + // a NaN poisons its own output and every later one, so the kernel only |
| 304 | + // sees the clean prefix; this branch fires once |
| 305 | + index_t k = 0; |
| 306 | + while (k < n && !std::isnan(data[k])) |
| 307 | + ++k; |
| 308 | + if (k < w.min_samples) { |
| 309 | + FillNaN(out); |
| 310 | + return; |
331 | 311 | } |
332 | | - |
333 | | -private: |
334 | | - std::unique_ptr<Entry[]> buffer_; |
335 | | - index_t window_size_; |
336 | | - index_t head_ = 0; |
337 | | - index_t tail_ = -1; |
338 | | - index_t i_ = 0; |
339 | | - [[no_unique_address]] std::conditional_t<SkipNA, std::monostate, bool> |
340 | | - has_nan_{}; |
341 | | - Comp comp_ = Comp(); |
342 | | -}; |
| 312 | + const index_t window_size = std::min(w.window_size, k); |
| 313 | + BlockScan<T, Op, false>(data.first(k), out.first(k), window_size, |
| 314 | + std::min(w.min_samples, window_size)); |
| 315 | + FillNaN(out.subspan(k)); |
| 316 | +} |
343 | 317 |
|
344 | 318 | template <typename T> |
345 | | -void MinTransform(std::span<const T> data, std::span<T> out, const Window &w) { |
346 | | - if (w.skipna) { |
347 | | - Transform<T, CompAccumulator<T, std::greater_equal<T>, true>>(data, out, w); |
348 | | - } else { |
349 | | - Transform<T, CompAccumulator<T, std::greater_equal<T>, false>>(data, out, |
350 | | - w); |
351 | | - } |
| 319 | +inline void MinTransform(std::span<const T> data, std::span<T> out, |
| 320 | + const Window &w) { |
| 321 | + CompTransform<T, MinOp<T>>(data, out, w); |
352 | 322 | } |
353 | 323 |
|
354 | 324 | template <typename T> |
355 | | -void MaxTransform(std::span<const T> data, std::span<T> out, const Window &w) { |
356 | | - if (w.skipna) { |
357 | | - Transform<T, CompAccumulator<T, std::less_equal<T>, true>>(data, out, w); |
358 | | - } else { |
359 | | - Transform<T, CompAccumulator<T, std::less_equal<T>, false>>(data, out, w); |
360 | | - } |
| 325 | +inline void MaxTransform(std::span<const T> data, std::span<T> out, |
| 326 | + const Window &w) { |
| 327 | + CompTransform<T, MaxOp<T>>(data, out, w); |
361 | 328 | } |
362 | 329 |
|
363 | 330 | // ============================================================================ |
|
0 commit comments