Freeze objects by default, stdlib improvements. (#1743)

This commit is contained in:
Nikolay Igotti
2018-07-03 16:45:34 +03:00
committed by GitHub
parent c19b9d787f
commit dfc01847c5
16 changed files with 220 additions and 142 deletions
+24
View File
@@ -0,0 +1,24 @@
# Immutability in Kotlin/Native
Kotlin/Native implements strict mutability checks, ensuring
important invariant that object is either immutable or
accessible from the single thread at the moment (`mutable XOR global`).
Immutability is the runtime property in Kotlin/Native, and can be applied
to an arbitrary object subgraph using `konan.worker.freeze` function.
It makes all objects reachable from the given one immutable, and
such a transition is a one way operation (object cannot be unfrozen later).
Some naturally immutable objects, such as `kotlin.String`, `kotlin.Int` and
other primitive types, along with `AtomicInt` and `AtomicReference` are frozen
by default. If mutating operation is applied to a frozen object,
an `InvalidMutabilityException` is thrown.
To achieve `mutable XOR global` invariant all globally visible state (currently,
`object` singletons and enums) are automatically frozen. If an object freezing
is not desirable, `konan.ThreadLocal` annotation could be used, which will make
object state thread local, and thus, mutable (but changed state not visible to
other threads).
Class `AtomicReference` could be used to publish changed frozen state to
other threads, and thus build patterns like shared caches.
@@ -686,7 +686,7 @@ internal class FunctionGenerationContext(val function: LLVMValueRef,
positionAtEnd(bbInit)
val typeInfo = codegen.typeInfoForAllocation(descriptor)
val defaultConstructor = descriptor.constructors.first { it.valueParameters.size == 0 }
val defaultConstructor = descriptor.constructors.single { it.valueParameters.size == 0 }
val ctor = codegen.llvmFunction(defaultConstructor)
val (initFunction, args) =
if (shared) {
@@ -38,13 +38,19 @@ import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.getArguments
import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
val IrClassSymbol.objectIsShared get() = owner.origin == DECLARATION_ORIGIN_ENUM
private val threadLocalAnnotationFqName = FqName("konan.ThreadLocal")
val IrClassSymbol.objectIsShared get() =
!owner.hasAnnotation(threadLocalAnnotationFqName)
internal fun emitLLVM(context: Context) {
val irModule = context.irModule!!
@@ -2662,4 +2668,4 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
internal data class LocationInfo(val scope:DIScopeOpaqueRef,
val line:Int,
val column:Int)
val column:Int)
@@ -562,7 +562,6 @@ internal class EnumClassLowering(val context: Context) : ClassLoweringPass {
result.putValueArgument(1,
IrGetValueImpl(startOffset, endOffset, ordinalParameter.type, ordinalParameter.symbol, origin)
)
return result
}
@@ -155,7 +155,8 @@ internal class InitializersLowering(val context: CommonBackendContext) : ClassLo
}
override fun visitConstructor(declaration: IrConstructor): IrStatement {
val blockBody = declaration.body as? IrBlockBody ?: throw AssertionError("Unexpected constructor body: ${declaration.body}")
val blockBody = declaration.body as? IrBlockBody
?: throw AssertionError("Unexpected constructor body: ${declaration.body}")
blockBody.statements.transformFlat {
when {
+6
View File
@@ -671,6 +671,12 @@ task freeze2(type: RunKonanTest) {
source = "runtime/workers/freeze2.kt"
}
task freeze3(type: RunKonanTest) {
disabled = (project.testTarget == 'wasm32') // No exceptions on WASM.
goldValue = "OK\n"
source = "runtime/workers/freeze3.kt"
}
task atomic0(type: RunKonanTest) {
disabled = (project.testTarget == 'wasm32') // Workers need pthreads.
goldValue = "35\n" + "20\n" + "OK\n"
@@ -0,0 +1,25 @@
package runtime.workers.freeze3
import kotlin.test.*
import konan.worker.*
object Immutable {
var x = 1
}
@konan.ThreadLocal
object Mutable {
var x = 2
}
@Test fun runTest() {
assertEquals(1, Immutable.x)
assertFailsWith<InvalidMutabilityException> {
Immutable.x++
}
assertEquals(1, Immutable.x)
Mutable.x++
assertEquals(3, Mutable.x)
println("OK")
}
+1 -1
View File
@@ -28,7 +28,7 @@ namespace {
ALWAYS_INLINE inline void mutabilityCheck(KConstRef thiz) {
// TODO: optimize it!
if (thiz->container()->frozen()) {
ThrowInvalidMutabilityException();
ThrowInvalidMutabilityException(thiz);
}
}
+2 -3
View File
@@ -16,6 +16,7 @@
#include "Atomic.h"
#include "Common.h"
#include "Exceptions.h"
#include "Memory.h"
#include "Types.h"
@@ -44,8 +45,6 @@ inline AtomicReferenceLayout* asAtomicReference(KRef thiz) {
extern "C" {
RUNTIME_NORETURN void ThrowInvalidMutabilityException();
KInt Kotlin_AtomicInt_addAndGet(KRef thiz, KInt delta) {
return addAndGetImpl(thiz, delta);
}
@@ -68,7 +67,7 @@ KNativePtr Kotlin_AtomicNativePtr_compareAndSwap(KRef thiz, KNativePtr expectedV
void Kotlin_AtomicReference_checkIfFrozen(KRef value) {
if (value != nullptr && !value->container()->permanentOrFrozen()) {
ThrowInvalidMutabilityException();
ThrowInvalidMutabilityException(value);
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ void RUNTIME_NORETURN ThrowNotImplementedError();
// Throws illegal character conversion exception (used in UTF8/UTF16 conversions).
void RUNTIME_NORETURN ThrowIllegalCharacterConversionException();
void RUNTIME_NORETURN ThrowIllegalArgumentException();
void RUNTIME_NORETURN ThrowInvalidMutabilityException();
void RUNTIME_NORETURN ThrowInvalidMutabilityException(KConstRef where);
// Prints out mesage of Throwable.
void PrintThrowable(KRef);
+122 -86
View File
@@ -420,7 +420,6 @@ RUNTIME_USED ContainerHeader theStaticObjectsContainer = {
void objc_release(void* ptr);
void Kotlin_ObjCExport_releaseAssociatedObject(void* associatedObject);
RUNTIME_NORETURN void ThrowFreezingException();
RUNTIME_NORETURN void ThrowInvalidMutabilityException();
} // extern "C"
@@ -1175,7 +1174,28 @@ OBJ_GETTER(InitInstance,
OBJ_GETTER(InitSharedInstance,
ObjHeader** location, ObjHeader** localLocation, const TypeInfo* type_info, void (*ctor)(ObjHeader*)) {
#if KONAN_NO_THREADS
RETURN_RESULT_OF(InitInstance, location, type_info, ctor);
ObjHeader* value = *location;
if (value != nullptr) {
// OK'ish, inited by someone else.
RETURN_OBJ(value);
}
ObjHeader* object = AllocInstance(type_info, OBJ_RESULT);
UpdateRef(location, object);
#if KONAN_NO_EXCEPTIONS
ctor(object);
FreezeSubgraph(object);
return object;
#else
try {
ctor(object);
FreezeSubgraph(object);
return object;
} catch (...) {
UpdateRef(OBJ_RESULT, nullptr);
UpdateRef(location, nullptr);
throw;
}
#endif
#else
ObjHeader* value = *localLocation;
if (value != nullptr) RETURN_OBJ(value);
@@ -1188,28 +1208,27 @@ OBJ_GETTER(InitSharedInstance,
// OK'ish, inited by someone else.
RETURN_OBJ(value);
}
ObjHeader* object = AllocInstance(type_info, OBJ_RESULT);
MEMORY_LOG("Calling UpdateRef from InitInstance\n")
RuntimeAssert(object->container()->normal() , "Shared object cannot be co-allocated");
MEMORY_LOG("Calling UpdateRef from InitSharedInstance\n")
UpdateRef(localLocation, object);
#if KONAN_NO_EXCEPTIONS
ctor(object);
if (!object->container()->frozen())
ThrowFreezingException();
FreezeSubgraph(object);
UpdateRef(location, object);
__sync_synchronize();
return object;
#else
try {
ctor(object);
if (!object->container()->frozen())
ThrowFreezingException();
FreezeSubgraph(object);
UpdateRef(location, object);
__sync_synchronize();
return object;
} catch (...) {
UpdateRef(OBJ_RESULT, nullptr);
UpdateRef(location, nullptr);
UpdateRef(localLocation, nullptr);
__sync_synchronize();
throw;
}
@@ -1550,6 +1569,91 @@ void traverseStronglyConnectedComponent(ContainerHeader* container,
}
}
void freezeAcyclic(ContainerHeader* rootContainer) {
KStdDeque<ContainerHeader*> queue;
queue.push_back(rootContainer);
while (!queue.empty()) {
ContainerHeader* current = queue.front();
queue.pop_front();
current->unMark();
current->resetBuffered();
current->setColor(CONTAINER_TAG_GC_BLACK);
// Note, that once object is frozen, it could be concurrently accessed, so
// color and similar attributes shall not be used.
current->freeze();
traverseContainerReferredObjects(current, [current, &queue](ObjHeader* obj) {
ContainerHeader* objContainer = obj->container();
if (!objContainer->permanentOrFrozen()) {
if (objContainer->marked())
queue.push_back(objContainer);
}
});
}
}
void freezeCyclic(ContainerHeader* rootContainer, const KStdVector<ContainerHeader*>& order) {
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 (!objContainer->permanentOrFrozen()) {
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) {
if (!obj->container()->permanentOrFrozen())
++internalRefsCount;
});
}
// Create fictitious container for the whole component.
auto superContainer = component.size() == 1 ? component[0] : AllocAggregatingFrozenContainer(component);
// Don't count internal references.
superContainer->setRefCount(totalCount - internalRefsCount);
// Freeze component.
for (auto* container : component) {
container->resetBuffered();
container->setColor(CONTAINER_TAG_GC_BLACK);
// Note, that once object is frozen, it could be concurrently accessed, so
// color and similar attributes shall not be used.
container->freeze();
}
}
}
/**
* Theory of operations.
*
@@ -1570,12 +1674,12 @@ void traverseStronglyConnectedComponent(ContainerHeader* container,
* 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 the atomic operations, and so frozen
* references could be passed accross multiple threads.
* 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) {
// TODO: for now, we just check that passed object graph has no cycles, and throw an exception,
// if it does. Next version will run Kosoraju-Sharir if cycles are found.
// 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 (rootContainer->permanentOrFrozen()) return;
@@ -1583,79 +1687,11 @@ void FreezeSubgraph(ObjHeader* root) {
bool hasCycles = false;
KStdVector<ContainerHeader*> order;
depthFirstTraversal(rootContainer, &hasCycles, order);
KStdUnorderedMap<ContainerHeader*, KStdVector<ContainerHeader*>> reversedEdges;
// Now unmark all marked objects, and freeze them, if no cycles detected.
KStdDeque<ContainerHeader*> queue;
queue.push_back(rootContainer);
while (!queue.empty()) {
ContainerHeader* current = queue.front();
queue.pop_front();
current->unMark();
if (hasCycles) {
reversedEdges.emplace(current, KStdVector<ContainerHeader*>(0));
} else {
current->resetBuffered();
current->setColor(CONTAINER_TAG_GC_BLACK);
// Note, that once object is frozen, it could be concurrently accessed, so
// color and similar attributes shall not be used.
current->freeze();
}
traverseContainerReferredObjects(current, [hasCycles, current, &queue, &reversedEdges](ObjHeader* obj) {
ContainerHeader* objContainer = obj->container();
if (!objContainer->permanentOrFrozen()) {
if (objContainer->marked())
queue.push_back(objContainer);
if (hasCycles)
reversedEdges.emplace(objContainer, KStdVector<ContainerHeader*>(0)).first->second.push_back(current);
}
});
}
if (hasCycles) {
KStdVector<KStdVector<ContainerHeader*>> components;
MEMORY_LOG("Condensation:\n");
// Enumerate in 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) {
if (!obj->container()->permanentOrFrozen())
++internalRefsCount;
});
}
auto superContainer = component.size() == 1
? component[0]
: AllocAggregatingFrozenContainer(component); // Create fictitious container for the whole component.
// Don't count internal references.
superContainer->setRefCount(totalCount - internalRefsCount);
// Freeze component.
for (auto* container : component) {
container->resetBuffered();
container->setColor(CONTAINER_TAG_GC_BLACK);
// Note, that once object is frozen, it could be concurrently accessed, so
// color and similar attributes shall not be used.
container->freeze();
}
}
freezeCyclic(rootContainer, order);
} else {
freezeAcyclic(rootContainer );
}
// Now remove frozen objects from the toFree list.
@@ -1671,14 +1707,14 @@ void FreezeSubgraph(ObjHeader* root) {
// 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) {
if (obj->container()->frozen()) ThrowInvalidMutabilityException();
if (obj->container()->frozen()) ThrowInvalidMutabilityException(obj);
}
OBJ_GETTER(SwapRefLocked,
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 lock.
// We do not use UpdateRef() here to avoid having ReleaseRef() on return slot under the lock.
if (oldValue == expectedValue) {
SetRef(location, newValue);
} else {
@@ -1697,7 +1733,7 @@ OBJ_GETTER(SwapRefLocked,
OBJ_GETTER(ReadRefLocked, ObjHeader** location, int32_t* spinlock) {
lock(spinlock);
ObjHeader* value = *location;
// We do not use UpdateRef() here to avoid having ReleaseRef() on return slot under lock.
// We do not use UpdateRef() here to avoid having ReleaseRef() on return slot under the lock.
if (value != nullptr)
AddRef(value);
unlock(spinlock);
+4
View File
@@ -67,6 +67,10 @@ struct ContainerHeader {
// Number of objects in the container.
uint32_t objectCount_;
inline bool normal() const {
return (refCount_ & CONTAINER_TAG_MASK) == CONTAINER_TAG_NORMAL;
}
inline bool permanent() const {
return (refCount_ & CONTAINER_TAG_MASK) == CONTAINER_TAG_PERMANENT;
}
+17 -36
View File
@@ -36,7 +36,7 @@ annotation class SymbolName(val name: String)
annotation class ExportTypeInfo(val name: String)
/**
* * If lambda shall be carefully lowered by the compiler.
* If a lambda shall be carefully lowered by the compiler.
*/
annotation class VolatileLambda
@@ -61,36 +61,6 @@ public annotation class Used
@Retention(AnnotationRetention.SOURCE)
public annotation class Throws(vararg val exceptionClasses: KClass<out Throwable>)
/**
* Need to be fixed because of reification support.
*/
public annotation class FixmeReified
/**
* Need to be fixed because of sorting support.
*/
public annotation class FixmeSorting
/**
* Need to be fixed because of specialization support.
*/
public annotation class FixmeSpecialization
/**
* Need to be fixed because of sequences support.
*/
public annotation class FixmeSequences
/**
* Need to be fixed because of variance support.
*/
public annotation class FixmeVariance
/**
* Need to be fixed because of regular expressions.
*/
public annotation class FixmeRegex
/**
* Need to be fixed because of reflection.
*/
@@ -101,18 +71,29 @@ public annotation class FixmeReflection
*/
public annotation class FixmeConcurrency
public annotation class FixmeInline
/**
* Need to be fixed because of header/impl notation
*/
public annotation class FixmeMultiplatform
/**
* Need to be fixed.
* Need to be fixed because of random support.
*/
public annotation class Fixme
public annotation class FixmeRandom
/**
* Escape analysis annotations.
*/
public annotation class Escapes(val who: Int)
public annotation class PointsTo(vararg val onWhom: Int)
/**
* Need to be fixed because of header/impl notation
* Top level variable or object is thread local, and so could be mutable.
* One may use this annotation as the stopgap measure for singleton
* object immutability.
* PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES.
*/
public annotation class FixmeMultiplatform
@Target(AnnotationTarget.FIELD, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
annotation class ThreadLocal
@@ -26,7 +26,8 @@ public class FreezingException() : RuntimeException()
/**
* Exception thrown whenever we attempt to mutate frozen objects.
*/
public class InvalidMutabilityException() : RuntimeException()
public class InvalidMutabilityException(where: Any) :
RuntimeException("mutation attempt of frozen $where (hash is 0x${where.hashCode().toString(16)})")
/**
* Freezes object subgraph reachable from this object. Frozen objects can be freely
@@ -50,4 +51,4 @@ internal external fun isFrozenInternal(it: Any?): Boolean
internal fun ThrowFreezingException(): Nothing = throw FreezingException()
@ExportForCppRuntime
internal fun ThrowInvalidMutabilityException(): Nothing = throw InvalidMutabilityException()
internal fun ThrowInvalidMutabilityException(where: Any): Nothing = throw InvalidMutabilityException(where)
@@ -18,10 +18,7 @@ package kotlin.collections
import kotlin.comparisons.*
// copies typed varargs array to array of objects
// TODO: generally wrong, wrt specialization.
@FixmeSpecialization
// Copies typed varargs array to an array of objects
internal actual fun <T> Array<out T>.copyToArrayOfAny(isVarargs: Boolean): Array<out Any?> =
if (isVarargs)
// if the array came from varargs and already is array of Any, copying isn't required.
@@ -232,6 +229,7 @@ public actual fun <T> MutableList<T>.fill(value: T): Unit {
*
* See: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
*/
@FixmeRandom
@SinceKotlin("1.2")
public actual fun <T> MutableList<T>.shuffle(): Unit {
for (i in lastIndex downTo 1) {
@@ -64,11 +64,9 @@ public actual fun <R, T> (suspend R.() -> T).createCoroutineUnchecked(
(this.create(receiver, completion) as CoroutineImpl).facade
// INTERNAL DEFINITIONS
// TODO: uncomment as soon as inlining works for stdlib.
@FixmeInline
private /*inline*/ fun <T> buildContinuationByInvokeCall(
private inline fun <T> buildContinuationByInvokeCall(
completion: Continuation<T>,
/*crossinline*/ block: () -> Any?
crossinline block: () -> Any?
): Continuation<Unit> {
val continuation =
object : Continuation<Unit> {