[K/N] Estimate allocator overhead

This increases the boundary for the next GC scheduling by how much is
estimated is currently being wasted by the allocator due to paging. This
is necessary, since the gc scheduling boundary is set based on the size
of the live set at the last GC, while the current memory use is only
tracked in whole allocator pages. This patch avoids some of the
situations where the GC would be triggered again immediately upon
completion, which happens with many sparsely filled pages.
This commit is contained in:
Troels Bjerre Lund
2023-09-25 13:31:16 +02:00
committed by Space Cloud
parent fc13dbdb91
commit a752d5d000
10 changed files with 40 additions and 2 deletions
@@ -51,6 +51,8 @@ public:
// TODO: Move into AllocatorTestSupport.hpp
void clearForTests() noexcept;
size_t estimateOverheadPerThread() noexcept;
private:
std::unique_ptr<Impl> impl_;
};
@@ -6,6 +6,7 @@
#include "AllocatorImpl.hpp"
#include "GCApi.hpp"
#include "Heap.hpp"
using namespace kotlin;
@@ -50,6 +51,10 @@ void alloc::Allocator::clearForTests() noexcept {
impl_->heap().ClearForTests();
}
size_t alloc::Allocator::estimateOverheadPerThread() noexcept {
return impl_->heap().EstimateOverheadPerThread();
}
void alloc::initObjectPool() noexcept {}
void alloc::compactObjectPoolInCurrentThread() noexcept {}
@@ -116,6 +116,16 @@ std::vector<ObjHeader*> Heap::GetAllocatedObjects() noexcept {
return unfinalized;
}
size_t Heap::EstimateOverheadPerThread() noexcept {
size_t overHeadPerThread = 0;
if (!nextFitPages_.isEmpty()) overHeadPerThread += NEXT_FIT_PAGE_SIZE;
for (int blockSize = 0; blockSize <= FIXED_BLOCK_PAGE_MAX_BLOCK_SIZE; ++blockSize) {
if (!fixedBlockPages_[blockSize].isEmpty()) overHeadPerThread += FIXED_BLOCK_PAGE_SIZE;
}
if (!extraObjectPages_.isEmpty()) overHeadPerThread += EXTRA_OBJECT_PAGE_SIZE;
return overHeadPerThread;
}
void Heap::ClearForTests() noexcept {
for (int blockSize = 0; blockSize <= FIXED_BLOCK_PAGE_MAX_BLOCK_SIZE; ++blockSize) {
fixedBlockPages_[blockSize].ClearForTests();
@@ -39,6 +39,7 @@ public:
void AddToFinalizerQueue(FinalizerQueue queue) noexcept;
FinalizerQueue ExtractFinalizerQueue() noexcept;
size_t EstimateOverheadPerThread() noexcept;
// Test method
std::vector<ObjHeader*> GetAllocatedObjects() noexcept;
@@ -78,6 +78,13 @@ public:
return page;
}
bool isEmpty() noexcept {
return empty_.isEmpty() &&
ready_.isEmpty() &&
used_.isEmpty() &&
unswept_.isEmpty();
}
~PageStore() noexcept {
T* page;
while ((page = empty_.Pop())) page->Destroy();
@@ -51,6 +51,10 @@ void alloc::Allocator::clearForTests() noexcept {
impl_->objectFactory().ClearForTests();
}
size_t alloc::Allocator::estimateOverheadPerThread() noexcept {
return 0;
}
gc::GC::ObjectData& alloc::objectDataForObject(ObjHeader* object) noexcept {
return ObjectFactoryImpl::NodeRef::From(object).ObjectData();
}
@@ -176,8 +176,10 @@ void gc::ConcurrentMarkAndSweep::PerformFullGC(int64_t epoch) noexcept {
// By this point all the alive heap must be marked.
// All the mutations (incl. allocations) after this method will be subject for the next GC.
// This should really be done by each individual thread while waiting
int threadCount = 0;
for (auto& thread : kotlin::mm::ThreadRegistry::Instance().LockForIter()) {
thread.allocator().prepareForGC();
++threadCount;
}
allocator_.prepareForGC();
@@ -206,7 +208,7 @@ void gc::ConcurrentMarkAndSweep::PerformFullGC(int64_t epoch) noexcept {
}
finalizerQueue.TransferAllFrom(allocator_.impl().heap().ExtractFinalizerQueue());
#endif
scheduler.onGCFinish(epoch, gcHandle.getKeptSizeBytes());
scheduler.onGCFinish(epoch, gcHandle.getKeptSizeBytes() + threadCount * allocator_.estimateOverheadPerThread());
state_.finish(epoch);
gcHandle.finalizersScheduled(finalizerQueue.size());
gcHandle.finished();
@@ -71,8 +71,10 @@ void gc::SameThreadMarkAndSweep::PerformFullGC(int64_t epoch) noexcept {
gc::processWeaks<DefaultProcessWeaksTraits>(gcHandle, mm::SpecialRefRegistry::instance());
// This should really be done by each individual thread while waiting
int threadCount = 0;
for (auto& thread : kotlin::mm::ThreadRegistry::Instance().LockForIter()) {
thread.allocator().prepareForGC();
++threadCount;
}
allocator_.prepareForGC();
@@ -96,7 +98,7 @@ void gc::SameThreadMarkAndSweep::PerformFullGC(int64_t epoch) noexcept {
finalizerQueue.TransferAllFrom(allocator_.impl().heap().ExtractFinalizerQueue());
#endif
scheduler.onGCFinish(epoch, gcHandle.getKeptSizeBytes());
scheduler.onGCFinish(epoch, gcHandle.getKeptSizeBytes() + threadCount * allocator_.estimateOverheadPerThread());
resumeTheWorld(gcHandle);
@@ -64,6 +64,7 @@ public:
void onGCStart() noexcept {
regularIntervalPacer_.OnPerformFullGC();
heapGrowthController_.unsetBoundaries();
timer_.restart(config_.regularGcInterval());
}
@@ -31,6 +31,10 @@ public:
triggerHeapBytes_ = targetHeapBytes_ * config_.heapTriggerCoefficient.load(std::memory_order_relaxed);
}
void unsetBoundaries() noexcept {
targetHeapBytes_ = triggerHeapBytes_ = std::numeric_limits<int64_t>::max();
}
// Can be called by any thread.
MemoryBoundary boundaryForHeapSize(size_t totalAllocatedBytes) noexcept {
if (totalAllocatedBytes >= targetHeapBytes_) {