Skip to content
This repository has been archived by the owner on Aug 10, 2021. It is now read-only.

Store initializing singletons in a separate thread local small map #4019

Merged
merged 8 commits into from
Mar 27, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,6 @@ internal class FunctionGenerationContext(val function: LLVMValueRef,
}
val singleton = context.llvmDeclarations.forSingleton(irClass)
val instanceAddress = singleton.instanceStorage
val instanceShadowAddress = singleton.instanceShadowStorage

if (storageKind == ObjectStorageKind.PERMANENT) {
return loadSlot(instanceAddress.getAddress(this), false)
Expand All @@ -938,13 +937,13 @@ internal class FunctionGenerationContext(val function: LLVMValueRef,
val typeInfo = codegen.typeInfoForAllocation(irClass)
val defaultConstructor = irClass.constructors.single { it.valueParameters.size == 0 }
val ctor = codegen.llvmFunction(defaultConstructor)
val (initFunction, args) =
val initFunction =
if (storageKind == ObjectStorageKind.SHARED && context.config.threadsAreAllowed) {
val shadowObjectPtr = instanceShadowAddress!!.getAddress(this)
context.llvm.initSharedInstanceFunction to listOf(objectPtr, shadowObjectPtr, typeInfo, ctor)
context.llvm.initSharedInstanceFunction
} else {
context.llvm.initInstanceFunction to listOf(objectPtr, typeInfo, ctor)
context.llvm.initInstanceFunction
}
val args = listOf(objectPtr, typeInfo, ctor)
val newValue = call(initFunction, args, Lifetime.GLOBAL, exceptionHandler)
val bbInitResult = currentBlock
br(bbExit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ internal class ClassLlvmDeclarations(
val singletonDeclarations: SingletonLlvmDeclarations?,
val objCDeclarations: KotlinObjCClassLlvmDeclarations?)

internal class SingletonLlvmDeclarations(val instanceStorage: AddressAccess, val instanceShadowStorage: AddressAccess?)
internal class SingletonLlvmDeclarations(val instanceStorage: AddressAccess)

internal class KotlinObjCClassLlvmDeclarations(
val classPointerGlobal: StaticData.Global,
Expand Down Expand Up @@ -279,12 +279,7 @@ private class DeclarationsGeneratorVisitor(override val context: Context) :
val symbolName = "kobjref:" + qualifyInternalName(irClass)
val instanceAddress = addKotlinGlobal(symbolName, getLLVMType(irClass.defaultType), threadLocal = threadLocal)

val instanceShadowAddress = if (threadLocal || storageKind == ObjectStorageKind.PERMANENT) null else {
val shadowSymbolName = "kshadowobjref:" + qualifyInternalName(irClass)
addKotlinGlobal(shadowSymbolName, getLLVMType(irClass.defaultType), threadLocal = true)
}

return SingletonLlvmDeclarations(instanceAddress, instanceShadowAddress)
return SingletonLlvmDeclarations(instanceAddress)
}

private fun createKotlinObjCClassDeclarations(irClass: IrClass): KotlinObjCClassLlvmDeclarations {
Expand Down
9 changes: 9 additions & 0 deletions backend.native/tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2675,6 +2675,15 @@ task initializers5(type: KonanLocalTest) {
source = "runtime/basic/initializers5.kt"
}

task initializers6(type: KonanLocalTest) {
disabled = project.testTarget == 'wasm32' // Needs workers.
source = "runtime/basic/initializers6.kt"
}

task initializers7(type: KonanLocalTest) {
source = "runtime/basic/initializers7.kt"
}

task expression_as_statement(type: KonanLocalTest) {
expectedFail = (project.testTarget == 'wasm32') // uses exceptions.
goldValue = "Ok\n"
Expand Down
75 changes: 75 additions & 0 deletions backend.native/tests/runtime/basic/initializers6.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/

package runtime.basic.initializers6

import kotlin.test.*

import kotlin.native.concurrent.*

val aWorkerId = 2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is always correct.

val bWorkersCount = 3

val aWorkerUnlocker = AtomicInt(0)
val bWorkerUnlocker = AtomicInt(0)

object A {
init {
// Must be called by aWorker only.
assertEquals(aWorkerId, Worker.current.id)
// Only allow b workers to run, when a worker has started initialization.
bWorkerUnlocker.increment()
// Only proceed with initialization, when all b workers have started executing.
while (aWorkerUnlocker.value < bWorkersCount) {}
// And now wait a bit, to increase probability of races.
Worker.current.park(1000 * 1000L)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100 ms would be enough :)

}
val a = produceA()
val b = produceB()
}

fun produceA(): String {
// Must've been called by aWorker only.
assertEquals(aWorkerId, Worker.current.id)
return "A"
}

fun produceB(): String {
// Must've been called by aWorker only.
assertEquals(aWorkerId, Worker.current.id)
// Also check that it's ok to get A.a while initializing A.b.
return "B+${A.a}"
}

@Test fun runTest() {
val aWorker = Worker.start()
// This test relies on aWorkerId value.
assertEquals(aWorkerId, aWorker.id)
val bWorkers = Array(bWorkersCount, { _ -> Worker.start() })

val aFuture = aWorker.execute(TransferMode.SAFE, {}, {
A.b
})
val bFutures = Array(bWorkers.size, {
bWorkers[it].execute(TransferMode.SAFE, {}, {
// Wait until A has started to initialize.
while (bWorkerUnlocker.value < 1) {}
// Now allow A initialization to continue.
aWorkerUnlocker.increment()
// And this should not've tried to init A itself.
A.a + A.b
})
})

for (future in bFutures) {
assertEquals("AB+A", future.result)
}
assertEquals("B+A", aFuture.result)

for (worker in bWorkers) {
worker.requestTermination().result
}
aWorker.requestTermination().result
}
33 changes: 33 additions & 0 deletions backend.native/tests/runtime/basic/initializers7.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/

package runtime.basic.initializers7

import kotlin.test.*

import kotlin.random.Random

object A {
val a1 = Random.nextInt(100)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Random makes the test non-deterministic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean if a1 and a2 become the same number and the test would fail to catch a potential bug? Fair enough, is there another way to dynamically initialize a1 while ensuring compiler won't ever optimize it away?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
Well, IIRC, the compiler doesn't optimize the dynamic initialization even if the initializer is literal.
This can likely be verified by more assertions (checking that values are zero in initial state).

val a2 = Random.nextInt(100)
}

object B {
val b1 = A.a2
val b2 = C.c1
}

object C {
val c1 = Random.nextInt(100)
val c2 = A.a1
val c3 = B.b1
val c4 = B.b2
}

@Test fun runTest() {
assertEquals(A.a1, C.c2)
assertEquals(A.a2, C.c3)
assertEquals(C.c1, C.c4)
}
32 changes: 23 additions & 9 deletions runtime/src/main/cpp/Memory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ struct MemoryState {
uint64_t allocSinceLastGcThreshold;
#endif // USE_GC

// This map is expected to be small, std::map consumes less memory than
// std::unordered_map and is just as efficient.
KStdOrderedMap<ObjHeader**, ObjHeader*> initializingSingletons;
Copy link
Collaborator

@SvyatoslavScherbina SvyatoslavScherbina Mar 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does using stack-like collection here have any effect on the performance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, actually. It outperforms std::map on benchmarks from #4018 And compared to the current implementation: it's 5% faster on a single initialization, and 10% slower on a nested initialization (with depth 500). I think, it's a win overall, since deeply nested initialization should be very rare.


#if COLLECT_STATISTIC
#define CONTAINER_ALLOC_STAT(state, size, container) state->statistic.incAlloc(size, container);
#define CONTAINER_DESTROY_STAT(state, container) \
Expand Down Expand Up @@ -1999,7 +2003,7 @@ OBJ_GETTER(initInstance,

template <bool Strict>
OBJ_GETTER(initSharedInstance,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
ObjHeader** location, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
#if KONAN_NO_THREADS
ObjHeader* value = *location;
if (value != nullptr) {
Expand All @@ -2025,25 +2029,32 @@ OBJ_GETTER(initSharedInstance,
}
#endif // KONAN_NO_EXCEPTIONS
#else // KONAN_NO_THREADS
ObjHeader* value = *localLocation;
if (value != nullptr) RETURN_OBJ(value);
auto it = memoryState->initializingSingletons.find(location);
if (it != memoryState->initializingSingletons.end()) {
RETURN_OBJ(it->second);
}

ObjHeader* initializing = reinterpret_cast<ObjHeader*>(1);

// Spin lock.
ObjHeader* value = nullptr;
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(typeInfo, OBJ_RESULT);
UpdateHeapRef(localLocation, object);
auto insertIt = memoryState->initializingSingletons.insert({location, object});
RuntimeCheck(insertIt.second, "object cannot be assigned twice into initializingSingletons");
addHeapRef(object);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually required?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but what's protecting this reference (and all that it references) from being garbage collected while (potentially long) initialization is taking place?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allocation above actually creates a stack reference. It is alive at least until the return from the function.

#if KONAN_NO_EXCEPTIONS
ctor(object);
if (Strict)
FreezeSubgraph(object);
UpdateHeapRef(location, object);
synchronize();
memoryState->initializingSingletons.erase(location);
releaseHeapRef<Strict>(object);
return object;
#else // KONAN_NO_EXCEPTIONS
try {
Expand All @@ -2052,11 +2063,14 @@ OBJ_GETTER(initSharedInstance,
FreezeSubgraph(object);
UpdateHeapRef(location, object);
synchronize();
memoryState->initializingSingletons.erase(location);
releaseHeapRef<Strict>(object);
return object;
} catch (...) {
UpdateReturnRef(OBJ_RESULT, nullptr);
zeroHeapRef(location);
zeroHeapRef(localLocation);
memoryState->initializingSingletons.erase(location);
releaseHeapRef<Strict>(object);
synchronize();
throw;
}
Expand Down Expand Up @@ -2803,12 +2817,12 @@ OBJ_GETTER(InitInstanceRelaxed,
}

OBJ_GETTER(InitSharedInstanceStrict,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
RETURN_RESULT_OF(initSharedInstance<true>, location, localLocation, typeInfo, ctor);
ObjHeader** location, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
RETURN_RESULT_OF(initSharedInstance<true>, location, typeInfo, ctor);
}
OBJ_GETTER(InitSharedInstanceRelaxed,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
RETURN_RESULT_OF(initSharedInstance<false>, location, localLocation, typeInfo, ctor);
ObjHeader** location, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
RETURN_RESULT_OF(initSharedInstance<false>, location, typeInfo, ctor);
}

void SetStackRefStrict(ObjHeader** location, const ObjHeader* object) {
Expand Down
6 changes: 3 additions & 3 deletions runtime/src/main/cpp/Memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -461,11 +461,11 @@ OBJ_GETTER(InitInstance,
ObjHeader** location, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*));

OBJ_GETTER(InitSharedInstanceStrict,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*));
ObjHeader** location, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*));
OBJ_GETTER(InitSharedInstanceRelaxed,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*));
ObjHeader** location, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*));
OBJ_GETTER(InitSharedInstance,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*));
ObjHeader** location, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*));

// Weak reference operations.
// Atomically clears counter object reference.
Expand Down
4 changes: 2 additions & 2 deletions runtime/src/relaxed/cpp/MemoryImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ OBJ_GETTER(InitInstance,
}

OBJ_GETTER(InitSharedInstance,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
RETURN_RESULT_OF(InitSharedInstanceRelaxed, location, localLocation, typeInfo, ctor);
ObjHeader** location, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
RETURN_RESULT_OF(InitSharedInstanceRelaxed, location, typeInfo, ctor);
}

void ReleaseHeapRef(const ObjHeader* object) {
Expand Down
4 changes: 2 additions & 2 deletions runtime/src/strict/cpp/MemoryImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ OBJ_GETTER(InitInstance,
}

OBJ_GETTER(InitSharedInstance,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
RETURN_RESULT_OF(InitSharedInstanceStrict, location, localLocation, typeInfo, ctor);
ObjHeader** location, const TypeInfo* typeInfo, void (*ctor)(ObjHeader*)) {
RETURN_RESULT_OF(InitSharedInstanceStrict, location, typeInfo, ctor);
}

void ReleaseHeapRef(const ObjHeader* object) {
Expand Down