Files
Jungfraujoch/image_analysis/ReflectionArena.h
T
leonarski_fandClaude Opus 5 ca9a21c45e rugnux: keep a pass's per-image reflections in large blocks
The whole-run passes retain every frame's integrated reflections until scaling is done - thousands
of vectors of a few megabytes each, allocated by the image workers in the allocator's per-thread
arenas. When a pass hands them back, most of that memory stays in those arenas as holes, and the
next pass's workers (new threads) do not reuse it, so on a fine-sliced long axis gigabytes of freed
reflections were carried to the end of the run.

IndexAndRefine now copies each retained frame's reflections into a ReflectionArena: 64 MiB blocks,
each its own mapping, carved by a bump pointer and returned to the system in one piece when the
last vector in them is gone. IntegrationOutcome::reflections becomes a std::vector with an allocator
that uses the arena when given one and plain new/delete otherwise (copies go to the heap), so the
read sites are unchanged; the few functions that took the vector by type now take a span.

No arithmetic changes; merged output byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT
2026-09-20 18:45:17 +02:00

114 lines
4.6 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <algorithm>
#include <cstddef>
#include <mutex>
#include <new>
#include <type_traits>
#include <vector>
// Large blocks that a whole pass's per-image reflection vectors are carved from.
//
// A rotation pass keeps every frame's integrated reflections until its scaling is done - thousands of
// vectors of a few megabytes each, gigabytes together, allocated by the image workers in the
// allocator's per-thread arenas. When the pass hands them back, that memory mostly stays in those
// arenas, as holes between whatever else the workers allocated, and the next pass's workers, being
// new threads, do not reuse it: on a fine-sliced long axis gigabytes of freed reflections were carried
// to the end of the run. Here a block is allocated in one piece (large enough that it is its own
// mapping) and returned in one piece once the last vector in it is gone, so a pass's reflections
// leave nothing behind.
//
// A vector is carved from the current block by bumping a pointer; the space of a vector freed early
// is only reclaimed with its whole block. That suits the use here: the vectors are written once,
// when the frame is integrated, and handed back together.
class ReflectionArena {
public:
ReflectionArena() = default;
ReflectionArena(const ReflectionArena &) = delete;
ReflectionArena &operator=(const ReflectionArena &) = delete;
~ReflectionArena() {
for (auto &b : blocks)
::operator delete(b.data);
}
void *Allocate(size_t bytes) {
bytes = (bytes + kAlign - 1) / kAlign * kAlign;
std::unique_lock ul(m);
if (blocks.empty() || blocks.back().size - blocks.back().used < bytes) {
const size_t size = std::max(kBlockBytes, bytes);
blocks.push_back(Block{static_cast<char *>(::operator new(size)), size, 0, 0});
}
Block &b = blocks.back();
void *p = b.data + b.used;
b.used += bytes;
b.live++;
return p;
}
void Deallocate(void *p) {
std::unique_lock ul(m);
for (size_t i = 0; i < blocks.size(); ++i) {
Block &b = blocks[i];
if (static_cast<char *>(p) < b.data || static_cast<char *>(p) >= b.data + b.size)
continue;
if (--b.live == 0) {
if (i + 1 == blocks.size()) {
b.used = 0; // the block being filled: keep it for the next vector
} else {
::operator delete(b.data);
blocks.erase(blocks.begin() + static_cast<std::ptrdiff_t>(i));
}
}
return;
}
}
private:
// 64 MiB: above the largest size glibc ever serves from its heaps (32 MiB), so every block is a
// mapping of its own and goes back to the system when freed. A frame's reflections are a few
// megabytes, so the tail a block cannot fit is a few percent of it.
static constexpr size_t kBlockBytes = size_t(64) << 20;
static constexpr size_t kAlign = alignof(std::max_align_t);
struct Block {
char *data;
size_t size, used, live;
};
std::mutex m;
std::vector<Block> blocks;
};
// A std::allocator that carves from a ReflectionArena, or is plain new/delete without one. A copy of
// a container is made on the heap (select_on_container_copy_construction), so only the containers the
// arena's owner fills itself live in the arena; a move carries the arena along with the storage.
template <class T>
struct ArenaAllocator {
using value_type = T;
using propagate_on_container_move_assignment = std::true_type;
using propagate_on_container_swap = std::true_type;
ReflectionArena *arena = nullptr;
ArenaAllocator() = default;
explicit ArenaAllocator(ReflectionArena *a) : arena(a) {}
template <class U> ArenaAllocator(const ArenaAllocator<U> &o) : arena(o.arena) {}
T *allocate(size_t n) {
if (arena)
return static_cast<T *>(arena->Allocate(n * sizeof(T)));
return static_cast<T *>(::operator new(n * sizeof(T)));
}
void deallocate(T *p, size_t) {
if (arena)
arena->Deallocate(p);
else
::operator delete(p);
}
ArenaAllocator select_on_container_copy_construction() const { return ArenaAllocator(); }
template <class U> bool operator==(const ArenaAllocator<U> &o) const { return arena == o.arena; }
template <class U> bool operator!=(const ArenaAllocator<U> &o) const { return arena != o.arena; }
};