Skip to content

Commit 13e19b2

Browse files
committed
vm+gc: Clean up GC and VM logic
Also added gc python module to control GC
1 parent 853ddbe commit 13e19b2

17 files changed

Lines changed: 410 additions & 255 deletions

integration/run_python_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
66

77
PYTHON_EXECUTABLE=$1
8-
GC_FREQUENCY=100000
8+
GC_FREQUENCY=${GC_FREQUENCY:-100000}
99

1010
# start by calling the tests that we need to work in order to trust the result of the other python tests
1111
if timeout 10s $PYTHON_EXECUTABLE $SCRIPT_DIR/tests/lemmas/assert_false.py --gc-frequency $GC_FREQUENCY &> /dev/null; then

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ set(RUNTIME_SOURCE_FILES
144144
runtime/modules/struct/module.cpp
145145
runtime/modules/SysModule.cpp
146146
runtime/modules/WarningsModule.cpp
147+
runtime/modules/GcModule.cpp
147148
runtime/types/builtin.cpp
148149
runtime/warnings/DeprecationWarning.cpp
149150
runtime/warnings/ImportWarning.cpp

src/executable/bytecode/Bytecode.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,14 @@ PyResult<Value> Bytecode::call_without_setup(VirtualMachine &vm, Interpreter &in
106106
// create main stack frame
107107
ASSERT(!vm.stack().empty());
108108

109-
constexpr auto sentinel = decltype(vm.stack().top().get().last_instruction_pointer)();
110-
if (vm.stack().top().get().last_instruction_pointer == sentinel) {
109+
constexpr auto sentinel = decltype(vm.stack().back().get().last_instruction_pointer)();
110+
if (vm.stack().back().get().last_instruction_pointer == sentinel) {
111111
// first time calling with the stack frame, so we don't have a last instruction pointer yet
112112
vm.set_instruction_pointer(begin());
113113
} else {
114114
// otherwise resume execution, by starting execution from the instruction after the last run
115115
// instruction
116-
vm.set_instruction_pointer(vm.stack().top().get().last_instruction_pointer + 1);
116+
vm.set_instruction_pointer(vm.stack().back().get().last_instruction_pointer + 1);
117117
}
118118

119119
return eval_loop(vm, interpreter);

src/interpreter/Interpreter.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ PyModule *Interpreter::get_imported_module(PyString *name) const
269269
ScopedStack::~ScopedStack()
270270
{
271271
auto &vm = VirtualMachine::the();
272-
if (!vm.stack().empty() && top_frame && &vm.stack().top().get() == top_frame.get()) {
272+
if (!vm.stack().empty() && top_frame && &vm.stack().back().get() == top_frame.get()) {
273273
vm.pop_frame(true);
274274
}
275275
}

src/memory/GarbageCollector.cpp

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -59,27 +59,38 @@ __attribute__((no_sanitize_address)) std::stack<Cell *> collect_roots_on_the_sta
5959
{
6060
std::stack<Cell *> roots;
6161

62-
// push register values onto the stack
62+
// Spill all callee-saved registers into `jump_buffer`. setjmp captures
63+
// them as part of saving the calling environment.
6364
std::jmp_buf jump_buffer;
6465
setjmp(jump_buffer);
6566

66-
// traverse the stack from the stack pointer to the stack origin/bottom
67-
// uintptr_t *rsp_;
68-
// asm volatile("movq %%rsp, %0" : "=r"(rsp_));
67+
auto scan_range = [&](uint8_t *begin, uint8_t *end) {
68+
for (uint8_t *p = begin; p < end; p += sizeof(uintptr_t)) {
69+
uint8_t *address =
70+
bit_cast_without_sanitizer<uint8_t *>(*bit_cast_without_sanitizer<uintptr_t *>(p))
71+
- sizeof(GarbageCollected);
72+
spdlog::trace("checking address {}, pointer address={}", (void *)address, (void *)p);
73+
if (heap.slab().has_address(address)) {
74+
spdlog::trace("valid address {}", (void *)address);
75+
auto *obj_header = bit_cast<GarbageCollected *>(address);
76+
add_root(obj_header, roots);
77+
}
78+
}
79+
};
6980

70-
// uint8_t *rsp = bit_cast<uint8_t *>(rsp_);
81+
// jump_buffer is a local variable, so it sits below the current frame
82+
// pointer on stacks that grow downward. The frame-pointer-to-stack-bottom
83+
// scan below therefore skips it, which would lose any GC root whose only
84+
// live reference is in a callee-saved register at this point. Scan the
85+
// buffer's bytes explicitly to recover those spilled values.
86+
auto *jb_begin = bit_cast_without_sanitizer<uint8_t *>(&jump_buffer);
87+
scan_range(jb_begin, jb_begin + sizeof(jump_buffer));
88+
89+
// Traverse the stack from the current frame pointer up to the recorded
90+
// stack bottom; this covers everything in our callers' frames.
7191
uint8_t *rsp = bit_cast_without_sanitizer<uint8_t *>(__builtin_frame_address(0));
72-
for (; rsp < stack_bottom; rsp += sizeof(uintptr_t)) {
73-
uint8_t *address =
74-
bit_cast_without_sanitizer<uint8_t *>(*bit_cast_without_sanitizer<uintptr_t *>(rsp))
75-
- sizeof(GarbageCollected);
76-
spdlog::trace("checking address {}, pointer address={}", (void *)address, (void *)rsp);
77-
if (heap.slab().has_address(address)) {
78-
spdlog::trace("valid address {}", (void *)address);
79-
auto *obj_header = bit_cast<GarbageCollected *>(address);
80-
add_root(obj_header, roots);
81-
}
82-
}
92+
scan_range(rsp, stack_bottom);
93+
8394
spdlog::debug("Done collecting roots from the stack, found {} roots", roots.size());
8495
return roots;
8596
}
@@ -149,20 +160,27 @@ struct MarkGCVisitor : Cell::Visitor
149160
Heap &m_heap;
150161
std::stack<Cell *> &m_to_visit;
151162

163+
// Collects the 1-hop neighbours of a given cell. visit_graph(visitor) on
164+
// the root invokes Visitor::visit() for each thing the root references
165+
// (which, by convention, may include the root itself for leaf cells); the
166+
// `m_collecting` gate isolates those nested calls so only direct
167+
// neighbours land in the vector and we do not recurse further.
152168
struct NeighbourVisitor : Cell::Visitor
153169
{
154170
std::vector<Cell *> m_neighbours;
155-
size_t m_depth{ 0 };
171+
bool m_collecting{ false };
156172

157-
void visit(Cell &cell)
173+
void collect_neighbours_of(Cell &root)
158174
{
159-
m_depth++;
160-
if (m_depth == 1) {
161-
cell.visit_graph(*this);
162-
} else if (m_depth == 2) {
163-
m_neighbours.push_back(&cell);
164-
}
165-
m_depth--;
175+
m_neighbours.clear();
176+
m_collecting = true;
177+
root.visit_graph(*this);
178+
m_collecting = false;
179+
}
180+
181+
void visit(Cell &cell) override
182+
{
183+
if (m_collecting) { m_neighbours.push_back(&cell); }
166184
}
167185
};
168186

@@ -174,7 +192,7 @@ struct MarkGCVisitor : Cell::Visitor
174192

175193
if (!is_static_memory(cell_start, m_heap)) {
176194
NeighbourVisitor nv{};
177-
nv.visit(cell);
195+
nv.collect_neighbours_of(cell);
178196
auto &neighbours = nv.m_neighbours;
179197
spdlog::trace("node: {}", static_cast<void *>(&cell));
180198
for (auto *neighbour : neighbours) {
@@ -212,8 +230,15 @@ struct MarkGCVisitor : Cell::Visitor
212230

213231
void MarkSweepGC::mark_all_cell_unreachable(Heap &heap) const
214232
{
215-
// TODO: once the ideal block sizes are fixed there should be an iterator
216-
// returning a list of all blocks
233+
// NOTE: this site intentionally does NOT use Slab::for_each_block. Doing
234+
// so produces a stack layout (lambda captures across inlining) that
235+
// keeps a Block pointer alive in a slot the next collect_roots scan
236+
// reads as a heap root, regressing
237+
// TestHeap.GarbageCollectorDeallocatesGCPointersWhenStackFrameIsPopped.
238+
// The conservative-GC fragility this exposes is the underlying issue;
239+
// until it is replaced by a precise RootSet, keep the explicit array
240+
// here so the test stays green. The other sweep/has_address sites are
241+
// fine because they run after the scan, not before.
217242
std::array blocks = {
218243
std::reference_wrapper{ heap.slab().block_16() },
219244
std::reference_wrapper{ heap.slab().block_32() },
@@ -262,21 +287,8 @@ void MarkSweepGC::sweep(Heap &heap) const
262287
{
263288
spdlog::trace("MarkSweepGC::sweep start");
264289

265-
// TODO: once the ideal block sizes are fixed there should be an iterator
266-
// returning a list of all blocks
267-
std::array blocks = {
268-
std::reference_wrapper{ heap.slab().block_16() },
269-
std::reference_wrapper{ heap.slab().block_32() },
270-
std::reference_wrapper{ heap.slab().block_64() },
271-
std::reference_wrapper{ heap.slab().block_128() },
272-
std::reference_wrapper{ heap.slab().block_256() },
273-
std::reference_wrapper{ heap.slab().block_512() },
274-
std::reference_wrapper{ heap.slab().block_1024() },
275-
std::reference_wrapper{ heap.slab().block_2048() },
276-
};
277-
// sweep all the dead objects
278-
for (const auto &block : blocks) {
279-
for (auto &chunk : block.get()->chunks()) {
290+
heap.slab().for_each_block([this, &heap](Block &block) {
291+
for (auto &chunk : block.chunks()) {
280292
chunk.for_each_cell_alive([this, &chunk, &heap](uint8_t *memory) {
281293
auto *header = bit_cast<GarbageCollected *>(memory);
282294
if (header->white()) {
@@ -293,27 +305,29 @@ void MarkSweepGC::sweep(Heap &heap) const
293305
}
294306
});
295307
}
296-
}
308+
});
297309
spdlog::trace("MarkSweepGC::sweep done");
298310
}
299311

300312

301-
void MarkSweepGC::run(Heap &heap) const
313+
void MarkSweepGC::mark_and_sweep(Heap &heap)
302314
{
303-
if (m_pause) { return; }
304-
if (++m_iterations_since_last_sweep < m_frequency) { return; }
305-
306315
mark_all_cell_unreachable(heap);
307-
308316
auto roots = collect_roots(heap);
309-
310317
mark_all_live_objects(heap, std::move(roots));
311-
312318
sweep(heap);
313-
314319
m_iterations_since_last_sweep = 0;
315320
}
316321

322+
void MarkSweepGC::run(Heap &heap)
323+
{
324+
if (m_pause) { return; }
325+
if (++m_iterations_since_last_sweep < m_frequency) { return; }
326+
mark_and_sweep(heap);
327+
}
328+
329+
void MarkSweepGC::force_run(Heap &heap) { mark_and_sweep(heap); }
330+
317331
void MarkSweepGC::resume()
318332
{
319333
ASSERT(!is_active());

src/memory/GarbageCollector.hpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,15 @@ class GarbageCollector
6464
{
6565
public:
6666
virtual ~GarbageCollector() = default;
67-
virtual void run(Heap &) const = 0;
67+
// Not const: a GC pass advances the collector's per-call cadence
68+
// counter, flips every cell's mark colour, and may destroy a large
69+
// chunk of the heap. Pretending the GC instance is immutable across
70+
// the call is a const-correctness lie that this signature avoids.
71+
virtual void run(Heap &) = 0;
72+
// Unconditional collection. Bypasses both the pause flag and the
73+
// cadence counter; used by gc.collect() so user code can force a
74+
// full sweep even while the GC has been disabled.
75+
virtual void force_run(Heap &) = 0;
6876
virtual void resume() = 0;
6977
virtual void pause() = 0;
7078
virtual bool is_active() const = 0;
@@ -81,7 +89,8 @@ class MarkSweepGC : public GarbageCollector
8189
{
8290
public:
8391
MarkSweepGC();
84-
void run(Heap &) const override;
92+
void run(Heap &) override;
93+
void force_run(Heap &) override;
8594
void resume() override;
8695
void pause() override;
8796
bool is_active() const override;
@@ -92,7 +101,11 @@ class MarkSweepGC : public GarbageCollector
92101
void sweep(Heap &heap) const;
93102

94103
private:
104+
void mark_and_sweep(Heap &);
105+
106+
// Lazily initialised once on first collect_roots; afterwards immutable.
107+
// `mutable` is the textbook one-time-cache use case.
95108
mutable uint8_t *m_stack_bottom{ nullptr };
96-
mutable size_t m_iterations_since_last_sweep{ 0 };
109+
size_t m_iterations_since_last_sweep{ 0 };
97110
bool m_pause{ false };
98111
};

src/memory/GarbageCollector_tests.cpp

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,52 @@ TEST_F(TestHeap, GarbageCollectorDeallocatesGCPointersWhenStackFrameIsPopped)
7777
m_heap->collect_garbage();
7878

7979
ASSERT_EQ(g_counter, 5);
80-
}
80+
}
81+
82+
namespace {
83+
84+
struct Cycle : Cell
85+
{
86+
Cycle *other{ nullptr };
87+
int64_t &counter;
88+
explicit Cycle(int64_t &counter_) : counter(counter_) {}
89+
~Cycle() { counter++; }
90+
std::string to_string() const override { return "Cycle"; }
91+
void visit_graph(Visitor &visitor) override
92+
{
93+
visitor.visit(*this);
94+
if (other) { visitor.visit(*other); }
95+
}
96+
};
97+
98+
// Allocates two mutually-referencing Cycle objects in a popped frame so
99+
// the conservative stack scan can't keep them alive past return.
100+
#if defined(__clang__)
101+
__attribute__((noinline, optnone)) void allocate_cycle_in_popped_frame(Heap &heap, int64_t &counter)
102+
#elif defined(__GNUC__)
103+
__attribute__((noinline, optimize("-O0"))) void allocate_cycle_in_popped_frame(Heap &heap,
104+
int64_t &counter)
105+
#endif
106+
{
107+
auto *a = heap.allocate<Cycle>(counter);
108+
auto *b = heap.allocate<Cycle>(counter);
109+
a->other = b;
110+
b->other = a;
111+
}
112+
113+
}// namespace
114+
115+
TEST_F(TestHeap, MutuallyReferencingObjectsAreCollected)
116+
{
117+
// Two Cycle objects pointing at each other should be collected once
118+
// no external roots reach them — this is the canonical mark-sweep
119+
// reachability test that a refcount-only GC would fail.
120+
int64_t counter = 0;
121+
m_heap->garbage_collector().set_frequency(1);
122+
123+
allocate_cycle_in_popped_frame(*m_heap, counter);
124+
125+
m_heap->collect_garbage();
126+
127+
ASSERT_EQ(counter, 2);
128+
}

src/memory/Heap.cpp

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -137,24 +137,17 @@ void Block::deallocate(uint8_t *ptr)
137137

138138
bool Slab::has_address(uint8_t *address) const
139139
{
140-
// TODO: once the ideal block sizes are fixed there should be an iterator
141-
// returning a list of all blocks
142-
std::array blocks = {
143-
block16.get(),
144-
block32.get(),
145-
block64.get(),
146-
block128.get(),
147-
block256.get(),
148-
block512.get(),
149-
block1024.get(),
150-
block2048.get(),
151-
};
152-
for (const auto &block : blocks) {
153-
for (auto &chunk : block->chunks()) {
154-
if (chunk.has_address(address)) { return true; }
140+
bool found = false;
141+
for_each_block([&](const Block &block) {
142+
if (found) { return; }
143+
for (const auto &chunk : block.chunks()) {
144+
if (chunk.has_address(address)) {
145+
found = true;
146+
return;
147+
}
155148
}
156-
}
157-
return false;
149+
});
150+
return found;
158151
}
159152

160153
Heap::Heap()

0 commit comments

Comments
 (0)