Files
kotlin-fork/runtime/src/main/cpp/Memory.cpp
T

2433 lines
83 KiB
C++

/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* 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.
*/
#include <string.h>
#include <stdio.h>
#include <cstddef> // for offsetof
#include "Alloc.h"
#include "KAssert.h"
#include "Atomic.h"
#include "Exceptions.h"
#include "KString.h"
#include "Memory.h"
#include "MemoryPrivate.hpp"
#include "Natives.h"
#include "Porting.h"
#include "Runtime.h"
// If garbage collection algorithm for cyclic garbage to be used.
// We are using the Bacon's algorithm for GC, see
// http://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon03Pure.pdf.
#define USE_GC 1
// Define to 1 to print all memory operations.
#define TRACE_MEMORY 0
// Define to 1 to print major GC events.
#define TRACE_GC 0
// Collect memory manager events statistics.
#define COLLECT_STATISTIC 0
// Auto-adjust GC thresholds.
#define GC_ERGONOMICS 1
namespace {
// Granularity of arena container chunks.
constexpr container_size_t kContainerAlignment = 1024;
// Single object alignment.
constexpr container_size_t kObjectAlignment = 8;
// Required e.g. for object size computations to be correct.
static_assert(sizeof(ContainerHeader) % kObjectAlignment == 0, "sizeof(ContainerHeader) is not aligned");
#if TRACE_MEMORY
#undef TRACE_GC
#define TRACE_GC 1
#define MEMORY_LOG(...) konan::consolePrintf(__VA_ARGS__);
#else
#define MEMORY_LOG(...)
#endif
#if TRACE_GC
#define GC_LOG(...) konan::consolePrintf(__VA_ARGS__);
#else
#define GC_LOG(...)
#endif
#if USE_GC
// Collection threshold default (collect after having so many elements in the
// release candidates set).
constexpr size_t kGcThreshold = 16 * 1024;
#if GC_ERGONOMICS
// Ergonomic thresholds.
// If GC to computations time ratio is above that value,
// increase GC threshold by 1.5 times.
constexpr double kGcToComputeRatioThreshold = 0.5;
// Never exceed this value when increasing GC threshold.
constexpr size_t kMaxErgonomicThreshold = 1024 * 1024;
#endif // GC_ERGONOMICS
// Threshold of size for toFree set, triggering actual cycle collector.
constexpr size_t kMaxToFreeSize = 8 * 1024;
// How many elements in finalizer queue allowed before cleaning it up.
constexpr size_t kFinalizerQueueThreshold = 32;
#endif // USE_GC
} // namespace
typedef KStdUnorderedSet<ContainerHeader*> ContainerHeaderSet;
typedef KStdVector<ContainerHeader*> ContainerHeaderList;
typedef KStdVector<KRef*> KRefPtrList;
typedef KStdDeque<ContainerHeader*> ContainerHeaderDeque;
// A little hack that allows to enable -O2 optimizations
// Prevents clang from replacing FrameOverlay struct
// with single pointer.
// Can be removed when FrameOverlay will become more complex.
FrameOverlay exportFrameOverlay;
// Current number of allocated containers.
volatile int allocCount = 0;
volatile int aliveMemoryStatesCount = 0;
// Forward declarations.
void freeContainer(ContainerHeader* header) NO_INLINE;
#if USE_GC
void garbageCollect(MemoryState* state, bool force) NO_INLINE;
#endif // USE_GC
#if COLLECT_STATISTIC
class MemoryStatistic {
public:
// UpdateRef per-object type counters.
uint64_t updateCounters[10][10];
// Alloc per container type counters.
uint64_t containerAllocs[2];
// Free per container type counters.
uint64_t objectAllocs[5];
// Histogram of allocation size distribution.
KStdUnorderedMap<int, int>* allocationHistogram;
// Number of allocation cache hits.
int allocCacheHit;
// Number of allocation cache misses.
int allocCacheMiss;
// Number of regular reference increments.
uint64_t addRefs;
// Number of atomic reference increments.
uint64_t atomicAddRefs;
// Number of regular reference decrements.
uint64_t releaseRefs;
// Number of atomic reference decrements.
uint64_t atomicReleaseRefs;
// Number of potential cycle candidates.
uint64_t releaseCyclicRefs;
// Map of array index to human readable name.
static constexpr const char* indexToName[] = {
"normal", "stack ", "perm ", "frozen", "null " };
void init() {
memset(containerAllocs, 0, sizeof(containerAllocs));
memset(objectAllocs, 0, sizeof(objectAllocs));
memset(updateCounters, 0, sizeof(updateCounters));
allocationHistogram = konanConstructInstance<KStdUnorderedMap<int, int>>();
allocCacheHit = 0;
allocCacheMiss = 0;
}
void deinit() {
konanDestructInstance(allocationHistogram);
allocationHistogram = nullptr;
}
void incAddRef(const ContainerHeader* header, bool atomic, int stack) {
if (atomic) atomicAddRefs++; else addRefs++;
}
void incReleaseRef(const ContainerHeader* header, bool atomic, bool cyclic, int stack) {
if (atomic) {
atomicReleaseRefs++;
} else {
if (cyclic) releaseCyclicRefs++; else releaseRefs++;
}
}
void incUpdateRef(const ObjHeader* objOld, const ObjHeader* objNew, int stack) {
updateCounters[toIndex(objOld, stack)][toIndex(objNew, stack)]++;
}
void incAlloc(size_t size, const ContainerHeader* header) {
containerAllocs[0]++;
++(*allocationHistogram)[size];
}
void incFree(const ContainerHeader* header) {
containerAllocs[1]++;
}
void incAlloc(size_t size, const ObjHeader* header) {
objectAllocs[toIndex(header, 0)]++;
}
static int toIndex(const ObjHeader* obj, int stack) {
if (reinterpret_cast<uintptr_t>(obj) > 1)
return toIndex(obj->container(), stack);
else
return 4 + stack * 5;
}
static int toIndex(const ContainerHeader* header, int stack) {
if (header == nullptr) return 2 + stack * 5; // permanent.
switch (header->tag()) {
case CONTAINER_TAG_NORMAL : return 0 + stack * 5;
case CONTAINER_TAG_STACK : return 1 + stack * 5;
case CONTAINER_TAG_FROZEN: return 3 + stack * 5;
}
RuntimeAssert(false, "unknown container type");
return -1;
}
static double percents(uint64_t value, uint64_t all) {
return all == 0 ? 0 : ((double)value / (double)all) * 100.0;
}
void printStatistic() {
konan::consolePrintf("\nMemory manager statistic:\n\n");
konan::consolePrintf("Container alloc: %lld, free: %lld\n",
containerAllocs[0], containerAllocs[1]);
for (int i = 0; i < 5; i++) {
// Only normal and frozen can be allocated.
if (i == 0 || i == 3)
konan::consolePrintf("Object %s alloc: %lld\n", indexToName[i], objectAllocs[i]);
}
konan::consolePrintf("\n");
uint64_t allUpdateRefs = 0, heapUpdateRefs = 0, stackUpdateRefs = 0;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
allUpdateRefs += updateCounters[i][j];
if (i < 5 && j < 5)
heapUpdateRefs += updateCounters[i][j];
if (i >= 5 && j >= 5)
stackUpdateRefs += updateCounters[i][j];
}
}
konan::consolePrintf("Total updates: %lld, stack: %lld(%.2lf%%), heap: %lld(%.2lf%%)\n",
allUpdateRefs,
stackUpdateRefs, percents(stackUpdateRefs, allUpdateRefs),
heapUpdateRefs, percents(heapUpdateRefs, allUpdateRefs));
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (updateCounters[i][j] != 0)
konan::consolePrintf("UpdateHeapRef[%s -> %s]: %lld (%.2lf%% of all, %.2lf%% of heap)\n",
indexToName[i], indexToName[j], updateCounters[i][j],
percents(updateCounters[i][j], allUpdateRefs),
percents(updateCounters[i][j], heapUpdateRefs));
}
}
for (int i = 5; i < 10; i++) {
for (int j = 5; j < 10; j++) {
if (updateCounters[i][j] != 0)
konan::consolePrintf("UpdateStackRef[%s -> %s]: %lld (%.2lf%% of all, %.2lf%% of stack)\n",
indexToName[i - 5], indexToName[j - 5],
updateCounters[i][j],
percents(updateCounters[i][j], allUpdateRefs),
percents(updateCounters[i][j], stackUpdateRefs));
}
}
konan::consolePrintf("\n");
konan::consolePrintf("Allocation histogram:\n");
KStdVector<int> keys(allocationHistogram->size());
int index = 0;
for (auto& it : *allocationHistogram) {
keys[index++] = it.first;
}
std::sort(keys.begin(), keys.end());
int perLine = 4;
int count = 0;
for (auto it : keys) {
konan::consolePrintf(
"%d bytes -> %d times ", it, (*allocationHistogram)[it]);
if (++count % perLine == (perLine - 1) || (count == keys.size()))
konan::consolePrintf("\n");
}
uint64_t allAddRefs = addRefs + atomicAddRefs;
uint64_t allReleases = releaseRefs + atomicReleaseRefs + releaseCyclicRefs;
konan::consolePrintf("AddRefs:\t%lld/%lld (%.2lf%% of atomic)\n"
"Releases:\t%lld/%lld (%.2lf%% of atomic)\n"
"ReleaseRefs affecting cycle collector : %lld (%.2lf%% of cyclic)\n",
addRefs, atomicAddRefs, percents(atomicAddRefs, allAddRefs),
releaseRefs, atomicReleaseRefs, percents(atomicReleaseRefs, allReleases),
releaseCyclicRefs, percents(releaseCyclicRefs, allReleases));
}
};
constexpr const char* MemoryStatistic::indexToName[];
#endif // COLLECT_STATISTIC
struct MemoryState {
#if TRACE_MEMORY
// Set of all containers.
ContainerHeaderSet* containers;
#endif
#if USE_GC
// Finalizer queue - linked list of containers scheduled for finalization.
ContainerHeader* finalizerQueue;
int finalizerQueueSize;
int finalizerQueueSuspendCount;
/*
* Typical scenario for GC is as following:
* we have 90% of objects with refcount = 0 which will be deleted during
* the first phase of the algorithm.
* We could mark them with a bit in order to tell the next two phases to skip them
* and thus requiring only one list, but the downside is that both of the
* next phases would iterate over the whole list of objects instead of only 10%.
*/
ContainerHeaderList* toFree; // List of all cycle candidates.
ContainerHeaderList* roots; // Real candidates excluding those with refcount = 0.
// How many GC suspend requests happened.
int gcSuspendCount;
// How many candidate elements in toRelease shall trigger collection.
size_t gcThreshold;
// If collection is in progress.
bool gcInProgress;
// Objects to be released.
ContainerHeaderList* toRelease;
#if GC_ERGONOMICS
uint64_t lastGcTimestamp;
#endif
#endif // USE_GC
#if COLLECT_STATISTIC
#define CONTAINER_ALLOC_STAT(state, size, container) state->statistic.incAlloc(size, container);
#define CONTAINER_DESTROY_STAT(state, container) \
state->statistic.incFree(container);
#define OBJECT_ALLOC_STAT(state, size, object) \
state->statistic.incAlloc(size, object); \
state->statistic.incAddRef(object->container(), 0, 0);
#define UPDATE_REF_STAT(state, oldRef, newRef, slot, stack) \
state->statistic.incUpdateRef(oldRef, newRef, stack);
#define UPDATE_ADDREF_STAT(state, obj, atomic, stack) \
state->statistic.incAddRef(obj, atomic, stack);
#define UPDATE_RELEASEREF_STAT(state, obj, atomic, cyclic, stack) \
state->statistic.incReleaseRef(obj, atomic, cyclic, stack);
#define INIT_STAT(state) \
state->statistic.init();
#define DEINIT_STAT(state) \
state->statistic.deinit();
#define PRINT_STAT(state) \
state->statistic.printStatistic();
MemoryStatistic statistic;
#else
#define CONTAINER_ALLOC_STAT(state, size, container)
#define CONTAINER_DESTROY_STAT(state, container)
#define OBJECT_ALLOC_STAT(state, size, object)
#define UPDATE_REF_STAT(state, oldRef, newRef, slot, stack)
#define UPDATE_ADDREF_STAT(state, obj, atomic, stack)
#define UPDATE_RELEASEREF_STAT(state, obj, atomic, cyclic, stack)
#define INIT_STAT(state)
#define DEINIT_STAT(state)
#define PRINT_STAT(state)
#endif // COLLECT_STATISTIC
};
#if TRACE_MEMORY
#define INIT_TRACE(state) \
memoryState->containers = konanConstructInstance<ContainerHeaderSet>();
#define DEINIT_TRACE(state) \
konanDestructInstance(memoryState->containers); \
memoryState->containers = nullptr;
#else
#define INIT_TRACE(state)
#define DEINIT_TRACE(state)
#endif
#define CONTAINER_ALLOC_TRACE(state, size, container) \
MEMORY_LOG("Container alloc %d at %p\n", size, container)
#define CONTAINER_DESTROY_TRACE(state, container) \
MEMORY_LOG("Container destroy %p\n", container)
#define OBJECT_ALLOC_TRACE(state, size, object) \
MEMORY_LOG("Object alloc %d at %p\n", size, object)
#define UPDATE_REF_TRACE(state, oldRef, newRef, slot, stack) \
MEMORY_LOG("UpdateRef %s*%p: %p -> %p\n", stack ? "stack " : "heap ", slot, oldRef, newRef)
// Events macro definitions.
// Called on worker's memory init.
#define INIT_EVENT(state) \
INIT_STAT(state) \
INIT_TRACE(state)
// Called on worker's memory deinit.
#define DEINIT_EVENT(state) \
DEINIT_STAT(state)
// Called on container allocation.
#define CONTAINER_ALLOC_EVENT(state, size, container) \
CONTAINER_ALLOC_STAT(state, size, container) \
CONTAINER_ALLOC_TRACE(state, size, container)
// Called on container destroy (memory is released to allocator).
#define CONTAINER_DESTROY_EVENT(state, container) \
CONTAINER_DESTROY_STAT(state, container) \
CONTAINER_DESTROY_TRACE(state, container)
// Object was just allocated.
#define OBJECT_ALLOC_EVENT(state, size, object) \
OBJECT_ALLOC_STAT(state, size, object) \
OBJECT_ALLOC_TRACE(state, size, object)
// Object is freed.
#define OBJECT_FREE_EVENT(state, size, object) \
OBJECT_FREE_STAT(state, size, object) \
OBJECT_FREE_TRACE(state, object)
// Reference in memory is being updated.
#define UPDATE_REF_EVENT(state, oldRef, newRef, slot, stack) \
UPDATE_REF_STAT(state, oldRef, newRef, slot, stack) \
UPDATE_REF_TRACE(state, oldRef, newRef, slot, stack)
// Infomation shall be printed as worker is exiting.
#define PRINT_EVENT(state) \
PRINT_STAT(state)
namespace {
// Container for a single object.
class ObjectContainer : public Container {
public:
// Single instance.
explicit ObjectContainer(MemoryState* state, const TypeInfo* type_info) {
Init(state, type_info);
}
// Object container shalln't have any dtor, as it's being freed by
// ::Release().
ObjHeader* GetPlace() const {
return reinterpret_cast<ObjHeader*>(header_ + 1);
}
private:
void Init(MemoryState* state, const TypeInfo* type_info);
};
class ArrayContainer : public Container {
public:
ArrayContainer(MemoryState* state, const TypeInfo* type_info, uint32_t elements) {
Init(state, type_info, elements);
}
// Array container shalln't have any dtor, as it's being freed by ::Release().
ArrayHeader* GetPlace() const {
return reinterpret_cast<ArrayHeader*>(header_ + 1);
}
private:
void Init(MemoryState* state, const TypeInfo* type_info, uint32_t elements);
};
// Class representing arena-style placement container.
// Container is used for reference counting, and it is assumed that objects
// with related placement will share container. Only
// whole container can be freed, individual objects are not taken into account.
class ArenaContainer;
struct ContainerChunk {
ContainerChunk* next;
ArenaContainer* arena;
// Then we have ContainerHeader here.
ContainerHeader* asHeader() {
return reinterpret_cast<ContainerHeader*>(this + 1);
}
};
class ArenaContainer {
public:
void Init();
void Deinit();
// Place individual object in this container.
ObjHeader* PlaceObject(const TypeInfo* type_info);
// Places an array of certain type in this container. Note that array_type_info
// is type info for an array, not for an individual element. Also note that exactly
// same operation could be used to place strings.
ArrayHeader* PlaceArray(const TypeInfo* array_type_info, container_size_t count);
ObjHeader** getSlot();
private:
void* place(container_size_t size);
bool allocContainer(container_size_t minSize);
void setHeader(ObjHeader* obj, const TypeInfo* typeInfo) {
obj->typeInfoOrMeta_ = const_cast<TypeInfo*>(typeInfo);
obj->setContainer(currentChunk_->asHeader());
// Here we do not take into account typeInfo's immutability for ARC strategy, as there's no ARC.
}
ContainerChunk* currentChunk_;
uint8_t* current_;
uint8_t* end_;
ArrayHeader* slots_;
uint32_t slotsCount_;
};
// TODO: can we pass this variable as an explicit argument?
THREAD_LOCAL_VARIABLE MemoryState* memoryState = nullptr;
THREAD_LOCAL_VARIABLE FrameOverlay* currentFrame = nullptr;
constexpr int kFrameOverlaySlots = sizeof(FrameOverlay) / sizeof(ObjHeader**);
inline bool isFreeable(const ContainerHeader* header) {
return header != nullptr && header->tag() != CONTAINER_TAG_STACK;
}
inline bool isArena(const ContainerHeader* header) {
return header != nullptr && header->stack();
}
inline bool isAggregatingFrozenContainer(const ContainerHeader* header) {
return header != nullptr && header->frozen() && header->objectCount() > 1;
}
inline container_size_t alignUp(container_size_t size, int alignment) {
return (size + alignment - 1) & ~(alignment - 1);
}
inline ContainerHeader* realShareableContainer(ContainerHeader* container) {
RuntimeAssert(container->shareable(), "Only makes sense on shareable objects");
return reinterpret_cast<ObjHeader*>(container + 1)->container();
}
inline uint32_t arrayObjectSize(const TypeInfo* typeInfo, uint32_t count) {
// Note: array body is aligned, but for size computation it is enough to align the sum.
static_assert(kObjectAlignment % alignof(KLong) == 0, "");
static_assert(kObjectAlignment % alignof(KDouble) == 0, "");
return alignUp(sizeof(ArrayHeader) - typeInfo->instanceSize_ * count, kObjectAlignment);
}
inline uint32_t arrayObjectSize(const ArrayHeader* obj) {
return arrayObjectSize(obj->type_info(), obj->count_);
}
// TODO: shall we do padding for alignment?
inline container_size_t objectSize(const ObjHeader* obj) {
const TypeInfo* type_info = obj->type_info();
container_size_t size = (type_info->instanceSize_ < 0 ?
// An array.
arrayObjectSize(obj->array())
:
type_info->instanceSize_);
return alignUp(size, kObjectAlignment);
}
inline FrameOverlay* asFrameOverlay(ObjHeader** slot) {
return reinterpret_cast<FrameOverlay*>(slot);
}
inline bool isRefCounted(KConstRef object) {
return isFreeable(object->container());
}
inline void lock(KInt* spinlock) {
while (compareAndSwap(spinlock, 0, 1) != 0) {}
}
inline void unlock(KInt* spinlock) {
RuntimeCheck(compareAndSwap(spinlock, 1, 0) == 1, "Must succeed");
}
} // namespace
void KRefSharedHolder::initRefOwner() {
RuntimeAssert(owner_ == nullptr, "Must be uninitialized");
owner_ = memoryState;
}
void KRefSharedHolder::verifyRefOwner() const {
// Note: checking for 'shareable()' and retrieving 'type_info()'
// are supposed to be correct even for unowned object.
if (owner_ != memoryState) {
// Initialized runtime is required to throw the exception below
// or to provide proper execution context for shared objects:
if (memoryState == nullptr) Kotlin_initRuntimeIfNeeded();
auto* container = obj_->container();
if (!Shareable(container)) {
// TODO: add some info about the owner.
ThrowIllegalObjectSharingException(obj_->type_info(), obj_);
}
}
}
extern "C" {
void objc_release(void* ptr);
void Kotlin_ObjCExport_releaseAssociatedObject(void* associatedObject);
RUNTIME_NORETURN void ThrowFreezingException(KRef toFreeze, KRef blocker);
} // extern "C"
void runDeallocationHooks(ContainerHeader* container) {
ObjHeader* obj = reinterpret_cast<ObjHeader*>(container + 1);
for (int index = 0; index < container->objectCount(); index++) {
if (obj->has_meta_object()) {
ObjHeader::destroyMetaObject(&obj->typeInfoOrMeta_);
}
obj = reinterpret_cast<ObjHeader*>(
reinterpret_cast<uintptr_t>(obj) + objectSize(obj));
}
}
void DeinitInstanceBody(const TypeInfo* typeInfo, void* body) {
for (int index = 0; index < typeInfo->objOffsetsCount_; index++) {
ObjHeader** location = reinterpret_cast<ObjHeader**>(
reinterpret_cast<uintptr_t>(body) + typeInfo->objOffsets_[index]);
ZeroHeapRef(location);
}
}
namespace {
template<typename func>
inline void traverseContainerObjectFields(ContainerHeader* container, func process) {
RuntimeAssert(!isAggregatingFrozenContainer(container), "Must not be called on such containers");
ObjHeader* obj = reinterpret_cast<ObjHeader*>(container + 1);
for (int object = 0; object < container->objectCount(); object++) {
const TypeInfo* typeInfo = obj->type_info();
if (typeInfo != theArrayTypeInfo) {
for (int index = 0; index < typeInfo->objOffsetsCount_; index++) {
ObjHeader** location = reinterpret_cast<ObjHeader**>(
reinterpret_cast<uintptr_t>(obj) + typeInfo->objOffsets_[index]);
process(location);
}
} else {
ArrayHeader* array = obj->array();
for (int index = 0; index < array->count_; index++) {
process(ArrayAddressOfElementAt(array, index));
}
}
obj = reinterpret_cast<ObjHeader*>(
reinterpret_cast<uintptr_t>(obj) + objectSize(obj));
}
}
template<typename func>
inline void traverseContainerReferredObjects(ContainerHeader* container, func process) {
traverseContainerObjectFields(container, [process](ObjHeader** location) {
ObjHeader* ref = *location;
if (ref != nullptr) process(ref);
});
}
inline bool isMarkedAsRemoved(ContainerHeader* container) {
return (reinterpret_cast<uintptr_t>(container) & 1) != 0;
}
inline ContainerHeader* markAsRemoved(ContainerHeader* container) {
return reinterpret_cast<ContainerHeader*>(reinterpret_cast<uintptr_t>(container) | 1);
}
inline ContainerHeader* clearRemoved(ContainerHeader* container) {
return reinterpret_cast<ContainerHeader*>(
reinterpret_cast<uintptr_t>(container) & ~static_cast<uintptr_t>(1));
}
#if USE_GC
void processFinalizerQueue(MemoryState* state) {
// TODO: reuse elements of finalizer queue for new allocations.
while (state->finalizerQueue != nullptr) {
auto* container = state->finalizerQueue;
state->finalizerQueue = container->nextLink();
state->finalizerQueueSize--;
#if TRACE_MEMORY
state->containers->erase(container);
#endif
CONTAINER_DESTROY_EVENT(state, container)
konanFreeMemory(container);
atomicAdd(&allocCount, -1);
}
RuntimeAssert(state->finalizerQueueSize == 0, "Queue must be empty here");
}
#endif
void scheduleDestroyContainer(MemoryState* state, ContainerHeader* container) {
#if USE_GC
RuntimeAssert(container != nullptr, "Cannot destroy null container");
container->setNextLink(state->finalizerQueue);
state->finalizerQueue = container;
state->finalizerQueueSize++;
// We cannot clean finalizer queue while in GC.
if (!state->gcInProgress && state->finalizerQueueSuspendCount == 0 &&
state->finalizerQueueSize >= kFinalizerQueueThreshold) {
processFinalizerQueue(state);
}
#else
konanFreeMemory(container);
atomicAdd(&allocCount, -1);
CONTAINER_DESTROY_EVENT(state, container);
#endif
}
#if !USE_GC
template <bool Atomic>
inline void IncrementRC(ContainerHeader* container) {
container->incRefCount<Atomic>();
}
template <bool Atomic, bool UseCycleCollector>
inline void DecrementRC(ContainerHeader* container) {
if (container->decRefCount<Atomic>() == 0) {
freeContainer(container);
}
}
inline void DecrementRC(ContainerHeader* container) {
if (Shareable(container))
DecrementRC<true, false>(container);
else
DecrementRC<false, false>(container);
}
template <bool CanCollect>
inline void EnqueueDecrementRC(ContainerHeader* container) {
RuntimeCheck(false, "Not yet implemeneted");
}
#else // USE_GC
template <bool Atomic>
inline void IncrementRC(ContainerHeader* container) {
container->incRefCount<Atomic>();
}
template <bool Atomic, bool UseCycleCollector>
inline void DecrementRC(ContainerHeader* container) {
// TODO: enable me, once account for inner references in frozen objects correctly.
// RuntimeAssert(container->refCount() > 0, "Must be positive");
if (container->decRefCount<Atomic>() == 0) {
freeContainer(container);
} else if (UseCycleCollector) { // Possible root.
RuntimeAssert(container->refCount() > 0, "Must be positive");
RuntimeAssert(!Atomic && !container->shareable(), "Cycle collector shalln't be used with shared objects yet");
RuntimeAssert(container->objectCount() == 1, "cycle collector shall only work with single object containers");
// We do not use cycle collector for frozen objects, as we already detected
// possible cycles during freezing.
// Also do not use cycle collector for provable acyclic objects.
int color = container->color();
if (color != CONTAINER_TAG_GC_PURPLE && color != CONTAINER_TAG_GC_GREEN) {
container->setColorAssertIfGreen(CONTAINER_TAG_GC_PURPLE);
if (!container->buffered()) {
auto* state = memoryState;
container->setBuffered();
if (state->toFree != nullptr) {
state->toFree->push_back(container);
MEMORY_LOG("toFree is now %d\n", state->toFree->size())
if (state->gcSuspendCount == 0 && state->toRelease->size() >= state->gcThreshold) {
GC_LOG("Calling GC from DecrementRC: %d\n", state->toRelease->size())
garbageCollect(state, false);
}
}
}
}
}
}
inline void DecrementRC(ContainerHeader* container) {
auto* state = memoryState;
RuntimeAssert(state->gcInProgress, "Must only be called during GC");
// TODO: enable me, once account for inner references in frozen objects correctly.
// RuntimeAssert(container->refCount() > 0, "Must be positive");
bool useCycleCollector = container->tag() == CONTAINER_TAG_NORMAL;
if (container->decRefCount() == 0) {
freeContainer(container);
} else if (useCycleCollector && state->toFree != nullptr) {
RuntimeAssert(container->refCount() > 0, "Must be positive");
RuntimeAssert(!container->shareable(), "Cycle collector shalln't be used with shared objects yet");
RuntimeAssert(container->objectCount() == 1, "cycle collector shall only work with single object containers");
// We do not use cycle collector for frozen objects, as we already detected
// possible cycles during freezing.
// Also do not use cycle collector for provable acyclic objects.
int color = container->color();
if (color != CONTAINER_TAG_GC_PURPLE && color != CONTAINER_TAG_GC_GREEN) {
container->setColorAssertIfGreen(CONTAINER_TAG_GC_PURPLE);
if (!container->buffered()) {
container->setBuffered();
state->toFree->push_back(container);
}
}
}
}
template <bool CanCollect>
inline void EnqueueDecrementRC(ContainerHeader* container) {
auto* state = memoryState;
if (CanCollect) {
if (state->toRelease->size() >= state->gcThreshold && state->gcSuspendCount == 0) {
GC_LOG("Calling GC from EnqueueDecrementRC: %d\n", state->toRelease->size())
garbageCollect(state, false);
}
}
state->toRelease->push_back(container);
}
inline void initGcThreshold(MemoryState* state, uint32_t gcThreshold) {
state->gcThreshold = gcThreshold;
state->toRelease->reserve(gcThreshold);
}
#if GC_ERGONOMICS
inline void increaseGcThreshold(MemoryState* state) {
auto newThreshold = state->gcThreshold * 3 / 2 + 1;
if (newThreshold <= kMaxErgonomicThreshold) {
initGcThreshold(state, newThreshold);
}
}
#endif // GC_ERGONOMICS
#endif // USE_GC
#if TRACE_MEMORY && USE_GC
const char* colorNames[] = {"BLACK", "GRAY", "WHITE", "PURPLE", "GREEN", "ORANGE", "RED"};
void dumpObject(ObjHeader* ref, int indent) {
for (int i = 0; i < indent; i++) MEMORY_LOG(" ");
auto* typeInfo = ref->type_info();
auto* packageName =
typeInfo->packageName_ != nullptr ? CreateCStringFromString(typeInfo->packageName_) : nullptr;
auto* relativeName =
typeInfo->relativeName_ != nullptr ? CreateCStringFromString(typeInfo->relativeName_) : nullptr;
MEMORY_LOG("%p %s.%s\n", ref,
packageName ? packageName : "<unknown>", relativeName ? relativeName : "<unknown>");
if (packageName) konan::free(packageName);
if (relativeName) konan::free(relativeName);
}
void dumpContainerContent(ContainerHeader* container) {
if (container->refCount() <= 0) {
MEMORY_LOG("%p has non-positive RC, likely a memory bug\n", container)
return;
}
if (isAggregatingFrozenContainer(container)) {
MEMORY_LOG("%s aggregating container %p with %d objects rc=%d\n",
colorNames[container->color()], container, container->objectCount(), container->refCount());
ContainerHeader** subContainer = reinterpret_cast<ContainerHeader**>(container + 1);
for (int i = 0; i < container->objectCount(); ++i) {
ContainerHeader* sub = *subContainer++;
MEMORY_LOG(" container %p\n ", sub);
dumpContainerContent(sub);
}
} else {
MEMORY_LOG("%s regular %s%scontainer %p with %d objects rc=%d\n",
colorNames[container->color()],
container->frozen() ? "frozen " : "",
container->stack() ? "stack " : "",
container, container->objectCount(),
container->refCount());
ObjHeader* obj = reinterpret_cast<ObjHeader*>(container + 1);
dumpObject(obj, 4);
}
}
void dumpWorker(const char* prefix, ContainerHeader* header, ContainerHeaderSet* seen) {
dumpContainerContent(header);
seen->insert(header);
if (!isAggregatingFrozenContainer(header)) {
traverseContainerReferredObjects(header, [prefix, seen](ObjHeader* ref) {
auto* child = ref->container();
RuntimeAssert(!isArena(child), "A reference to local object is encountered");
if (child != nullptr && (seen->count(child) == 0)) {
dumpWorker(prefix, child, seen);
}
});
}
}
void dumpReachable(const char* prefix, const ContainerHeaderSet* roots) {
ContainerHeaderSet seen;
for (auto* container : *roots) {
dumpWorker(prefix, container, &seen);
}
}
#endif
#if USE_GC
void MarkRoots(MemoryState*);
void ScanRoots(MemoryState*);
void CollectRoots(MemoryState*);
void Scan(ContainerHeader* container);
template<bool useColor>
void MarkGray(ContainerHeader* start) {
ContainerHeaderDeque toVisit;
toVisit.push_front(start);
while (!toVisit.empty()) {
auto* container = toVisit.front();
MEMORY_LOG("MarkGray visit %p [%s]\n", container, colorNames[container->color()]);
toVisit.pop_front();
if (useColor) {
int color = container->color();
if (color == CONTAINER_TAG_GC_GRAY) continue;
// If see an acyclic object not being garbage - ignore it. We must properly traverse garbage, although.
if (color == CONTAINER_TAG_GC_GREEN && container->refCount() != 0) {
continue;
}
// Only garbage green object could be recolored here.
container->setColorEvenIfGreen(CONTAINER_TAG_GC_GRAY);
} else {
if (container->marked()) continue;
container->mark();
}
traverseContainerReferredObjects(container, [&toVisit](ObjHeader* ref) {
auto* childContainer = ref->container();
RuntimeAssert(!isArena(childContainer), "A reference to local object is encountered");
if (!Shareable(childContainer)) {
childContainer->decRefCount<false>();
toVisit.push_front(childContainer);
}
});
}
}
template<bool useColor>
void ScanBlack(ContainerHeader* start) {
ContainerHeaderDeque toVisit;
toVisit.push_front(start);
while (!toVisit.empty()) {
auto* container = toVisit.front();
MEMORY_LOG("ScanBlack visit %p [%s]\n", container, colorNames[container->color()]);
toVisit.pop_front();
if (useColor) {
auto color = container->color();
if (color == CONTAINER_TAG_GC_GREEN || color == CONTAINER_TAG_GC_BLACK) continue;
container->setColorAssertIfGreen(CONTAINER_TAG_GC_BLACK);
} else {
if (!container->marked()) continue;
container->unMark();
}
traverseContainerReferredObjects(container, [&toVisit](ObjHeader* ref) {
auto childContainer = ref->container();
RuntimeAssert(!isArena(childContainer), "A reference to local object is encountered");
if (!Shareable(childContainer)) {
childContainer->incRefCount<false>();
if (useColor) {
int color = childContainer->color();
if (color != CONTAINER_TAG_GC_BLACK)
toVisit.push_front(childContainer);
} else {
if (childContainer->marked())
toVisit.push_front(childContainer);
}
}
});
}
}
void CollectWhite(MemoryState*, ContainerHeader* container);
void CollectCycles(MemoryState* state) {
MarkRoots(state);
ScanRoots(state);
CollectRoots(state);
state->toFree->clear();
state->roots->clear();
}
void MarkRoots(MemoryState* state) {
for (auto container : *(state->toFree)) {
if (isMarkedAsRemoved(container))
continue;
// Acyclic containers cannot be in this list.
RuntimeCheck(container->color() != CONTAINER_TAG_GC_GREEN, "Must not be green");
auto color = container->color();
auto rcIsZero = container->refCount() == 0;
if (color == CONTAINER_TAG_GC_PURPLE && !rcIsZero) {
MarkGray<true>(container);
state->roots->push_back(container);
} else {
container->resetBuffered();
RuntimeAssert(color != CONTAINER_TAG_GC_GREEN, "Must not be green");
if (color == CONTAINER_TAG_GC_BLACK && rcIsZero) {
scheduleDestroyContainer(state, container);
}
}
}
}
void ScanRoots(MemoryState* state) {
for (auto* container : *(state->roots)) {
Scan(container);
}
}
void CollectRoots(MemoryState* state) {
// Here we might free some objects and call deallocation hooks on them,
// which in turn might call DecrementRC and trigger new GC - forbid that.
state->gcSuspendCount++;
for (auto* container : *(state->roots)) {
container->resetBuffered();
CollectWhite(state, container);
}
state->gcSuspendCount--;
}
void Scan(ContainerHeader* start) {
ContainerHeaderDeque toVisit;
toVisit.push_front(start);
while (!toVisit.empty()) {
auto* container = toVisit.front();
toVisit.pop_front();
if (container->color() != CONTAINER_TAG_GC_GRAY) continue;
if (container->refCount() != 0) {
ScanBlack<true>(container);
continue;
}
container->setColorAssertIfGreen(CONTAINER_TAG_GC_WHITE);
traverseContainerReferredObjects(container, [&toVisit](ObjHeader* ref) {
auto* childContainer = ref->container();
RuntimeAssert(!isArena(childContainer), "A reference to local object is encountered");
if (!Shareable(childContainer)) {
toVisit.push_front(childContainer);
}
});
}
}
void CollectWhite(MemoryState* state, ContainerHeader* start) {
ContainerHeaderDeque toVisit;
toVisit.push_back(start);
while (!toVisit.empty()) {
auto* container = toVisit.front();
toVisit.pop_front();
if (container->color() != CONTAINER_TAG_GC_WHITE || container->buffered()) continue;
container->setColorAssertIfGreen(CONTAINER_TAG_GC_BLACK);
traverseContainerObjectFields(container, [state, &toVisit](ObjHeader** location) {
auto* ref = *location;
if (ref == nullptr) return;
auto* childContainer = ref->container();
RuntimeAssert(!isArena(childContainer), "A reference to local object is encountered");
if (Shareable(childContainer)) {
ZeroHeapRef(location);
} else {
toVisit.push_front(childContainer);
}
});
runDeallocationHooks(container);
scheduleDestroyContainer(state, container);
}
}
#endif
inline bool needAtomicAccess(ContainerHeader* container) {
return container->shareable();
}
inline bool canBeCyclic(ContainerHeader* container) {
if (container->refCount() == 1) return false;
if (container->color() == CONTAINER_TAG_GC_GREEN) return false;
return true;
}
inline void AddHeapRef(ContainerHeader* container) {
MEMORY_LOG("AddHeapRef %p: rc=%d\n", container, container->refCount())
UPDATE_ADDREF_STAT(memoryState, container, needAtomicAccess(container), 0)
switch (container->tag()) {
case CONTAINER_TAG_STACK:
break;
case CONTAINER_TAG_NORMAL:
IncrementRC</* Atomic = */ false>(container);
break;
/* case CONTAINER_TAG_FROZEN: case CONTAINER_TAG_ATOMIC: */
default:
IncrementRC</* Atomic = */ true>(container);
break;
}
}
inline void AddHeapRef(const ObjHeader* header) {
auto* container = header->container();
if (container != nullptr)
AddHeapRef(const_cast<ContainerHeader*>(container));
}
inline void AddStackRef(ContainerHeader* container) {
UPDATE_ADDREF_STAT(memoryState, container, needAtomicAccess(container), 1);
if (container->shareable()) {
IncrementRC</* Atomic = */ true>(container);
}
}
inline void AddStackRef(const ObjHeader* header) {
auto* container = header->container();
if (container != nullptr) {
AddStackRef(const_cast<ContainerHeader*>(container));
}
}
inline void ReleaseHeapRef(ContainerHeader* container) {
MEMORY_LOG("ReleaseHeapRef %p: rc=%d\n", container, container->refCount())
UPDATE_RELEASEREF_STAT(memoryState, container, needAtomicAccess(container), canBeCyclic(container), 0)
switch (container->tag()) {
case CONTAINER_TAG_STACK:
break;
case CONTAINER_TAG_NORMAL:
EnqueueDecrementRC</* CanCollect = */ true>(container);
break;
/* case CONTAINER_TAG_FROZEN: case CONTAINER_TAG_ATOMIC: */
default:
DecrementRC</* Atomic = */ true, /* UseCyclicCollector = */ false>(container);
break;
}
}
inline void ReleaseStackRef(ContainerHeader* container) {
UPDATE_RELEASEREF_STAT(memoryState, container, needAtomicAccess(container), canBeCyclic(container), 1);
if (container->shareable() && container->decRefCount<true>() == 0) {
freeContainer(container);
}
}
inline void ReleaseHeapRef(const ObjHeader* header) {
auto* container = header->container();
if (container != nullptr)
ReleaseHeapRef(const_cast<ContainerHeader*>(container));
}
inline void ReleaseStackRef(const ObjHeader* header) {
auto* container = header->container();
if (container != nullptr)
ReleaseStackRef(const_cast<ContainerHeader*>(container));
}
// We use first slot as place to store frame-local arena container.
// TODO: create ArenaContainer object on the stack, so that we don't
// do two allocations per frame (ArenaContainer + actual container).
inline ArenaContainer* initedArena(ObjHeader** auxSlot) {
auto frame = asFrameOverlay(auxSlot);
auto arena = reinterpret_cast<ArenaContainer*>(frame->arena);
if (!arena) {
arena = konanConstructInstance<ArenaContainer>();
MEMORY_LOG("Initializing arena in %p\n", frame)
arena->Init();
frame->arena = arena;
}
return arena;
}
inline size_t containerSize(const ContainerHeader* container) {
size_t result = 0;
const ObjHeader* obj = reinterpret_cast<const ObjHeader*>(container + 1);
for (int object = 0; object < container->objectCount(); object++) {
size_t size = objectSize(obj);
result += size;
obj = reinterpret_cast<ObjHeader*>(reinterpret_cast<uintptr_t>(obj) + size);
}
return result;
}
} // namespace
MetaObjHeader* ObjHeader::createMetaObject(TypeInfo** location) {
MetaObjHeader* meta = konanConstructInstance<MetaObjHeader>();
TypeInfo* typeInfo = *location;
RuntimeCheck(!hasPointerBits(typeInfo, OBJECT_TAG_MASK), "Object must not be tagged");
meta->typeInfo_ = typeInfo;
#if KONAN_NO_THREADS
*location = reinterpret_cast<TypeInfo*>(meta);
#else
TypeInfo* old = __sync_val_compare_and_swap(location, typeInfo, reinterpret_cast<TypeInfo*>(meta));
if (old->typeInfo_ != old) {
// Someone installed a new meta-object since the check.
konanFreeMemory(meta);
meta = reinterpret_cast<MetaObjHeader*>(old);
}
#endif
return meta;
}
void ObjHeader::destroyMetaObject(TypeInfo** location) {
MetaObjHeader* meta = clearPointerBits(*(reinterpret_cast<MetaObjHeader**>(location)), OBJECT_TAG_MASK);
*const_cast<const TypeInfo**>(location) = meta->typeInfo_;
if (meta->counter_ != nullptr) {
WeakReferenceCounterClear(meta->counter_);
ZeroHeapRef(&meta->counter_);
}
#ifdef KONAN_OBJC_INTEROP
Kotlin_ObjCExport_releaseAssociatedObject(meta->associatedObject_);
#endif
konanFreeMemory(meta);
}
ContainerHeader* AllocContainer(MemoryState* state, size_t size) {
ContainerHeader* result = nullptr;
#if USE_GC
// We recycle elements of finalizer queue for new allocations, to avoid trashing memory manager.
ContainerHeader* container = state != nullptr ? state->finalizerQueue : nullptr;
ContainerHeader* previous = nullptr;
while (container != nullptr) {
// TODO: shall it be == instead?
if (container->hasContainerSize() &&
container->containerSize() >= size && container->containerSize() <= size + 16) {
MEMORY_LOG("recycle %p for request %d\n", container, size)
result = container;
if (previous == nullptr)
state->finalizerQueue = container->nextLink();
else
previous->setNextLink(container->nextLink());
state->finalizerQueueSize--;
memset(container, 0, size);
break;
}
previous = container;
container = container->nextLink();
}
#endif
if (result == nullptr) {
result = konanConstructSizedInstance<ContainerHeader>(alignUp(size, kObjectAlignment));
atomicAdd(&allocCount, 1);
}
if (state != nullptr) {
CONTAINER_ALLOC_EVENT(state, size, result);
#if TRACE_MEMORY
state->containers->insert(result);
#endif
}
return result;
}
ContainerHeader* AllocAggregatingFrozenContainer(KStdVector<ContainerHeader*>& containers) {
auto componentSize = containers.size();
auto* superContainer = AllocContainer(memoryState, sizeof(ContainerHeader) + sizeof(void*) * componentSize);
auto* place = reinterpret_cast<ContainerHeader**>(superContainer + 1);
for (auto* container : containers) {
*place++ = container;
// Set link to the new container.
auto* obj = reinterpret_cast<ObjHeader*>(container + 1);
obj->setContainer(superContainer);
MEMORY_LOG("Set fictitious frozen container for %p: %p\n", obj, superContainer);
}
superContainer->setObjectCount(componentSize);
superContainer->freeze();
return superContainer;
}
void FreeAggregatingFrozenContainer(ContainerHeader* container) {
auto* state = memoryState;
RuntimeAssert(isAggregatingFrozenContainer(container), "expected fictitious frozen container");
MEMORY_LOG("%p is fictitious frozen container\n", container);
RuntimeAssert(!container->buffered(), "frozen objects must not participate in GC")
#if USE_GC
// Forbid finalizerQueue handling.
++state->finalizerQueueSuspendCount;
#endif
// Special container for frozen objects.
ContainerHeader** subContainer = reinterpret_cast<ContainerHeader**>(container + 1);
MEMORY_LOG("Total subcontainers = %d\n", container->objectCount());
for (int i = 0; i < container->objectCount(); ++i) {
MEMORY_LOG("Freeing subcontainer %p\n", *subContainer);
freeContainer(*subContainer++);
}
#if USE_GC
--state->finalizerQueueSuspendCount;
#endif
scheduleDestroyContainer(state, container);
MEMORY_LOG("Freeing subcontainers done\n");
}
void freeContainer(ContainerHeader* container) {
RuntimeAssert(container != nullptr, "this kind of container shalln't be freed");
if (isAggregatingFrozenContainer(container)) {
FreeAggregatingFrozenContainer(container);
return;
}
runDeallocationHooks(container);
// Now let's clean all object's fields in this container.
traverseContainerObjectFields(container, [container](ObjHeader** location) {
ZeroHeapRef(location);
});
// And release underlying memory.
if (isFreeable(container)) {
container->setColorEvenIfGreen(CONTAINER_TAG_GC_BLACK);
if (!container->buffered())
scheduleDestroyContainer(memoryState, container);
}
}
void ObjectContainer::Init(MemoryState* state, const TypeInfo* typeInfo) {
RuntimeAssert(typeInfo->instanceSize_ >= 0, "Must be an object");
uint32_t alloc_size = sizeof(ContainerHeader) + typeInfo->instanceSize_;
header_ = AllocContainer(state, alloc_size);
RuntimeCheck(header_ != nullptr, "Cannot alloc memory");
// One object in this container, no need to set.
header_->setContainerSize(alloc_size);
RuntimeAssert(header_->objectCount() == 1, "Must work properly");
// header->refCount_ is zero initialized by AllocContainer().
SetHeader(GetPlace(), typeInfo);
OBJECT_ALLOC_EVENT(memoryState, typeInfo->instanceSize_, GetPlace())
}
void ArrayContainer::Init(MemoryState* state, const TypeInfo* typeInfo, uint32_t elements) {
RuntimeAssert(typeInfo->instanceSize_ < 0, "Must be an array");
uint32_t alloc_size =
sizeof(ContainerHeader) + arrayObjectSize(typeInfo, elements);
header_ = AllocContainer(state, alloc_size);
RuntimeCheck(header_ != nullptr, "Cannot alloc memory");
// One object in this container, no need to set.
header_->setContainerSize(alloc_size);
RuntimeAssert(header_->objectCount() == 1, "Must work properly");
// header->refCount_ is zero initialized by AllocContainer().
GetPlace()->count_ = elements;
SetHeader(GetPlace()->obj(), typeInfo);
OBJECT_ALLOC_EVENT(memoryState, arrayObjectSize(typeInfo, elements), GetPlace()->obj())
}
// TODO: store arena containers in some reuseable data structure, similar to
// finalizer queue.
void ArenaContainer::Init() {
allocContainer(1024);
}
void ArenaContainer::Deinit() {
MEMORY_LOG("Arena::Deinit start: %p\n", this)
auto chunk = currentChunk_;
while (chunk != nullptr) {
// freeContainer() doesn't release memory when CONTAINER_TAG_STACK is set.
MEMORY_LOG("Arena::Deinit free chunk %p\n", chunk)
freeContainer(chunk->asHeader());
chunk = chunk->next;
}
chunk = currentChunk_;
while (chunk != nullptr) {
auto toRemove = chunk;
chunk = chunk->next;
konanFreeMemory(toRemove);
}
}
bool ArenaContainer::allocContainer(container_size_t minSize) {
auto size = minSize + sizeof(ContainerHeader) + sizeof(ContainerChunk);
size = alignUp(size, kContainerAlignment);
// TODO: keep simple cache of container chunks.
ContainerChunk* result = konanConstructSizedInstance<ContainerChunk>(size);
RuntimeCheck(result != nullptr, "Cannot alloc memory");
if (result == nullptr) return false;
result->next = currentChunk_;
result->arena = this;
result->asHeader()->refCount_ = (CONTAINER_TAG_STACK | CONTAINER_TAG_INCREMENT);
currentChunk_ = result;
current_ = reinterpret_cast<uint8_t*>(result->asHeader() + 1);
end_ = reinterpret_cast<uint8_t*>(result) + size;
return true;
}
void* ArenaContainer::place(container_size_t size) {
size = alignUp(size, kObjectAlignment);
// Fast path.
if (current_ + size < end_) {
void* result = current_;
current_ += size;
return result;
}
if (!allocContainer(size)) {
return nullptr;
}
void* result = current_;
current_ += size;
RuntimeAssert(current_ <= end_, "Must not overflow");
return result;
}
#define ARENA_SLOTS_CHUNK_SIZE 16
ObjHeader** ArenaContainer::getSlot() {
if (slots_ == nullptr || slotsCount_ >= ARENA_SLOTS_CHUNK_SIZE) {
slots_ = PlaceArray(theArrayTypeInfo, ARENA_SLOTS_CHUNK_SIZE);
slotsCount_ = 0;
}
return ArrayAddressOfElementAt(slots_, slotsCount_++);
}
ObjHeader* ArenaContainer::PlaceObject(const TypeInfo* type_info) {
RuntimeAssert(type_info->instanceSize_ >= 0, "must be an object");
uint32_t size = type_info->instanceSize_;
ObjHeader* result = reinterpret_cast<ObjHeader*>(place(size));
if (!result) {
return nullptr;
}
OBJECT_ALLOC_EVENT(memoryState, type_info->instanceSize_, result)
currentChunk_->asHeader()->incObjectCount();
setHeader(result, type_info);
return result;
}
ArrayHeader* ArenaContainer::PlaceArray(const TypeInfo* type_info, uint32_t count) {
RuntimeAssert(type_info->instanceSize_ < 0, "must be an array");
container_size_t size = arrayObjectSize(type_info, count);
ArrayHeader* result = reinterpret_cast<ArrayHeader*>(place(size));
if (!result) {
return nullptr;
}
OBJECT_ALLOC_EVENT(memoryState, arrayObjectSize(type_info, count), result->obj())
currentChunk_->asHeader()->incObjectCount();
setHeader(result->obj(), type_info);
result->count_ = count;
return result;
}
void AddRefFromAssociatedObject(const ObjHeader* object) {
AddHeapRef(const_cast<ObjHeader*>(object));
}
void ReleaseRefFromAssociatedObject(const ObjHeader* object) {
ReleaseHeapRef(const_cast<ObjHeader*>(object));
}
#if USE_GC
void incrementStack(MemoryState* state) {
FrameOverlay* frame = currentFrame;
while (frame != nullptr) {
ObjHeader** current = reinterpret_cast<ObjHeader**>(frame + 1) + frame->parameters;
ObjHeader** end = current + frame->count - kFrameOverlaySlots - frame->parameters;
while (current < end) {
ObjHeader* obj = *current++;
if (obj != nullptr) {
auto* container = obj->container();
if (container != nullptr && container->tag() == CONTAINER_TAG_NORMAL)
IncrementRC<false>(container);
}
}
frame = frame->previous;
}
}
void actualizeNewlySharedOnStack(MemoryState* state, const ContainerHeaderSet* newlyShared) {
// For all frozen objects in stack slots - perform reference increment.
FrameOverlay* frame = currentFrame;
MEMORY_LOG("actualizeNewlySharedOnStack: newly shared size is %d\n", newlyShared->size())
while (frame != nullptr) {
MEMORY_LOG("current frame %p: %d parameters %d locals\n", frame, frame->parameters, frame->count)
ObjHeader** current = reinterpret_cast<ObjHeader**>(frame + 1) + frame->parameters;
ObjHeader** end = current + frame->count - kFrameOverlaySlots - frame->parameters;
while (current < end) {
ObjHeader* obj = *current;
current++;
if (obj != nullptr) {
auto* container = obj->container();
// No need to use atomic increment yet, object is still local.
if (container != nullptr && container->shareable() && newlyShared->count(container) != 0) {
container->incRefCount<false>();
MEMORY_LOG("incremented rc of %p to %d\n", container, container->refCount());
}
}
}
frame = frame->previous;
}
// And actualize RC of those objects using toRelease set.
for (auto& container : *(state->toRelease)) {
if (!isMarkedAsRemoved(container) && container->shareable()) {
RuntimeAssert(newlyShared->count(container) != 0, "Must be newly shared");
// To account for aggregating containers.
ContainerHeader* realContainer = realShareableContainer(container);
auto newRc = realContainer->decRefCount<false>();
MEMORY_LOG("decremented rc of %p to %d\n", realContainer, newRc);
container = markAsRemoved(container);
}
}
}
void processDecrements(MemoryState* state) {
auto* toRelease = state->toRelease;
state->gcSuspendCount++;
while (toRelease->size() > 0) {
auto* container = toRelease->back();
toRelease->pop_back();
if (isMarkedAsRemoved(container))
continue;
DecrementRC(container);
}
state->gcSuspendCount--;
}
void decrementStack(MemoryState* state) {
state->gcSuspendCount++;
FrameOverlay* frame = currentFrame;
while (frame != nullptr) {
ObjHeader** current = reinterpret_cast<ObjHeader**>(frame + 1) + frame->parameters;
ObjHeader** end = current + frame->count - kFrameOverlaySlots - frame->parameters;
while (current < end) {
ObjHeader* obj = *current++;
if (obj != nullptr) {
auto* container = obj->container();
if (container != nullptr && container->tag() == CONTAINER_TAG_NORMAL)
EnqueueDecrementRC</* CanCollect = */ false>(container);
}
}
frame = frame->previous;
}
state->gcSuspendCount--;
}
void garbageCollect(MemoryState* state, bool force) {
RuntimeAssert(!state->gcInProgress, "Recursive GC is disallowed");
GC_LOG(">>> %s GC: threshold = %d toFree %d toRelease %d\n", \
force ? "forced" : "regular", state->gcThreshold, state->toFree->size(), state->toRelease->size())
#if GC_ERGONOMICS
auto gcStartTime = konan::getTimeMicros();
#endif
state->gcInProgress = true;
incrementStack(state);
processDecrements(state);
size_t beforeDecrements = state->toRelease->size();
decrementStack(state);
size_t afterDecrements = state->toRelease->size();
ssize_t stackReferences = afterDecrements - beforeDecrements;
if (stackReferences * 5 > state->gcThreshold) {
#if GC_ERGONOMICS
increaseGcThreshold(state);
GC_LOG("||| GC: too many stack references, increased threshold to \n", state->gcThreshold);
#else
GC_LOG("Too many stack references for the threshold: %d vs %d\n", stackReferences, state->gcThreshold)
#endif
}
GC_LOG("||| GC: toFree %d toRelease %d\n", state->toFree->size(), state->toRelease->size())
processFinalizerQueue(state);
if (force || state->toFree->size() > kMaxToFreeSize) {
while (state->toFree->size() > 0) {
CollectCycles(state);
processFinalizerQueue(state);
}
}
state->gcInProgress = false;
#if GC_ERGONOMICS
auto gcEndTime = konan::getTimeMicros();
auto gcToComputeRatio = double(gcEndTime - gcStartTime) / (gcStartTime - state->lastGcTimestamp + 1);
if (gcToComputeRatio > kGcToComputeRatioThreshold) {
increaseGcThreshold(state);
GC_LOG("Adjusting GC threshold to %d\n", state->gcThreshold);
}
GC_LOG("GC: duration=%lld sinceLast=%lld\n", (gcEndTime - gcStartTime), gcStartTime - state->lastGcTimestamp);
state->lastGcTimestamp = gcEndTime;
#endif
GC_LOG("<<< GC: toFree %d toRelease %d\n", state->toFree->size(), state->toRelease->size())
}
#endif // USE_GC
extern "C" {
MemoryState* InitMemory() {
RuntimeAssert(offsetof(ArrayHeader, typeInfoOrMeta_)
==
offsetof(ObjHeader, typeInfoOrMeta_),
"Layout mismatch");
RuntimeAssert(offsetof(TypeInfo, typeInfo_)
==
offsetof(MetaObjHeader, typeInfo_),
"Layout mismatch");
RuntimeAssert(sizeof(FrameOverlay) % sizeof(ObjHeader**) == 0, "Frame overlay should contain only pointers")
RuntimeAssert(memoryState == nullptr, "memory state must be clear");
memoryState = konanConstructInstance<MemoryState>();
INIT_EVENT(memoryState)
#if USE_GC
memoryState->toFree = konanConstructInstance<ContainerHeaderList>();
memoryState->roots = konanConstructInstance<ContainerHeaderList>();
memoryState->gcInProgress = false;
memoryState->gcSuspendCount = 0;
memoryState->toRelease = konanConstructInstance<ContainerHeaderList>();
initGcThreshold(memoryState, kGcThreshold);
#endif
atomicAdd(&aliveMemoryStatesCount, 1);
return memoryState;
}
void DeinitMemory(MemoryState* memoryState) {
#if USE_GC
do {
GC_LOG("Calling GarbageCollect from DeinitMemory()\n")
garbageCollect(memoryState, true);
} while (memoryState->toRelease->size() > 0);
RuntimeAssert(memoryState->toFree->size() == 0, "Some memory have not been released after GC");
RuntimeAssert(memoryState->toRelease->size() == 0, "Some memory have not been released after GC");
konanDestructInstance(memoryState->toFree);
konanDestructInstance(memoryState->roots);
konanDestructInstance(memoryState->toRelease);
RuntimeAssert(memoryState->finalizerQueue == nullptr, "Finalizer queue must be empty");
RuntimeAssert(memoryState->finalizerQueueSize == 0, "Finalizer queue must be empty");
#endif // USE_GC
bool lastMemoryState = atomicAdd(&aliveMemoryStatesCount, -1) == 0;
#if TRACE_MEMORY
if (lastMemoryState && allocCount > 0) {
MEMORY_LOG("*** Memory leaks, leaked %d containers ***\n", allocCount);
dumpReachable("", memoryState->containers);
}
#else
#if USE_GC
if (lastMemoryState)
RuntimeAssert(allocCount == 0, "Memory leaks found");
#endif
#endif
PRINT_EVENT(memoryState)
DEINIT_EVENT(memoryState)
konanFreeMemory(memoryState);
::memoryState = nullptr;
}
MemoryState* SuspendMemory() {
auto result = ::memoryState;
::memoryState = nullptr;
return result;
}
void ResumeMemory(MemoryState* state) {
RuntimeAssert(::memoryState == nullptr, "Cannot schedule on existing state");
::memoryState = state;
}
OBJ_GETTER(AllocInstance, const TypeInfo* type_info) {
RuntimeAssert(type_info->instanceSize_ >= 0, "must be an object");
auto container = ObjectContainer(memoryState, type_info);
ContainerHeader* header = container.header();
// We cannot collect until reference will be stored into the stack slot.
if (header->tag() == CONTAINER_TAG_NORMAL) {
IncrementRC</* Atomic = */ false>(header);
EnqueueDecrementRC</* CanCollect = */ true>(header);
}
RETURN_OBJ(container.GetPlace());
}
OBJ_GETTER(AllocArrayInstance, const TypeInfo* type_info, int32_t elements) {
RuntimeAssert(type_info->instanceSize_ < 0, "must be an array");
if (elements < 0) ThrowIllegalArgumentException();
auto container = ArrayContainer(memoryState, type_info, elements);
ContainerHeader* header = container.header();
// We cannot collect until reference will be stored into the stack slot.
if (header->tag() == CONTAINER_TAG_NORMAL) {
IncrementRC</* Atomic = */ false>(header);
EnqueueDecrementRC</* CanCollect = */ true>(header);
}
RETURN_OBJ(container.GetPlace()->obj());
}
OBJ_GETTER(InitInstance,
ObjHeader** location, const TypeInfo* type_info, void (*ctor)(ObjHeader*)) {
ObjHeader* value = *location;
if (value != nullptr) {
// OK'ish, inited by someone else.
RETURN_OBJ(value);
}
ObjHeader* object = AllocInstance(type_info, OBJ_RESULT);
UpdateHeapRef(location, object);
#if KONAN_NO_EXCEPTIONS
ctor(object);
return object;
#else
try {
ctor(object);
return object;
} catch (...) {
UpdateReturnRef(OBJ_RESULT, nullptr);
ZeroHeapRef(location);
throw;
}
#endif
}
OBJ_GETTER(InitSharedInstance,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* type_info, void (*ctor)(ObjHeader*)) {
#if KONAN_NO_THREADS
ObjHeader* value = *location;
if (value != nullptr) {
// OK'ish, inited by someone else.
RETURN_OBJ(value);
}
ObjHeader* object = AllocInstance(type_info, OBJ_RESULT);
UpdateHeapRef(location, object);
#if KONAN_NO_EXCEPTIONS
ctor(object);
FreezeSubgraph(object);
return object;
#else
try {
ctor(object);
FreezeSubgraph(object);
return object;
} catch (...) {
UpdateReturnRef(OBJ_RESULT, nullptr);
ZeroHeapRef(location);
throw;
}
#endif
#else
ObjHeader* value = *localLocation;
if (value != nullptr) RETURN_OBJ(value);
ObjHeader* initializing = reinterpret_cast<ObjHeader*>(1);
// Spin lock.
while ((value = __sync_val_compare_and_swap(location, nullptr, initializing)) == initializing);
if (value != nullptr) {
// OK'ish, inited by someone else.
RETURN_OBJ(value);
}
ObjHeader* object = AllocInstance(type_info, OBJ_RESULT);
RuntimeAssert(object->container()->normal() , "Shared object cannot be co-allocated");
UpdateHeapRef(localLocation, object);
#if KONAN_NO_EXCEPTIONS
ctor(object);
FreezeSubgraph(object);
UpdateHeapRef(location, object);
synchronize();
return object;
#else
try {
ctor(object);
FreezeSubgraph(object);
UpdateHeapRef(location, object);
synchronize();
return object;
} catch (...) {
UpdateReturnRef(OBJ_RESULT, nullptr);
ZeroHeapRef(location);
ZeroHeapRef(localLocation);
synchronize();
throw;
}
#endif
#endif
}
void SetStackRef(ObjHeader** location, const ObjHeader* object) {
MEMORY_LOG("SetStackRef *%p: %p\n", location, object)
UPDATE_REF_EVENT(memoryState, nullptr, object, location, 1);
if (object != nullptr)
AddStackRef(const_cast<ObjHeader*>(object));
*const_cast<const ObjHeader**>(location) = object;
}
void SetHeapRef(ObjHeader** location, const ObjHeader* object) {
MEMORY_LOG("SetHeapRef *%p: %p\n", location, object)
UPDATE_REF_EVENT(memoryState, nullptr, object, location, 0);
if (object != nullptr)
AddHeapRef(const_cast<ObjHeader*>(object));
*const_cast<const ObjHeader**>(location) = object;
}
void ZeroHeapRef(ObjHeader** location) {
MEMORY_LOG("ZeroHeapRef %p\n", location)
auto* value = *location;
if (value != nullptr) {
UPDATE_REF_EVENT(memoryState, value, nullptr, location, 0);
*location = nullptr;
ReleaseHeapRef(value);
}
}
void ZeroStackRef(ObjHeader** location) {
MEMORY_LOG("ZeroStackRef %p\n", location)
auto* value = *location;
if (value != nullptr) {
UPDATE_REF_EVENT(memoryState, value, nullptr, location, 1);
*location = nullptr;
ReleaseStackRef(value);
}
}
void UpdateStackRef(ObjHeader** location, const ObjHeader* object) {
UPDATE_REF_EVENT(memoryState, *location, object, location, 1)
RuntimeAssert(object != reinterpret_cast<ObjHeader*>(1), "Markers disallowed here");
ObjHeader* old = *location;
if (old != object) {
if (object != nullptr) {
AddStackRef(object);
}
*const_cast<const ObjHeader**>(location) = object;
if (old != nullptr ) {
ReleaseStackRef(old);
}
}
}
void UpdateHeapRef(ObjHeader** location, const ObjHeader* object) {
UPDATE_REF_EVENT(memoryState, *location, object, location, 0);
ObjHeader* old = *location;
if (old != object) {
if (object != nullptr) {
AddHeapRef(object);
}
*const_cast<const ObjHeader**>(location) = object;
if (reinterpret_cast<uintptr_t>(old) > 1) {
ReleaseHeapRef(old);
}
}
}
ObjHeader** GetReturnSlotIfArena(ObjHeader** returnSlot, ObjHeader** localSlot) {
RuntimeCheck(false, "No longer supported");
return nullptr;
}
ObjHeader** GetParamSlotIfArena(ObjHeader** returnSlot, ObjHeader** localSlot) {
RuntimeCheck(false, "No longer supported");
return nullptr;
}
inline void updateReturnRefAdded(ObjHeader** returnSlot, const ObjHeader* value) {
MEMORY_LOG("updateReturnRefAdded %p\n", returnSlot)
ObjHeader* old = *returnSlot;
UPDATE_REF_EVENT(memoryState, old, value, returnSlot, 1)
*const_cast<const ObjHeader**>(returnSlot) = value;
if (old != nullptr) {
ReleaseStackRef(old);
}
}
void UpdateReturnRef(ObjHeader** returnSlot, const ObjHeader* value) {
UpdateStackRef(returnSlot, value);
}
void UpdateHeapRefIfNull(ObjHeader** location, const ObjHeader* object) {
if (object != nullptr) {
#if KONAN_NO_THREADS
ObjHeader* old = *location;
if (old == nullptr) {
AddHeapRef(const_cast<ObjHeader*>(object));
*const_cast<const ObjHeader**>(location) = object;
}
#else
AddHeapRef(const_cast<ObjHeader*>(object));
auto old = __sync_val_compare_and_swap(location, nullptr, const_cast<ObjHeader*>(object));
if (old != nullptr) {
// Failed to store, was not null.
ReleaseHeapRef(const_cast<ObjHeader*>(object));
}
#endif
UPDATE_REF_EVENT(memoryState, old, object, location, 0);
}
}
void EnterFrame(ObjHeader** start, int parameters, int count) {
MEMORY_LOG("EnterFrame %p: %d parameters %d locals\n", start, parameters, count)
FrameOverlay* frame = reinterpret_cast<FrameOverlay*>(start);
frame->previous = currentFrame;
currentFrame = frame;
// TODO: maybe compress in single value somehow.
frame->parameters = parameters;
frame->count = count;
}
void LeaveFrame(ObjHeader** start, int parameters, int count) {
MEMORY_LOG("LeaveFrame %p: %d parameters %d locals\n", start, parameters, count)
MemoryState* state = memoryState;
ObjHeader** current = start + parameters + kFrameOverlaySlots;
ObjHeader** end = start + count;
while (current < end) {
ObjHeader* object = *current++;
if (object != nullptr) {
ReleaseStackRef(object);
}
}
FrameOverlay* frame = reinterpret_cast<FrameOverlay*>(start);
currentFrame = frame->previous;
}
#if USE_GC
void GarbageCollect() {
garbageCollect(memoryState, true);
}
#endif // USE_GC
void Kotlin_native_internal_GC_collect(KRef) {
#if USE_GC
GC_LOG("Kotlin_native_internal_GC_collect\n")
GarbageCollect();
#endif
}
void Kotlin_native_internal_GC_suspend(KRef) {
#if USE_GC
GC_LOG("Kotlin_native_internal_GC_suspend\n")
memoryState->gcSuspendCount++;
#endif
}
void Kotlin_native_internal_GC_resume(KRef) {
#if USE_GC
MemoryState* state = memoryState;
if (state->gcSuspendCount > 0) {
state->gcSuspendCount--;
if (state->toRelease != nullptr &&
state->toRelease->size() >= state->gcThreshold &&
state->gcSuspendCount == 0) {
GC_LOG("Kotlin_native_internal_GC_resume\n")
garbageCollect(state, false);
}
}
#endif
}
void Kotlin_native_internal_GC_stop(KRef) {
#if USE_GC
GC_LOG("Kotlin_native_internal_GC_stop\n")
if (memoryState->toRelease != nullptr) {
memoryState->gcSuspendCount = 0;
garbageCollect(memoryState, true);
konanDestructInstance(memoryState->toRelease);
konanDestructInstance(memoryState->toFree);
konanDestructInstance(memoryState->roots);
memoryState->toRelease = nullptr;
memoryState->toFree = nullptr;
memoryState->roots = nullptr;
}
#endif
}
void Kotlin_native_internal_GC_start(KRef) {
#if USE_GC
GC_LOG("Kotlin_native_internal_GC_start\n")
if (memoryState->toFree == nullptr) {
memoryState->toFree = konanConstructInstance<ContainerHeaderList>();
memoryState->toRelease = konanConstructInstance<ContainerHeaderList>();
memoryState->roots = konanConstructInstance<ContainerHeaderList>();
memoryState->gcSuspendCount = 0;
}
#endif
}
void Kotlin_native_internal_GC_setThreshold(KRef, KInt value) {
#if USE_GC
GC_LOG("Kotlin_native_internal_setThreshold %d\n", value)
if (value > 0) {
initGcThreshold(memoryState, value);
}
#endif
}
KInt Kotlin_native_internal_GC_getThreshold(KRef) {
#if USE_GC
GC_LOG("Kotlin_native_internal_getThreshold %d\n")
return memoryState->gcThreshold;
#else
return -1;
#endif
}
KNativePtr CreateStablePointer(KRef any) {
if (any == nullptr) return nullptr;
MEMORY_LOG("CreateStablePointer for %p rc=%d\n", any, any->container() ? any->container()->refCount() : 0)
AddHeapRef(any);
return reinterpret_cast<KNativePtr>(any);
}
void DisposeStablePointer(KNativePtr pointer) {
if (pointer == nullptr) return;
KRef ref = reinterpret_cast<KRef>(pointer);
ReleaseHeapRef(ref);
}
OBJ_GETTER(DerefStablePointer, KNativePtr pointer) {
KRef ref = reinterpret_cast<KRef>(pointer);
RETURN_OBJ(ref);
}
OBJ_GETTER(AdoptStablePointer, KNativePtr pointer) {
synchronize();
KRef ref = reinterpret_cast<KRef>(pointer);
MEMORY_LOG("adopting stable pointer %p, rc=%d\n", \
ref, (ref && ref->container()) ? ref->container()->refCount() : -1)
UpdateReturnRef(OBJ_RESULT, ref);
DisposeStablePointer(pointer);
return ref;
}
#if USE_GC
bool hasExternalRefs(ContainerHeader* start, ContainerHeaderSet* visited) {
ContainerHeaderDeque toVisit;
toVisit.push_back(start);
while (!toVisit.empty()) {
auto* container = toVisit.front();
toVisit.pop_front();
visited->insert(container);
if (container->refCount() > 0) {
MEMORY_LOG("container %p with rc %d blocks transfer\n", container, container->refCount())
return true;
}
traverseContainerReferredObjects(container, [&toVisit, visited](ObjHeader* ref) {
auto* child = ref->container();
if (!Shareable(child) && (visited->count(child) == 0)) {
toVisit.push_front(child);
}
});
}
return false;
}
#endif // USE_GC
bool ClearSubgraphReferences(ObjHeader* root, bool checked) {
#if USE_GC
MEMORY_LOG("ClearSubgraphReferences %p\n", root)
if (root == nullptr) return true;
auto state = memoryState;
auto* container = root->container();
if (Shareable(container))
// We assume, that frozen/shareable objects can be safely passed and not present
// in the GC candidate list.
// TODO: assert for that?
return true;
ContainerHeaderSet visited;
if (!checked) {
hasExternalRefs(container, &visited);
} else {
// Now decrement RC of elements in toRelease set for reachibility analysis.
for (auto it = state->toRelease->begin(); it != state->toRelease->end(); ++it) {
auto released = *it;
if (!isMarkedAsRemoved(released) && released->tag() == CONTAINER_TAG_NORMAL) {
released->decRefCount<false>();
}
}
container->decRefCount<false>();
MarkGray<false>(container);
auto bad = hasExternalRefs(container, &visited);
ScanBlack<false>(container);
// Restore original RC.
container->incRefCount<false>();
for (auto it = state->toRelease->begin(); it != state->toRelease->end(); ++it) {
auto released = *it;
if (!isMarkedAsRemoved(released) && released->tag() == CONTAINER_TAG_NORMAL) {
released->incRefCount<false>();
}
}
if (bad) {
return false;
}
}
// Remove all no longer owned containers from GC structures.
// TODO: not very efficient traversal.
for (auto it = state->toFree->begin(); it != state->toFree->end(); ++it) {
auto container = *it;
if (visited.count(container) != 0) {
MEMORY_LOG("removing %p from the toFree list\n", container)
container->resetBuffered();
container->setColorAssertIfGreen(CONTAINER_TAG_GC_BLACK);
*it = markAsRemoved(container);
}
}
for (auto it = state->toRelease->begin(); it != state->toRelease->end(); ++it) {
auto container = *it;
if (!isMarkedAsRemoved(container) && visited.count(container) != 0) {
MEMORY_LOG("removing %p from the toRelease list\n", container)
container->decRefCount<false>();
*it = markAsRemoved(container);
}
}
#if TRACE_MEMORY
// Forget transferred containers.
for (auto* it: visited) {
state->containers->erase(it);
}
#endif
#endif // USE_GC
return true;
}
/**
* Do DFS cycle detection with three colors:
* - 'marked' bit as BLACK marker (object and its descendants processed)
* - 'seen' bit as GRAY marker (object is being processed)
* - not 'marked' and not 'seen' as WHITE marker (object is unprocessed)
* When we see GREY during DFS, it means we see cycle.
*/
void depthFirstTraversal(ContainerHeader* start, bool* hasCycles,
KRef* firstBlocker, KStdVector<ContainerHeader*>* order) {
ContainerHeaderDeque toVisit;
toVisit.push_back(start);
start->setSeen();
while (!toVisit.empty()) {
auto* container = toVisit.front();
toVisit.pop_front();
if (isMarkedAsRemoved(container)) {
container = clearRemoved(container);
// Mark BLACK.
container->resetSeen();
container->mark();
order->push_back(container);
continue;
}
toVisit.push_front(markAsRemoved(container));
traverseContainerReferredObjects(container, [hasCycles, firstBlocker, &order, &toVisit](ObjHeader* obj) {
if (*firstBlocker != nullptr)
return;
if (obj->has_meta_object() && ((obj->meta_object()->flags_ & MF_NEVER_FROZEN) != 0)) {
*firstBlocker = obj;
return;
}
ContainerHeader* objContainer = obj->container();
if (!Shareable(objContainer)) {
// Marked GREY, there's cycle.
if (objContainer->seen()) *hasCycles = true;
// Go deeper if WHITE.
if (!objContainer->seen() && !objContainer->marked()) {
// Mark GRAY.
objContainer->setSeen();
toVisit.push_front(objContainer);
}
}
});
}
}
void traverseStronglyConnectedComponent(ContainerHeader* start,
KStdUnorderedMap<ContainerHeader*,
KStdVector<ContainerHeader*>> const* reversedEdges,
KStdVector<ContainerHeader*>* component) {
ContainerHeaderDeque toVisit;
toVisit.push_back(start);
start->mark();
while (!toVisit.empty()) {
auto* container = toVisit.front();
toVisit.pop_front();
component->push_back(container);
auto it = reversedEdges->find(container);
RuntimeAssert(it != reversedEdges->end(), "unknown node during condensation building");
for (auto* nextContainer : it->second) {
if (!nextContainer->marked()) {
nextContainer->mark();
toVisit.push_front(nextContainer);
}
}
}
}
void freezeAcyclic(ContainerHeader* rootContainer, ContainerHeaderSet* newlyFrozen) {
KStdDeque<ContainerHeader*> queue;
queue.push_back(rootContainer);
while (!queue.empty()) {
ContainerHeader* current = queue.front();
queue.pop_front();
current->unMark();
current->resetBuffered();
current->setColorUnlessGreen(CONTAINER_TAG_GC_BLACK);
// Note, that once object is frozen, it could be concurrently accessed, so
// color and similar attributes shall not be used.
if (current->tag() == CONTAINER_TAG_NORMAL)
newlyFrozen->insert(current);
MEMORY_LOG("freezeing %p\n", current)
current->freeze();
traverseContainerReferredObjects(current, [current, &queue](ObjHeader* obj) {
ContainerHeader* objContainer = obj->container();
if (!Shareable(objContainer)) {
if (objContainer->marked())
queue.push_back(objContainer);
}
});
}
}
void freezeCyclic(ContainerHeader* rootContainer,
const KStdVector<ContainerHeader*>& order,
ContainerHeaderSet* newlyFrozen) {
KStdUnorderedMap<ContainerHeader*, KStdVector<ContainerHeader*>> reversedEdges;
KStdDeque<ContainerHeader*> queue;
queue.push_back(rootContainer);
while (!queue.empty()) {
ContainerHeader* current = queue.front();
queue.pop_front();
current->unMark();
reversedEdges.emplace(current, KStdVector<ContainerHeader*>(0));
traverseContainerReferredObjects(current, [current, &queue, &reversedEdges](ObjHeader* obj) {
ContainerHeader* objContainer = obj->container();
if (!Shareable(objContainer)) {
if (objContainer->marked())
queue.push_back(objContainer);
reversedEdges.emplace(objContainer, KStdVector<ContainerHeader*>(0)).first->second.push_back(current);
}
});
}
KStdVector<KStdVector<ContainerHeader*>> components;
MEMORY_LOG("Condensation:\n");
// Enumerate in the topological order.
for (auto it = order.rbegin(); it != order.rend(); ++it) {
auto* container = *it;
if (container->marked()) continue;
KStdVector<ContainerHeader*> component;
traverseStronglyConnectedComponent(container, &reversedEdges, &component);
MEMORY_LOG("SCC:\n");
#if TRACE_MEMORY
for (auto c: component)
konan::consolePrintf(" %p\n", c);
#endif
components.push_back(std::move(component));
}
// Enumerate strongly connected components in reversed topological order.
for (auto it = components.rbegin(); it != components.rend(); ++it) {
auto& component = *it;
int internalRefsCount = 0;
int totalCount = 0;
for (auto* container : component) {
totalCount += container->refCount();
traverseContainerReferredObjects(container, [&internalRefsCount](ObjHeader* obj) {
auto* container = obj->container();
if (!Shareable(container))
++internalRefsCount;
});
}
// Freeze component.
for (auto* container : component) {
container->resetBuffered();
container->setColorUnlessGreen(CONTAINER_TAG_GC_BLACK);
if (container->tag() == CONTAINER_TAG_NORMAL)
newlyFrozen->insert(container);
// Note, that once object is frozen, it could be concurrently accessed, so
// color and similar attributes shall not be used.
MEMORY_LOG("freezeing %p\n", container)
container->freeze();
// We set refcount of original container to zero, so that it is seen as such after removal
// meta-object, where aggregating container is stored.
container->setRefCount(0);
}
// Create fictitious container for the whole component.
auto superContainer = component.size() == 1 ? component[0] : AllocAggregatingFrozenContainer(component);
// Don't count internal references.
MEMORY_LOG("Setting aggregating %p rc to %d (total %d inner %d)\n", \
superContainer, totalCount - internalRefsCount, totalCount, internalRefsCount)
superContainer->setRefCount(totalCount - internalRefsCount);
newlyFrozen->insert(superContainer);
}
}
/**
* Theory of operations.
*
* Kotlin/Native supports object graph freezing, allowing to make certain subgraph immutable and thus
* suitable for safe sharing amongst multiple concurrent executors. This operation recursively operates
* on all objects reachable from the given object, and marks them as frozen. In frozen state object's
* fields cannot be modified, and so, lifetime of frozen objects correlates. Practically, it means
* that lifetimes of all strongly connected components are fully controlled by incoming reference
* counters, and so if we place all members of strongly connected component to the single container
* it could be correctly released by just atomic decrement on reference counter, without additional
* cycle collector run.
* So during subgraph freezing operation, we perform the following steps:
* - run Kosoraju-Sharir algorithm to find strongly connected components
* - put all objects in each strongly connected component into an artificial container
* (we assume that they all were in single element containers initially), single-object
* components remain in the same container
* - artificial container sums up outer reference counters of all its objects (i.e.
* incoming references from the same strongly connected component are not counted)
* - mark all object's headers as frozen
*
* Further reference counting on frozen objects is performed with atomic operations, and so frozen
* references could be passed across multiple threads.
*/
void FreezeSubgraph(ObjHeader* root) {
if (root == nullptr) return;
// First check that passed object graph has no cycles.
// If there are cycles - run graph condensation on cyclic graphs using Kosoraju-Sharir.
ContainerHeader* rootContainer = root->container();
if (Shareable(rootContainer)) return;
// Do DFS cycle detection.
bool hasCycles = false;
KRef firstBlocker = root->has_meta_object() && ((root->meta_object()->flags_ & MF_NEVER_FROZEN) != 0) ?
root : nullptr;
KStdVector<ContainerHeader*> order;
depthFirstTraversal(rootContainer, &hasCycles, &firstBlocker, &order);
if (firstBlocker != nullptr) {
ThrowFreezingException(root, firstBlocker);
}
ContainerHeaderSet newlyFrozen;
// Now unmark all marked objects, and freeze them, if no cycles detected.
if (hasCycles) {
freezeCyclic(rootContainer, order, &newlyFrozen);
} else {
freezeAcyclic(rootContainer, &newlyFrozen);
}
#if USE_GC
// Now remove frozen objects from the toFree list.
// TODO: optimize it by keeping ignored (i.e. freshly frozen) objects in the set,
// and use it when analyzing toFree during collection.
auto state = memoryState;
for (auto& container : *(state->toFree)) {
if (!isMarkedAsRemoved(container) && container->frozen()) {
RuntimeAssert(newlyFrozen.count(container) != 0, "Must be newly frozen");
container = markAsRemoved(container);
}
}
// Actualize reference counters of newly frozen objects.
actualizeNewlySharedOnStack(state, &newlyFrozen);
#endif
}
// This function is called from field mutators to check if object's header is frozen.
// If object is frozen, an exception is thrown.
void MutationCheck(ObjHeader* obj) {
auto* container = obj->container();
if (container != nullptr && container->frozen()) ThrowInvalidMutabilityException(obj);
}
OBJ_GETTER(SwapHeapRefLocked,
ObjHeader** location, ObjHeader* expectedValue, ObjHeader* newValue, int32_t* spinlock) {
lock(spinlock);
ObjHeader* oldValue = *location;
// We do not use UpdateRef() here to avoid having ReleaseRef() on return slot under the lock.
if (oldValue == expectedValue) {
SetHeapRef(location, newValue);
} else {
// We create an additional reference to the [oldValue] in the return slot.
if (oldValue != nullptr && isRefCounted(oldValue)) {
AddHeapRef(oldValue);
}
}
unlock(spinlock);
// [oldValue] ownership was either transferred from *location to return slot if CAS succeeded, or
// we explicitly added a new reference if CAS failed.
updateReturnRefAdded(OBJ_RESULT, oldValue);
return oldValue;
}
void SetHeapRefLocked(ObjHeader** location, ObjHeader* newValue, int32_t* spinlock) {
lock(spinlock);
ObjHeader* oldValue = *location;
// We do not use UpdateRef() here to avoid having ReleaseRef() on old value under the lock.
SetHeapRef(location, newValue);
unlock(spinlock);
if (oldValue != nullptr)
ReleaseHeapRef(oldValue);
}
OBJ_GETTER(ReadHeapRefLocked, ObjHeader** location, int32_t* spinlock) {
MEMORY_LOG("ReadHeapRefLocked: %p\n", location)
lock(spinlock);
ObjHeader* value = *location;
// We do not use UpdateRef() here to avoid having ReleaseRef() on return slot under the lock.
if (value != nullptr) {
AddStackRef(value);
}
unlock(spinlock);
updateReturnRefAdded(OBJ_RESULT, value);
return value;
}
void EnsureNeverFrozen(ObjHeader* object) {
auto* container = object->container();
if (container == nullptr || container->frozen())
ThrowFreezingException(object, object);
// TODO: note, that this API could not not be called on frozen objects, so no need to care much about concurrency,
// although there's subtle race with case, where other thread freezes the same object after check.
object->meta_object()->flags_ |= MF_NEVER_FROZEN;
}
KBoolean Konan_ensureAcyclicAndSet(ObjHeader* where, KInt index, ObjHeader* what) {
RuntimeAssert(where->container() != nullptr && where->container()->frozen(), "Must be used on frozen objects only");
RuntimeAssert(what == nullptr || PermanentOrFrozen(what),
"Must be used with an immutable value");
if (what != nullptr) {
// Now we check that `where` is not reachable from `what`.
// As we cannot modify objects while traversing, instead we remember all seen objects in a set.
KStdUnorderedSet<ContainerHeader*> seen;
KStdDeque<ContainerHeader*> queue;
if (what->container() != nullptr)
queue.push_back(what->container());
bool acyclic = true;
while (!queue.empty() && acyclic) {
ContainerHeader* current = queue.front();
queue.pop_front();
seen.insert(current);
if (isAggregatingFrozenContainer(current)) {
ContainerHeader** subContainer = reinterpret_cast<ContainerHeader**>(current + 1);
for (int i = 0; i < current->objectCount(); ++i) {
if (seen.count(*subContainer) == 0)
queue.push_back(*subContainer++);
}
} else {
traverseContainerReferredObjects(current, [where, &queue, &acyclic, &seen](ObjHeader* obj) {
if (obj == where) {
acyclic = false;
} else {
auto* objContainer = obj->container();
if (objContainer != nullptr && seen.count(objContainer) == 0)
queue.push_back(objContainer);
}
});
}
}
if (!acyclic) return false;
}
UpdateHeapRef(reinterpret_cast<ObjHeader**>(
reinterpret_cast<uintptr_t>(where) + where->type_info()->objOffsets_[index]), what);
// Fence on updated location?
return true;
}
void Kotlin_Any_share(ObjHeader* obj) {
auto* container = obj->container();
if (Shareable(container)) return;
RuntimeCheck(container->objectCount() == 1, "Must be a single object container");
container->makeShareable();
ContainerHeaderSet newlyShared;
newlyShared.insert(container);
actualizeNewlySharedOnStack(memoryState, &newlyShared);
}
} // extern "C"