| //===- StaticMemoryPlanning.cpp - Memory planning algorithms --------------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h" |
| #include "llvm/Support/MathExtras.h" |
| #include <numeric> |
| |
| using namespace mlir::bufferization; |
| |
| /// Align an offset to the specified alignment. |
| static int64_t alignOffset(int64_t offset, int64_t alignment) { |
| return llvm::alignTo(offset, alignment); |
| } |
| |
| /// Trivial sequential packing: places each allocation immediately after the |
| /// previous one with alignment padding. Does not consider lifetimes, so no |
| /// memory is reused. This gives a simple upper bound on arena size. |
| /// Complexity: O(n) where n is the number of allocations. |
| llvm::SmallVector<int64_t> mlir::bufferization::trivialMemoryPlanner( |
| int64_t arenaAlignment, llvm::ArrayRef<MemoryPlannerAlloc> allocs) { |
| llvm::SmallVector<int64_t> offsets; |
| int64_t currentOffset = 0; |
| |
| for (const auto &alloc : allocs) { |
| currentOffset = alignOffset(currentOffset, alloc.alignment); |
| assert((arenaAlignment + currentOffset) % alloc.alignment == 0 && |
| "invalid alignment"); |
| offsets.push_back(currentOffset); |
| currentOffset += alloc.sizeInBytes; |
| } |
| |
| return offsets; |
| } |
| |
| /// Best-fit lifetime-aware packing: processes allocations in start-time order |
| /// and places each one in the smallest gap left by allocations whose lifetimes |
| /// have ended. If no existing gap is large enough, the arena is extended. |
| /// This minimizes peak memory usage when allocations have non-overlapping |
| /// lifetimes. |
| /// Complexity: O(n^2) where n is the number of allocations. |
| llvm::SmallVector<int64_t> mlir::bufferization::bestFitMemoryPlanner( |
| int64_t arenaAlignment, llvm::ArrayRef<MemoryPlannerAlloc> allocs) { |
| // Tracks where each allocation was placed. We only need timeEnd because |
| // allocations are processed in timeStart order — by the time we place a new |
| // allocation, all earlier placements already started, so we only need to |
| // check which ones are still live. |
| struct Placement { |
| int64_t offset; |
| int64_t size; |
| int64_t timeEnd; |
| }; |
| |
| // Process allocations in order of start time. |
| llvm::SmallVector<unsigned> order(allocs.size()); |
| std::iota(order.begin(), order.end(), 0); |
| llvm::sort(order, [&](unsigned a, unsigned b) { |
| return allocs[a].timeStart < allocs[b].timeStart; |
| }); |
| |
| llvm::SmallVector<Placement> placements; |
| llvm::SmallVector<int64_t> offsets(allocs.size(), 0); |
| int64_t arenaEnd = 0; |
| |
| // Loop over all required allocations and fit them into best gaps. |
| for (unsigned idx : order) { |
| const MemoryPlannerAlloc &alloc = allocs[idx]; |
| |
| // Collect allocations that are still live at this alloc's start time. |
| // occupied is pairs of <offset_start, offset_end>. |
| llvm::SmallVector<std::pair<int64_t, int64_t>> occupied; |
| for (const auto &p : placements) { |
| if (p.timeEnd > alloc.timeStart) |
| occupied.push_back({p.offset, p.offset + p.size}); |
| } |
| llvm::sort(occupied); |
| |
| // Find the best (smallest) gap that fits this allocation. |
| int64_t bestOffset = -1; |
| int64_t bestGapSize = INT64_MAX; |
| |
| int64_t gapStart = 0; |
| for (const auto &[occStart, occEnd] : occupied) { |
| int64_t alignedStart = alignOffset(gapStart, alloc.alignment); |
| if (alignedStart >= occStart) { |
| gapStart = std::max(gapStart, occEnd); |
| continue; |
| } |
| int64_t gapEnd = occStart; |
| int64_t gapSize = gapEnd - alignedStart; |
| // Gap is large enough to fit this allocation (after alignment). |
| if (gapSize >= alloc.sizeInBytes) { |
| // Track the smallest sufficient gap (best-fit strategy). |
| if (gapSize < bestGapSize) { |
| bestGapSize = gapSize; |
| bestOffset = alignedStart; |
| } |
| } |
| gapStart = std::max(gapStart, occEnd); |
| } |
| |
| // Check the trailing gap (between last occupied region and arena end). |
| // This is only a reuse candidate if the allocation fits within the current |
| // arena bounds. If it doesn't fit, placing here would extend the arena — |
| // that case is handled by the fallback below (bestOffset < 0). |
| int64_t alignedTrailing = alignOffset(gapStart, alloc.alignment); |
| if (alignedTrailing + alloc.sizeInBytes <= arenaEnd) { |
| int64_t trailingSize = arenaEnd - alignedTrailing; |
| if (trailingSize < bestGapSize) { |
| bestGapSize = trailingSize; |
| bestOffset = alignedTrailing; |
| } |
| } |
| |
| // If no existing gap worked, append at the end. |
| if (bestOffset < 0) |
| bestOffset = alignedTrailing; |
| |
| assert((arenaAlignment + bestOffset) % alloc.alignment == 0 && |
| "invalid alignment"); |
| offsets[idx] = bestOffset; |
| placements.push_back({bestOffset, alloc.sizeInBytes, alloc.timeEnd}); |
| arenaEnd = std::max(arenaEnd, bestOffset + alloc.sizeInBytes); |
| } |
| |
| return offsets; |
| } |