Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cpp/allocators/best_fit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ std::vector<Allocation> best_fit_place(
const std::vector<Allocation>& allocations) {
// Lambda rather than the function pointer so the placement loop inlines
// the offset scan instead of an indirect call per allocation
return place_indexed(allocations, compute_conflict_indices(allocations),
return place_indexed(allocations, build_conflict_adjacency(allocations),
[](int64_t size, const auto& spans) {
return find_best_fit_offset(size, spans);
});
Expand Down
22 changes: 11 additions & 11 deletions src/cpp/allocators/first_fit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <cstring>
#include <limits>
#include <numeric>
#include <span>
#include <stdexcept>
#include <string>
#include <utility>
Expand Down Expand Up @@ -302,12 +303,13 @@ PortfolioPlacement place_portfolio(const std::vector<Allocation>& allocations,
return best;
}

void gather_spans(const std::vector<size_t>& neighbors,
void gather_spans(std::span<const int32_t> neighbors,
const std::vector<std::optional<int64_t>>& offsets,
const std::vector<Allocation>& allocations,
std::vector<std::pair<int64_t, int64_t>>& spans) {
spans.clear();
for (size_t j : neighbors) {
for (const int32_t neighbor : neighbors) {
const auto j = static_cast<size_t>(neighbor);
if (offsets[j].has_value()) {
spans.emplace_back(*offsets[j], *offsets[j] + allocations[j].size());
}
Expand All @@ -328,25 +330,23 @@ int64_t first_fit_offset(
}

std::vector<Allocation> first_fit_place_indexed(
const std::vector<Allocation>& allocations,
const ConflictIndices& indices) {
const std::vector<Allocation>& allocations, const CsrAdjacency& adj) {
// Lambda rather than the function pointer so the placement loop inlines
// the offset scan instead of an indirect call per allocation
return place_indexed(allocations, indices,
[](int64_t size, const auto& spans) {
return first_fit_offset(size, spans);
});
return place_indexed(allocations, adj, [](int64_t size, const auto& spans) {
return first_fit_offset(size, spans);
});
}

std::vector<Allocation> first_fit_place(
const std::vector<Allocation>& allocations) {
return first_fit_place_indexed(allocations,
compute_conflict_indices(allocations));
build_conflict_adjacency(allocations));
}

FirstFitPlacer::FirstFitPlacer(std::vector<Allocation> allocations)
: allocations_(std::move(allocations)),
indices_(compute_conflict_indices(allocations_)) {
adj_(build_conflict_adjacency(allocations_)) {
check_total_size(allocations_);
}

Expand Down Expand Up @@ -379,7 +379,7 @@ std::vector<std::optional<int64_t>> FirstFitPlacer::place_offsets(
if (alloc.offset().has_value()) {
continue;
}
gather_spans(indices_[idx], offsets, allocations_, spans);
gather_spans(adj_.row(idx), offsets, allocations_, spans);
offsets[idx] = first_fit_offset(alloc.size(), spans);
}
return offsets;
Expand Down
15 changes: 7 additions & 8 deletions src/cpp/allocators/first_fit.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <cstdint>
#include <optional>
#include <span>
#include <utility>
#include <vector>

Expand All @@ -16,7 +17,7 @@ namespace omnimalloc {

// Occupied (offset, end) spans of the already-placed neighbors of one
// allocation, sorted by offset so the gap scans can go left-to-right
void gather_spans(const std::vector<size_t>& neighbors,
void gather_spans(std::span<const int32_t> neighbors,
const std::vector<std::optional<int64_t>>& offsets,
const std::vector<Allocation>& allocations,
std::vector<std::pair<int64_t, int64_t>>& spans);
Expand Down Expand Up @@ -47,14 +48,14 @@ struct PortfolioPlacement {
// Greedily place allocations in order using first-fit over an index-based
// adjacency (the fast path: each step only visits the allocation's neighbors)
[[nodiscard]] std::vector<Allocation> first_fit_place_indexed(
const std::vector<Allocation>& allocations, const ConflictIndices& indices);
const std::vector<Allocation>& allocations, const CsrAdjacency& adj);

// Shared placement skeleton of the first-fit and best-fit placers: place in
// index order, choosing each offset with `choose_offset` over the sorted spans
// of already-placed neighbors. Seeding pins makes them obstacles throughout.
template <typename OffsetFn>
[[nodiscard]] std::vector<Allocation> place_indexed(
const std::vector<Allocation>& allocations, const ConflictIndices& indices,
const std::vector<Allocation>& allocations, const CsrAdjacency& adj,
OffsetFn choose_offset) {
check_total_size(allocations);
std::vector<std::optional<int64_t>> offsets(allocations.size());
Expand All @@ -66,7 +67,7 @@ template <typename OffsetFn>
placed.reserve(allocations.size());
for (size_t i = 0; i < allocations.size(); ++i) {
if (!allocations[i].offset().has_value()) {
gather_spans(indices[i], offsets, allocations, spans);
gather_spans(adj.row(i), offsets, allocations, spans);
offsets[i] = choose_offset(allocations[i].size(), spans);
}
placed.push_back(allocations[i].with_offset(*offsets[i]));
Expand All @@ -91,9 +92,7 @@ class FirstFitPlacer {
const std::vector<size_t>& order) const;

// The resident index adjacency, for the local searches' inner loops.
[[nodiscard]] const ConflictIndices& indices() const noexcept {
return indices_;
}
[[nodiscard]] const CsrAdjacency& adjacency() const noexcept { return adj_; }

private:
// Throw std::invalid_argument unless every index in `order` is in range
Expand All @@ -106,7 +105,7 @@ class FirstFitPlacer {
const std::vector<size_t>& order) const;

std::vector<Allocation> allocations_;
ConflictIndices indices_;
CsrAdjacency adj_;
};

} // namespace omnimalloc
11 changes: 6 additions & 5 deletions src/cpp/allocators/local_search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <algorithm>
#include <cassert>
#include <cstdint>
#include <numeric>

namespace omnimalloc {
Expand Down Expand Up @@ -43,13 +44,13 @@ std::vector<size_t> initial_order(const std::vector<Allocation>& allocations) {

std::vector<size_t> earlier_neighbors(const std::vector<size_t>& order,
size_t target_pos,
const ConflictIndices& indices) {
const CsrAdjacency& adj) {
// Mark the target's conflicts, then keep the earlier positions holding
// one; the marks cost a pass over the order, the alternative a hash
// lookup per earlier position
std::vector<char> conflicting(order.size(), 0);
for (size_t other : indices[order[target_pos]]) {
conflicting[other] = 1;
for (const int32_t other : adj.row(order[target_pos])) {
conflicting[static_cast<size_t>(other)] = 1;
}
std::vector<size_t> neighbors;
for (size_t pos = 0; pos < target_pos; ++pos) {
Expand All @@ -66,12 +67,12 @@ std::vector<size_t> earlier_neighbors(const std::vector<size_t>& order,

std::optional<std::pair<size_t, size_t>> propose_peak_swap(
const std::vector<size_t>& peaks, const std::vector<size_t>& order,
const ConflictIndices& indices, std::mt19937_64& rng) {
const CsrAdjacency& adj, std::mt19937_64& rng) {
assert(!peaks.empty()); // full placements always attain their peak
std::uniform_int_distribution<size_t> pick_peak(0, peaks.size() - 1);
const size_t target_pos = peaks[pick_peak(rng)];
const std::vector<size_t> neighbors =
earlier_neighbors(order, target_pos, indices);
earlier_neighbors(order, target_pos, adj);
if (neighbors.empty()) {
return std::nullopt;
}
Expand Down
4 changes: 2 additions & 2 deletions src/cpp/allocators/local_search.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ namespace omnimalloc {
// adjacency: this runs per sampled move, where hashing ids would dominate.
[[nodiscard]] std::vector<size_t> earlier_neighbors(
const std::vector<size_t>& order, size_t target_pos,
const ConflictIndices& indices);
const CsrAdjacency& adj);

// One random peak-lowering move for the local searches: a random position
// among `peaks` paired with a random earlier temporal neighbor, or nullopt
// when the chosen target has no earlier position to swap with.
[[nodiscard]] std::optional<std::pair<size_t, size_t>> propose_peak_swap(
const std::vector<size_t>& peaks, const std::vector<size_t>& order,
const ConflictIndices& indices, std::mt19937_64& rng);
const CsrAdjacency& adj, std::mt19937_64& rng);

} // namespace omnimalloc
2 changes: 1 addition & 1 deletion src/cpp/allocators/simulated_annealing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ std::vector<Allocation> simulated_annealing_place(
peak_positions(current_placed, current_peak);

const auto proposal =
propose_peak_swap(peaks, order, placer.indices(), rng);
propose_peak_swap(peaks, order, placer.adjacency(), rng);
if (!proposal) {
temperature *= config.cooling_rate;
continue;
Expand Down
2 changes: 1 addition & 1 deletion src/cpp/allocators/tabu_search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ std::vector<Allocation> tabu_search_place(
break;
}
const auto proposal =
propose_peak_swap(peaks, order, placer.indices(), rng);
propose_peak_swap(peaks, order, placer.adjacency(), rng);
if (!proposal) {
continue;
}
Expand Down
24 changes: 11 additions & 13 deletions src/cpp/allocators/telamalloc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ constexpr int64_t kUnbounded = std::numeric_limits<int64_t>::max() / 4;

// Connected components of the overlap graph: the paper's "phases". Buffers
// in different components never interact, so each packs independently.
std::vector<std::vector<int>> build_phases(const ConflictIndices& neighbors) {
std::vector<std::vector<int>> build_phases(const CsrAdjacency& neighbors) {
const int n = static_cast<int>(neighbors.size());
std::vector<std::vector<int>> phases;
std::vector<char> visited(n, 0);
Expand All @@ -45,7 +45,7 @@ std::vector<std::vector<int>> build_phases(const ConflictIndices& neighbors) {
int idx = stack.back();
stack.pop_back();
phase.push_back(idx);
for (size_t other : neighbors[idx]) {
for (const int32_t other : neighbors.row(static_cast<size_t>(idx))) {
if (!visited[other]) {
visited[other] = 1;
stack.push_back(static_cast<int>(other));
Expand Down Expand Up @@ -88,10 +88,9 @@ QueueKey queue_key(const Allocation& alloc, int idx, int evictions,
// fitting gap, evicting the cheapest blocking set on conflict and requeueing it
// at raised priority; a spent eviction budget wipes and re-packs the phase.
std::optional<std::vector<int64_t>> pack_phase(
const std::vector<Allocation>& allocations,
const ConflictIndices& neighbors, const std::vector<int>& phase,
int64_t capacity, int max_backtracks, const Deadline& deadline,
bool size_major, uint64_t seed) {
const std::vector<Allocation>& allocations, const CsrAdjacency& neighbors,
const std::vector<int>& phase, int64_t capacity, int max_backtracks,
const Deadline& deadline, bool size_major, uint64_t seed) {
std::vector<int64_t> offsets(allocations.size(), -1);
std::vector<int> evictions(allocations.size(), 0);
std::mt19937_64 rng(seed);
Expand Down Expand Up @@ -130,9 +129,9 @@ std::optional<std::vector<int64_t>> pack_phase(
// spans inherit occupied's offset order (ties differ only in end
// order, which cannot change a gap scan's result), so one sort does.
occupied.clear();
for (size_t other : neighbors[idx]) {
for (const int32_t other : neighbors.row(static_cast<size_t>(idx))) {
if (offsets[other] >= 0) {
occupied.emplace_back(offsets[other], static_cast<int>(other));
occupied.emplace_back(offsets[other], other);
}
}
std::sort(occupied.begin(), occupied.end());
Expand Down Expand Up @@ -232,10 +231,9 @@ int64_t phase_peak(const std::vector<Allocation>& allocations,
// search on capacity down toward the load lower bound. Budget-exhausted
// attempts count as infeasible, keeping the search anytime rather than exact.
void solve_phase(const std::vector<Allocation>& allocations,
const ConflictIndices& neighbors,
const std::vector<int>& phase, int64_t lower_bound,
const TelamallocConfig& config, const Deadline& deadline,
std::vector<int64_t>& result) {
const CsrAdjacency& neighbors, const std::vector<int>& phase,
int64_t lower_bound, const TelamallocConfig& config,
const Deadline& deadline, std::vector<int64_t>& result) {
// Unbounded capacity never conflicts, so these incumbents are plain
// first-fit in each tiered order and cannot fail. The winner's order also
// steers the capacity search below.
Expand Down Expand Up @@ -284,7 +282,7 @@ std::vector<Allocation> telamalloc_place(
// Bound by kUnbounded so the unbounded-capacity pack_phase incumbents in
// solve_phase can never fail and the cursor arithmetic cannot overflow.
check_total_size(allocations, kUnbounded);
const ConflictIndices neighbors = compute_conflict_indices(allocations);
const CsrAdjacency neighbors = build_conflict_adjacency(allocations);
if (allocations.size() < 2) {
return first_fit_place_indexed(allocations, neighbors);
}
Expand Down
17 changes: 14 additions & 3 deletions src/cpp/analysis/clock.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,22 @@ inline std::vector<int64_t> interval_peaks(
struct CsrAdjacency {
std::vector<int64_t> offsets;
std::vector<int32_t> neighbors;

[[nodiscard]] size_t size() const noexcept { return offsets.size() - 1; }

// One row as a view into the shared storage, so walking the relation costs
// no copy and the 4-bytes-per-edge accounting holds all the way through.
[[nodiscard]] std::span<const int32_t> row(size_t index) const noexcept {
return {neighbors.data() + offsets[index],
static_cast<size_t>(offsets[index + 1] - offsets[index])};
}
};

// Ceiling on the neighbor entries one adjacency may materialize, 4 bytes each.
// No work budget bounds the CSR: budgets count the sweep, and the sweep is what
// fills the rows. A guard against taking the host down, not a tuning knob.
// Ceiling on the neighbor entries one adjacency may materialize, 4 bytes each
// and 4 all the way through: every consumer walks the CSR rows in place, so the
// accounting is the whole cost, not a stage of it. No work budget bounds the
// CSR: budgets count the sweep, and the sweep is what fills the rows. A guard
// against taking the host down, not a tuning knob.
inline constexpr uint64_t kMaxAdjacencyEntries = uint64_t{1} << 31;

// Pairwise happens-before conflict sweep, O(n^2 * T) worst case; vector clocks
Expand Down
16 changes: 0 additions & 16 deletions src/cpp/analysis/conflicts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,6 @@ void check_sweep_budget(const ConflictSweep& sweep,
}
}

// Pairwise happens-before adjacency unpacked from CSR form
ConflictIndices indices_from_adjacency(size_t n, const CsrAdjacency& adj) {
ConflictIndices indices(n);
for (size_t i = 0; i < n; ++i) {
indices[i].assign(adj.neighbors.begin() + adj.offsets[i],
adj.neighbors.begin() + adj.offsets[i + 1]);
}
return indices;
}

// Total id-keyed conflict map straight from CSR: the index adjacency in
// between would cost a widening copy of every directed edge.
ConflictMap conflict_map_from_adjacency(
Expand Down Expand Up @@ -74,12 +64,6 @@ CsrAdjacency build_conflict_adjacency(
return sweep.adjacency(parallel_threads(sweep.count()));
}

ConflictIndices compute_conflict_indices(
const std::vector<Allocation>& allocations) {
return indices_from_adjacency(allocations.size(),
build_conflict_adjacency(allocations));
}

ConflictMap conflicts(const std::vector<Allocation>& allocations,
std::optional<uint64_t> work_budget) {
// The sweep work equals the conflict-pair count on scalar input, so one
Expand Down
7 changes: 0 additions & 7 deletions src/cpp/analysis/conflicts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,12 @@ using ConflictMap =
std::unordered_map<IdType, std::unordered_set<IdType, IdTypeHash>,
IdTypeHash>;

// Index-based conflict adjacency: position i -> positions conflicting with i
using ConflictIndices = std::vector<std::vector<size_t>>;

// Map each allocation id to the ids of conflicting allocations (the
// happens-before conflict relation every placement packs against). A set
// `work_budget` bounds the quadratic sweep, throwing rather than stalling.
[[nodiscard]] ConflictMap conflicts(const std::vector<Allocation>& allocations,
std::optional<uint64_t> work_budget);

// Map each allocation index to the indices of conflicting allocations
[[nodiscard]] ConflictIndices compute_conflict_indices(
const std::vector<Allocation>& allocations);

// Per-allocation count of conflicting allocations, aligned with `allocations`
// and counted with multiplicity. Scalar timelines count in O(N log N) without
// enumerating pairs; on vector clocks `work_budget` bounds the sweep.
Expand Down
Loading
Loading