Skip to content

Commit ffc40d4

Browse files
Riley Dulinfacebook-github-bot
authored andcommitted
Implement sequential collection for HadesGC
Summary: Implement allocation into young gen, evacuation of young gen, and collection of old gen. All of these are implemented sequentially right now, to get the basic structure working and tests passing. Currently only marking and sweeping are done in the old gen, no compaction is done at all. Concurrency will be added in later stages. It is expected that there will probably be some major changes to data structures and algorithms used during those changes. For those reasons I have left some performance-related work as "TODO". Currently this GC is not yet suitable to be activated, it is 2x slower on v8-splay (probably mostly from the naive free-list implementation), and is missing some fringe functionality (snapshots, tripwire, clearing DPM, etc.). Reviewed By: davedets Differential Revision: D20241740 fbshipit-source-id: 685eaa1e7bb9ad255e1b2a61efed270ddd29e3d3
1 parent 250f3a5 commit ffc40d4

16 files changed

Lines changed: 1424 additions & 28 deletions

include/hermes/VM/AlignedHeapSegment.h

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class GCGeneration;
5555
///
5656
/// The tables in (1), and (2) cover the contiguous allocation space (3)
5757
/// into which GCCells are bump allocated.
58-
class AlignedHeapSegment final {
58+
class AlignedHeapSegment {
5959
friend CompactionResult::Allocator;
6060
friend CompactionResult::Chunk;
6161

@@ -88,6 +88,16 @@ class AlignedHeapSegment final {
8888
friend class AlignedHeapSegment;
8989

9090
CardTable cardTable_;
91+
92+
#ifdef HERMESVM_GC_HADES
93+
// We use a MarkBitArray to mark the start of cells. Scan backwards from a
94+
// card's address in this array to find a marked bit, then go from there to
95+
// the actual cell that crosses the boundary.
96+
// If every cell is alive after marking, then markBitArray_ has bitwise
97+
// identical contents to startOfCells_.
98+
MarkBitArrayNC startOfCells_;
99+
#endif
100+
91101
MarkBitArrayNC markBitArray_;
92102

93103
/// Memory made inaccessible through protectGuardPage, for security and
@@ -236,6 +246,18 @@ class AlignedHeapSegment final {
236246
/// managed by this segment.
237247
inline MarkBitArrayNC &markBitArray() const;
238248

249+
#ifdef HERMESVM_GC_HADES
250+
/// Return a reference to the heads of objects. This needs to be updated
251+
/// whenever a new object is allocated.
252+
MarkBitArrayNC &cellHeads() {
253+
return contents()->startOfCells_;
254+
}
255+
256+
const MarkBitArrayNC &cellHeads() const {
257+
return contents()->startOfCells_;
258+
}
259+
#endif
260+
239261
explicit inline operator bool() const;
240262

241263
/// \return \c true if and only if \p ptr is within the memory range owned by

include/hermes/VM/CellKinds.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
CELL_KIND(Uninitialized)
3030
CELL_KIND(FillerCell)
31+
CELL_KIND(Freelist)
3132
CELL_KIND(DynamicUTF16StringPrimitive)
3233
CELL_KIND(DynamicASCIIStringPrimitive)
3334
CELL_KIND(BufferedUTF16StringPrimitive)

include/hermes/VM/GCCell.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ class GCCell {
5656
public:
5757
explicit GCCell(GC *gc, const VTable *vtp);
5858

59+
/// Makes a new GCCell with only a type and a size.
60+
/// NOTE: This bypasses some debugging checks in the GCCell constructor taking
61+
/// a GC parameter, so this should only be used in cases where the debug
62+
/// checks will be wrong.
63+
explicit GCCell(const VTable *vtp);
64+
5965
// GCCell-s are not copyable (in the C++ sense).
6066
GCCell(const GCCell &) = delete;
6167
void operator=(const GCCell &) = delete;
@@ -262,6 +268,16 @@ class VariableSizeRuntimeCell : public GCCell {
262268
"the size of a cell");
263269
}
264270

271+
/// Makes a new VariableSizeRuntimeCell with only a type and a size.
272+
VariableSizeRuntimeCell(const VTable *vtp, uint32_t size)
273+
: GCCell(vtp), variableSize_(heapAlignSize(size)) {
274+
// Need to align to the GC here, since the GC doesn't know about this field.
275+
assert(
276+
size >= sizeof(VariableSizeRuntimeCell) &&
277+
"Should not allocate a VariableSizeRuntimeCell of size less than "
278+
"the size of a cell");
279+
}
280+
265281
public:
266282
uint32_t getSize() const {
267283
return variableSize_;
@@ -289,6 +305,8 @@ static_assert(
289305

290306
#ifdef NDEBUG
291307
inline GCCell::GCCell(GC *, const VTable *vtp) : vtp_(vtp) {}
308+
309+
inline GCCell::GCCell(const VTable *vtp) : vtp_(vtp) {}
292310
#endif
293311

294312
inline uint32_t GCCell::getAllocatedSize(const VTable *vtp) const {

include/hermes/VM/GenGCNC.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ class GenGC final : public GCBase {
252252
/// result of this collection.
253253
void collect(bool canEffectiveOOM = false);
254254

255+
static constexpr uint32_t minAllocationSize() {
256+
// NCGen doesn't enforce a minimum allocation requirement.
257+
return 0;
258+
}
259+
255260
static constexpr uint32_t maxAllocationSize() {
256261
// The largest allocation allowable in NCGen is the max size a single
257262
// segment supports.

include/hermes/VM/HadesGC.h

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#include "hermes/VM/GCBase.h"
1313

1414
#include <cstdint>
15+
#include <deque>
16+
#include <memory>
17+
#include <vector>
1518

1619
namespace hermes {
1720
namespace vm {
@@ -55,6 +58,8 @@ class HadesGC final : public GCBase {
5558

5659
~HadesGC();
5760

61+
static uint32_t minAllocationSize();
62+
5863
static constexpr uint32_t maxAllocationSize() {
5964
// The largest allocation allowable in Hades is the max size a single
6065
// segment supports.
@@ -64,9 +69,11 @@ class HadesGC final : public GCBase {
6469
/// \name GCBase overrides
6570
/// \{
6671

72+
void getHeapInfo(HeapInfo &info) override;
6773
void getHeapInfoWithMallocSize(HeapInfo &info) override;
6874
void getCrashManagerHeapInfo(CrashManager::HeapInformation &info) override;
6975
void createSnapshot(llvm::raw_ostream &os) override;
76+
void printStats(llvm::raw_ostream &os, bool trailingComma) override;
7077

7178
/// \}
7279

@@ -89,7 +96,7 @@ class HadesGC final : public GCBase {
8996
/// \tparam hasFinalizer Indicates whether the object being allocated will
9097
/// have a finalizer.
9198
template <HasFinalizer hasFinalizer = HasFinalizer::No>
92-
inline void *allocLongLived(uint32_t size);
99+
inline void *allocLongLived(uint32_t sz);
93100

94101
/// Force a garbage collection cycle.
95102
/// (Part of general GC API defined in GCBase.h).
@@ -141,6 +148,9 @@ class HadesGC final : public GCBase {
141148

142149
/// \}
143150

151+
/// \return true if the pointer lives in the young generation.
152+
bool inYoungGen(const void *p) const;
153+
144154
#ifndef NDEBUG
145155
/// \name Debug APIs
146156
/// \{
@@ -168,20 +178,150 @@ class HadesGC final : public GCBase {
168178
/// \}
169179
#endif
170180

181+
class HeapSegment;
182+
class CollectionSection;
183+
class EvacAcceptor;
184+
class MarkAcceptor;
185+
class WeakRootAcceptor;
186+
171187
private:
188+
const uint64_t maxHeapSize_;
189+
190+
/// Keeps the storage provider alive until after the GC is fully destructed.
191+
std::shared_ptr<StorageProvider> provider_;
192+
193+
/// youngGen is a bump-pointer space, so it can re-use AlignedHeapSegment.
194+
std::unique_ptr<HeapSegment> youngGen_;
195+
/// List of cells in YG that have finalizers. Iterate through this to clean
196+
/// them out.
197+
std::vector<GCCell *> youngGenFinalizables_;
198+
199+
/// oldGen_ is a free list space, so it needs a different segment
200+
/// representation.
201+
std::vector<std::unique_ptr<HeapSegment>> oldGen_;
202+
203+
/// weakPointers_ is a list of all the weak pointers in the system. They are
204+
/// invalidated if they point to an object that is dead, and do not count
205+
/// towards whether an object is live or dead.
206+
std::deque<WeakRefSlot> weakPointers_;
207+
208+
/// The main entrypoint for all allocations.
209+
/// \param sz The size of allocation requested. This might be rounded up to
210+
/// fit heap alignment requirements.
211+
/// \param longLived If true, allocate directly into OG, instead of YG.
212+
/// \param fixedSize If true, the allocation is of a cell type that always has
213+
/// the same size. The requirement enforced by Hades is that all fixed-size
214+
/// allocations must go into YG, unless \p longLived is also true.
215+
/// \param hasFinalizer If true, the cell about to be allocated into the
216+
/// requested space will have a finalizer that the GC will need to invoke.
217+
void *allocWork(
218+
uint32_t sz,
219+
bool longLived,
220+
bool fixedSize,
221+
HasFinalizer hasFinalizer);
222+
223+
/// Allocate into OG. Returns a pointer to the newly allocated space. That
224+
/// space must be filled immediately after this call completes.
225+
/// \return A non-null pointer to memory in the old gen that should have a
226+
/// constructor run in immediately.
227+
/// \post This function either successfully allocates, or reports OOM.
228+
GCCell *oldGenAlloc(uint32_t sz);
229+
230+
/// Searches the OG for a space to allocate memory into.
231+
/// \return A pointer to uninitialized memory that can be written into, null
232+
/// if no such space exists.
233+
/// NOTE: oldGenAlloc should be called instead, which will try to do
234+
/// collections until this function returns a non-null pointer.
235+
GCCell *oldGenSearch(uint32_t sz);
236+
237+
/// Frees the weak slot, so it can be re-used by future WeakRef allocations.
238+
void freeWeakSlot(WeakRefSlot *slot);
239+
240+
/// Perform a YG garbage collection. All live objects in YG will be evacuated
241+
/// to the OG.
242+
/// \post The YG is completely empty, and all bytes are available for new
243+
/// allocations.
244+
void youngGenCollection(bool allowOGBegin);
245+
246+
/// Perform an OG garbage collection. All live objects in OG will be left
247+
/// untouched, all unreachable objects will be placed into a free list that
248+
/// can be used by \c oldGenAlloc.
249+
void oldGenCollection();
250+
251+
/// Find all pointers from OG into YG during a YG collection. This is done
252+
/// quickly through use of write barriers that detect the creation of OG-to-YG
253+
/// pointers.
254+
void scanDirtyCards(EvacAcceptor &acceptor);
255+
256+
/// Finalize all objects in YG that have finalizers.
257+
void finalizeYoungGenObjects();
258+
259+
/// Update all of the weak references and invalidate the ones that point to
260+
/// dead objects.
261+
void updateWeakReferencesForYoungGen();
262+
263+
/// Update all of the weak references, invalidate the ones that point to
264+
/// dead objects, and free the ones that were not marked at all.
265+
void updateWeakReferencesForOldGen();
266+
267+
/// The WeakMap type in JS has special semantics for handling keys kept alive
268+
/// by only their values. In between marking and sweeping, this function is
269+
/// called to handle that special case.
270+
void completeWeakMapMarking(MarkAcceptor &acceptor);
271+
272+
/// Sets all weak references to unmarked in preparation for a collection.
273+
void resetWeakReferences();
274+
275+
/// Return the total number of bytes that are in use by the JS heap.
276+
uint64_t allocatedBytes();
277+
278+
/// Return the total number of bytes that are in use by the OG section of the
279+
/// JS heap.
280+
uint64_t oldGenAllocatedBytes();
281+
282+
/// Accessor for the YG.
283+
HeapSegment &youngGen();
284+
const HeapSegment &youngGen() const;
285+
286+
/// Accessors for the segments of OG.
287+
std::vector<std::unique_ptr<HeapSegment>>::iterator oldGenBegin();
288+
std::vector<std::unique_ptr<HeapSegment>>::const_iterator oldGenBegin() const;
289+
290+
std::vector<std::unique_ptr<HeapSegment>>::iterator oldGenEnd();
291+
std::vector<std::unique_ptr<HeapSegment>>::const_iterator oldGenEnd() const;
292+
293+
/// Create a new OG segment and attach it to the end of the OG segment vector.
294+
/// \return a reference to the newly created segment.
295+
HeapSegment &createOldGenSegment();
296+
297+
/// Searches the old gen for this pointer. This is O(number of OG segments).
298+
/// NOTE: In any non-debug case, \c inYoungGen should be used instead, because
299+
/// it is O(1).
300+
/// \return true if the pointer is in the old gen.
301+
bool inOldGen(const void *p) const;
302+
303+
#ifdef HERMES_SLOW_DEBUG
304+
/// Checks the heap to make sure all cells are valid.
305+
void checkWellFormed();
306+
307+
/// Verify that the card table used to find pointers from OG into YG has the
308+
/// correct cards dirtied, given the contents of the OG currently.
309+
void verifyCardTable();
310+
void verifyCardTableBoundaries() const;
311+
#endif
172312
};
173313

174314
/// \name Inline implementations
175315
/// \{
176316

177317
template <bool fixedSize, HasFinalizer hasFinalizer>
178318
void *HadesGC::alloc(uint32_t sz) {
179-
return nullptr;
319+
return allocWork(sz, false, fixedSize, hasFinalizer);
180320
}
181321

182322
template <HasFinalizer hasFinalizer>
183-
void *HadesGC::allocLongLived(uint32_t size) {
184-
return nullptr;
323+
void *HadesGC::allocLongLived(uint32_t sz) {
324+
return allocWork(sz, true, false, hasFinalizer);
185325
}
186326

187327
/// \}

include/hermes/VM/MallocGC.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ class MallocGC final : public GCBase {
187187
/// weak pointers that point to dead objects.
188188
void collect();
189189

190+
static constexpr uint32_t minAllocationSize() {
191+
// MallocGC imposes no limit on individual allocations.
192+
return 0;
193+
}
194+
190195
static constexpr uint32_t maxAllocationSize() {
191196
// MallocGC imposes no limit on individual allocations.
192197
return std::numeric_limits<uint32_t>::max();

include/hermes/VM/StringPrimitive.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,11 @@ class DynamicStringPrimitive final
484484
/// Calculate the allocation size of a StringPrimitive given character
485485
/// length.
486486
static uint32_t allocationSize(uint32_t length) {
487-
return DynamicStringPrimitive::template totalSizeToAlloc<T>(length);
487+
// In the future it would be better to have the GC return a new size
488+
// instead of having the caller decide.
489+
return std::max(
490+
DynamicStringPrimitive::template totalSizeToAlloc<T>(length),
491+
static_cast<size_t>(GC::minAllocationSize()));
488492
}
489493

490494
const T *getRawPointer() const {

lib/VM/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,15 @@ if (${HERMESVM_GCKIND} STREQUAL "NONCONTIG_GENERATIONAL")
155155
elseif (${HERMESVM_GCKIND} STREQUAL "MALLOC")
156156
list(APPEND source_files gcs/MallocGC.cpp gcs/FillerCell.cpp)
157157
elseif (${HERMESVM_GCKIND} STREQUAL "HADES")
158-
list(APPEND source_files gcs/HadesGC.cpp gcs/FillerCell.cpp)
158+
list(APPEND source_files
159+
gcs/AlignedStorage.cpp
160+
gcs/AlignedHeapSegment.cpp
161+
gcs/CardTableNC.cpp
162+
gcs/CompleteMarkState.cpp
163+
gcs/FillerCell.cpp
164+
gcs/HadesGC.cpp
165+
gcs/MarkBitArrayNC.cpp
166+
)
159167
else()
160168
message(WARNING "Not linking garbage collector")
161169
endif()

lib/VM/GCCell.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ namespace hermes {
1212
namespace vm {
1313

1414
#ifndef NDEBUG
15+
GCCell::GCCell(const VTable *vtp) : vtp_(vtp), _debugAllocationId_(0) {}
16+
1517
GCCell::GCCell(GC *gc, const VTable *vtp)
1618
: vtp_(vtp), _debugAllocationId_(gc->nextObjectID()) {
1719
// If the vtp has a finalizer, then it should be the most recent thing

lib/VM/IdentifierTable.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ std::string IdentifierTable::convertSymbolToUTF8(SymbolID id) {
233233
void IdentifierTable::markIdentifiers(SlotAcceptor &acceptor, GC *gc) {
234234
for (auto &vectorEntry : lookupVector_) {
235235
if (!vectorEntry.isFreeSlot() && vectorEntry.isStringPrim()) {
236-
#ifdef HERMESVM_GC_NONCONTIG_GENERATIONAL
236+
#if defined(HERMESVM_GC_NONCONTIG_GENERATIONAL) || defined(HERMESVM_GC_HADES)
237237
assert(
238238
!gc->inYoungGen(vectorEntry.getStringPrimRef()) &&
239239
"Identifiers must be allocated in the old gen");
@@ -510,7 +510,7 @@ CallResult<SymbolID> IdentifierTable::createNotUniquedSymbol(
510510
Handle<StringPrimitive> desc) {
511511
uint32_t nextID = allocNextID();
512512

513-
#ifdef HERMESVM_GC_NONCONTIG_GENERATIONAL
513+
#if defined(HERMESVM_GC_NONCONTIG_GENERATIONAL) || defined(HERMESVM_GC_HADES)
514514
if (runtime->getHeap().inYoungGen(desc.get())) {
515515
// Need to reallocate in the old gen if the description is in the young gen.
516516
CallResult<PseudoHandle<StringPrimitive>> longLivedStr = desc->isASCII()

0 commit comments

Comments
 (0)