Skip to content
Merged
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
17 changes: 8 additions & 9 deletions fibers/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test")

cc_library(
name = "stackpool",
srcs = [
"stackpool.cc",
] + select({
"@platforms//os:linux": ["stackpool_linux.cc"],
name = "stackarena",
srcs = select({
"//platforms:linux_x86_64": ["stackarena_linux_x86_64.cc"],
}),
hdrs = ["stackpool.h"],
hdrs = ["stackarena.h"],
deps = [
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
"@abseil-cpp//absl/synchronization",
],
linkopts = ["-latomic"],
copts = ["-mcx16"],
Expand All @@ -49,10 +48,10 @@ cc_library(
)

cc_test(
name = "stackpool_test",
srcs = ["stackpool_test.cc"],
name = "stackarena_test",
srcs = ["stackarena_test.cc"],
deps = [
":stackpool",
":stackarena",
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
"@abseil-cpp//absl/status:status_matchers",
Expand Down
123 changes: 123 additions & 0 deletions fibers/stackarena.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: Apache-2.0

// Copyright 2026 Adrian Gjerstad.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// -----------------------------------------------------------------------------
// loom/fibers/stackarena.h
// -----------------------------------------------------------------------------
//
// Fibers require stacks to run on, and in a performance-minded fiber
// implementation, stacks are expensive to allocate. A stack arena provides a
// single, massive, pre-allocated space in memory from which smaller stacks may
// be leased.
//
// Stacks returned as a result of `loom::StackArena::Lease()` all have the
// following properties:
//
// - Unique (system is battle-tested against race conditions involving multiple
// threads).
// - Aligned to a multiple of the system's memory page size.
// - *Never* executable.
// - Likely to be provided at an address that is advantageous for cache
// locality.
//

#ifndef LOOM_FIBERS_STACKARENA_H_
#define LOOM_FIBERS_STACKARENA_H_

#include <atomic>
#include <vector>

#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/synchronization/mutex.h"

namespace loom {

class StackArena {
public:
// Copying is disallowed, as doing so would result in double-free upon
// destruction.
StackArena(const StackArena& other) = delete;
StackArena& operator=(const StackArena& other) = delete;

// Moving is allowed.
StackArena(StackArena&& other) noexcept;
StackArena& operator=(StackArena&& other) noexcept;

// Virtual to allow for mocks and fakes.
virtual ~StackArena();

// Creates a new loom::StackArena where each stack is of the given size
// (aligned to system page boundaries, in favor of more memory).
//
// Fails if the allocator could not allocate enough memory to support
// allocation of the initial chunk of memory.
static absl::StatusOr<StackArena> Create(size_t stack_size);

// Attempts to obtain a new stack lease from the arena. Returns the pointer to
// the address of the lowest byte in the stack. May fail if the allocator
// could not allocate enough memory to support the operation.
virtual absl::StatusOr<void*> Lease();

// Marks a given stack lease as unneeded. Summarily equivalent to UNIX
// `free()`. It is undefined behavior to pass a pointer to this function that
// was not previously returned by `Lease()` or has already been passed to
// `Release()`.
virtual void Release(void* stack_base);

// Getters
size_t stack_size() const { return stack_size_; }

protected:
// Invoked by `Create()` to create the initial `StackArena`. No alignment
// checks are performed on stack_size.
explicit StackArena(size_t stack_size);

private:
// Contains metadata about a chunk acquired by the allocator.
struct Chunk {
void* memory;
size_t size;
};

// A single stack in a free list is referenced by this Node struct.
struct Node {
Node* next;
};

// The free list is accessed like this to prevent ABA races.
struct TaggedNode {
Node* node;
uintptr_t tag;
};

// Obtains a new chunk of memory for use for stacks. Will fail if the
// allocator cannot obtain enough memory.
absl::Status CreateNewChunk();

size_t stack_size_;

// This list is kept for clerical purposes, mostly cleanup at the destructor.
absl::Mutex chunk_mutex_;
std::vector<Chunk> chunks_;

alignas(16) std::atomic<TaggedNode> free_list_head_{TaggedNode{nullptr, 0}};
};

}

#endif // LOOM_FIBERS_STACKARENA_H_

187 changes: 187 additions & 0 deletions fibers/stackarena_linux_x86_64.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// SPDX-License-Identifier: Apache-2.0

// Copyright 2026 Adrian Gjerstad.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// -----------------------------------------------------------------------------
// loom/fibers/stackarena_linux_x86_64.cc
// -----------------------------------------------------------------------------
//
// Linux on x86_64 implementation of `loom::StackArena`.
//

#include "fibers/stackarena.h"

#include <errno.h>
#include <sys/mman.h>
#include <unistd.h>

#include <atomic>

#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/synchronization/mutex.h"

namespace loom {

namespace {

// Each chunk can only be so big, and we should avoid allocating a bajillion of
// them. This variable dictates how many stacks are supposed to fit into each
// chunk of contiguous memory.
constexpr size_t kStacksPerChunk = 16;

// Syscalls like `mmap` have arguments that are required to be aligned to a
// multiple of the system's page size.
const size_t kPageSize = sysconf(_SC_PAGESIZE);

}

StackArena::StackArena(StackArena&& other) noexcept
: stack_size_(other.stack_size_), chunks_(other.chunks_),
free_list_head_(other.free_list_head_.load(std::memory_order_relaxed)) {
absl::MutexLock lock(other.chunk_mutex_);
other.chunks_.clear();
other.free_list_head_.store(StackArena::TaggedNode{nullptr, 0});
}

StackArena& StackArena::operator=(StackArena&& other) noexcept {
// Make `this` safe to move into first (release chunks)
absl::MutexLock lock(chunk_mutex_);
absl::MutexLock otherlock(other.chunk_mutex_);

free_list_head_.store(StackArena::TaggedNode{nullptr, 0});
for (const auto& chunk : chunks_) {
munmap(chunk.memory, chunk.size);
}
chunks_.clear();

// Now we can perform the destructive move.
stack_size_ = other.stack_size_;
chunks_ = other.chunks_;
free_list_head_.store(other.free_list_head_.load(std::memory_order_relaxed));
other.chunks_.clear();
other.free_list_head_.store(StackArena::TaggedNode{nullptr, 0});

return *this;
}

StackArena::~StackArena() {
absl::MutexLock lock(chunk_mutex_);

free_list_head_.store(StackArena::TaggedNode{nullptr, 0});

for (const auto& chunk : chunks_) {
munmap(chunk.memory, chunk.size);
}
}

absl::StatusOr<StackArena> StackArena::Create(size_t stack_size) {
// Align stack_size to page boundary.
stack_size = (stack_size + kPageSize - 1) & ~(kPageSize - 1);

// We have to have at least one page of usable memory.
if (stack_size <= kPageSize) {
stack_size = 2 * kPageSize;
}

StackArena arena(stack_size);
auto status = arena.CreateNewChunk();
if (!status.ok()) {
return status;
}

return std::move(arena);
}

absl::StatusOr<void*> StackArena::Lease() {
StackArena::TaggedNode old_head =
free_list_head_.load(std::memory_order_acquire);

while (old_head.node != nullptr) {
// Access the 'next' pointer stored in the stack itself
// Note: Node is stored at the beginning of the usable stack memory
StackArena::Node* next_ptr =
static_cast<StackArena::Node*>(old_head.node)->next;
StackArena::TaggedNode new_head = {next_ptr, old_head.tag + 1};

if (free_list_head_.compare_exchange_weak(old_head, new_head,
std::memory_order_release,
std::memory_order_acquire)) {
// Tell the kernel that we will need this memory soon.
madvise(old_head.node, stack_size_, MADV_WILLNEED);
return static_cast<void*>(old_head.node);
}
// on failure, old_head is updated with the current value
}

// No free stacks in any current chunk
auto status = CreateNewChunk();
if (!status.ok()) {
return status;
}

return Lease();
}

void StackArena::Release(void* stack_base) {
// Allow kernel to reclaim physical memory but keep virtual reservation
madvise(stack_base, stack_size_, MADV_DONTNEED);

StackArena::Node* new_node = static_cast<StackArena::Node*>(stack_base);
StackArena::TaggedNode old_head =
free_list_head_.load(std::memory_order_acquire);
StackArena::TaggedNode new_head;

do {
new_node->next = old_head.node;
new_head = {new_node, old_head.tag + 1};
} while (!free_list_head_.compare_exchange_weak(old_head, new_head,
std::memory_order_release,
std::memory_order_acquire));
}

StackArena::StackArena(size_t stack_size) : stack_size_(stack_size) {}

absl::Status StackArena::CreateNewChunk() {
absl::MutexLock lock(chunk_mutex_);

size_t chunk_size = stack_size_ * kStacksPerChunk;

void* base = mmap(nullptr, chunk_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

if (base == MAP_FAILED) {
switch (errno) {
case EAGAIN:
case ENOMEM:
case EOVERFLOW:
return absl::ResourceExhaustedError("not enough memory for allocation");
default:
return absl::UnknownError("mmap failed unexpectedly");
}
}

// Push the new stacks into the free list
for (size_t i = 0; i < kStacksPerChunk; ++i) {
void* stack = static_cast<char*>(base) + (i * stack_size_);
Release(stack);
}

chunks_.push_back(StackArena::Chunk{base, chunk_size});
return absl::OkStatus();
}

}

Loading
Loading