From 31f4655ebd25e40022386939fc5a9c88a20fe46d Mon Sep 17 00:00:00 2001 From: Igor Chevdar Date: Thu, 9 Jul 2020 17:38:37 +0500 Subject: [PATCH] [optmz][codegen] Rewrote escape analysis --- .../kotlin/backend/konan/GraphAlgorithms.kt | 1 + .../kotlin/backend/konan/ToplevelPhases.kt | 4 +- .../backend/konan/llvm/BitcodePhases.kt | 32 +- .../backend/konan/llvm/CodeGenerator.kt | 235 ++- .../kotlin/backend/konan/llvm/ContextUtils.kt | 14 +- .../backend/konan/llvm/IntrinsicGenerator.kt | 4 +- .../kotlin/backend/konan/llvm/IrToBitcode.kt | 15 +- .../objcexport/ObjCExportCodeGenerator.kt | 2 +- .../konan/optimizations/CallGraphBuilder.kt | 224 +-- .../backend/konan/optimizations/DFGBuilder.kt | 4 +- .../backend/konan/optimizations/DataFlowIR.kt | 9 +- .../konan/optimizations/EscapeAnalysis.kt | 1678 +++++++++++++---- .../optimizations/LocalEscapeAnalysis.kt | 3 +- runtime/src/main/cpp/Memory.cpp | 8 + runtime/src/main/cpp/Memory.h | 4 +- runtime/src/main/kotlin/kotlin/Array.kt | 4 +- runtime/src/main/kotlin/kotlin/Arrays.kt | 2 +- .../kotlin/kotlin/collections/ArrayUtil.kt | 6 +- .../src/main/kotlin/kotlin/native/Runtime.kt | 2 + .../kotlin/native/internal/Annotations.kt | 1 + .../kotlin/kotlin/native/ref/WeakPrivate.kt | 2 + 21 files changed, 1684 insertions(+), 570 deletions(-) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/GraphAlgorithms.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/GraphAlgorithms.kt index 9cdbf4ced58..8e08a573852 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/GraphAlgorithms.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/GraphAlgorithms.kt @@ -20,6 +20,7 @@ internal class DirectedGraphMultiNode(val nodes: Set) internal class DirectedGraphCondensation(val topologicalOrder: List>) +// The Kosoraju-Sharir algorithm. internal class DirectedGraphCondensationBuilder>(private val graph: DirectedGraph) { private val visited = mutableSetOf() private val order = mutableListOf() diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt index a3b1bbd623e..4ee646401f1 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt @@ -522,7 +522,7 @@ internal fun PhaseConfig.disableUnless(phase: AnyNamedPhase, condition: Boolean) internal fun PhaseConfig.konanPhasesConfig(config: KonanConfig) { with(config.configuration) { disable(compileTimeEvaluatePhase) - disable(escapeAnalysisPhase) + disable(localEscapeAnalysisPhase) // Don't serialize anything to a final executable. disableUnless(serializerPhase, config.produce == CompilerOutputKind.LIBRARY) @@ -536,7 +536,7 @@ internal fun PhaseConfig.konanPhasesConfig(config: KonanConfig) { disableIf(testProcessorPhase, getNotNull(KonanConfigKeys.GENERATE_TEST_RUNNER) == TestRunnerKind.NONE) disableUnless(buildDFGPhase, getBoolean(KonanConfigKeys.OPTIMIZATION)) disableUnless(devirtualizationPhase, getBoolean(KonanConfigKeys.OPTIMIZATION)) - disableUnless(localEscapeAnalysisPhase, getBoolean(KonanConfigKeys.OPTIMIZATION)) + disableUnless(escapeAnalysisPhase, getBoolean(KonanConfigKeys.OPTIMIZATION)) disableUnless(dcePhase, getBoolean(KonanConfigKeys.OPTIMIZATION)) disableUnless(ghaPhase, getBoolean(KonanConfigKeys.OPTIMIZATION)) disableUnless(verifyBitcodePhase, config.needCompilerVerification || getBoolean(KonanConfigKeys.VERIFY_BITCODE)) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt index 3a390486cb3..3b0cfd58d2b 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt @@ -129,10 +129,15 @@ internal val dcePhase = makeKonanModuleOpPhase( context, context.moduleDFG!!, externalModulesDFG, context.devirtualizationAnalysisResult!!, - true + // For DCE we don't wanna miss any potentially reachable function. + nonDevirtualizedCallSitesUnfoldFactor = Int.MAX_VALUE ).build() val referencedFunctions = mutableSetOf() + callGraph.rootExternalFunctions.forEach { + if (!it.isGlobalInitializer) + referencedFunctions.add(it.irFunction ?: error("No IR for: $it")) + } for (node in callGraph.directEdges.values) { if (!node.symbol.isGlobalInitializer) referencedFunctions.add(node.symbol.irFunction ?: error("No IR for: ${node.symbol}")) @@ -200,22 +205,23 @@ internal val dcePhase = makeKonanModuleOpPhase( ) internal val escapeAnalysisPhase = makeKonanModuleOpPhase( - // Disabled by default !!!! name = "EscapeAnalysis", description = "Escape analysis", prerequisite = setOf(buildDFGPhase, devirtualizationPhase), op = { context, _ -> - context.externalModulesDFG?.let { externalModulesDFG -> - val callGraph = CallGraphBuilder( - context, context.moduleDFG!!, - externalModulesDFG, - context.devirtualizationAnalysisResult!!, - false - ).build() - EscapeAnalysis.computeLifetimes( - context, context.moduleDFG!!, externalModulesDFG, callGraph, context.lifetimes - ) - } + val entryPoint = context.ir.symbols.entryPoint?.owner + val externalModulesDFG = ExternalModulesDFG(emptyList(), emptyMap(), emptyMap(), emptyMap()) + val callGraph = CallGraphBuilder( + context, context.moduleDFG!!, + externalModulesDFG, + context.devirtualizationAnalysisResult!!, + // Can't tolerate any non-devirtualized call site for a library. + // TODO: What about private virtual functions? + nonDevirtualizedCallSitesUnfoldFactor = if (entryPoint == null) 0 else 5 + ).build() + EscapeAnalysis.computeLifetimes( + context, context.moduleDFG!!, externalModulesDFG, callGraph, context.lifetimes + ) } ) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt index ed35db3b41a..630e1e6454d 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt @@ -15,7 +15,6 @@ import org.jetbrains.kotlin.konan.target.CompilerOutputKind import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.util.* -import org.jetbrains.kotlin.backend.konan.descriptors.resolveFakeOverride import org.jetbrains.kotlin.backend.konan.ir.* import org.jetbrains.kotlin.descriptors.konan.CompiledKlibModuleOrigin import org.jetbrains.kotlin.ir.expressions.IrDelegatingConstructorCall @@ -161,6 +160,143 @@ private inline fun generateFunctionBody( */ internal data class LocationInfoRange(var start: LocationInfo, var end: LocationInfo?) +internal interface StackLocalsManager { + fun alloc(irClass: IrClass, cleanFieldsExplicitly: Boolean): LLVMValueRef + + fun allocArray(irClass: IrClass, count: LLVMValueRef): LLVMValueRef + + fun clean(refsOnly: Boolean) +} + +internal class StackLocalsManagerImpl( + val functionGenerationContext: FunctionGenerationContext, + val bbInitStackLocals: LLVMBasicBlockRef +) : StackLocalsManager { + private class StackLocal(val isArray: Boolean, val irClass: IrClass, + val bodyPtr: LLVMValueRef, val objHeaderPtr: LLVMValueRef) + + private val stackLocals = mutableListOf() + + override fun alloc(irClass: IrClass, cleanFieldsExplicitly: Boolean): LLVMValueRef = with(functionGenerationContext) { + val type = context.llvmDeclarations.forClass(irClass).bodyType + val stackLocal = appendingTo(bbInitStackLocals) { + val stackSlot = LLVMBuildAlloca(builder, type, "")!! + + call(context.llvm.memsetFunction, + listOf(bitcast(kInt8Ptr, stackSlot), + Int8(0).llvm, + Int32(LLVMSizeOfTypeInBits(codegen.llvmTargetData, type).toInt() / 8).llvm, + Int1(0).llvm)) + + val objectHeader = structGep(stackSlot, 0, "objHeader") + val typeInfo = codegen.typeInfoForAllocation(irClass) + setTypeInfoForLocalObject(objectHeader, typeInfo) + StackLocal(false, irClass, stackSlot, objectHeader) + } + + stackLocals += stackLocal + if (cleanFieldsExplicitly) + clean(stackLocal, false) + stackLocal.objHeaderPtr + } + + // Returns generated special type for local array. + // It's needed to prevent changing variables order on stack. + private fun localArrayType(irClass: IrClass, count: Int) = with(functionGenerationContext) { + val name = "local#${irClass.name}${count}#internal" + // Create new type or get already created. + context.declaredLocalArrays.getOrPut(name) { + val fieldTypes = listOf(kArrayHeader, LLVMArrayType(arrayToElementType[irClass.symbol]!!, count)) + val classType = LLVMStructCreateNamed(LLVMGetModuleContext(context.llvmModule), name)!! + LLVMStructSetBody(classType, fieldTypes.toCValues(), fieldTypes.size, 1) + classType + } + } + + private val symbols = functionGenerationContext.context.ir.symbols + // TODO: find better place? + private val arrayToElementType = mapOf( + symbols.array to functionGenerationContext.kObjHeaderPtr, + symbols.byteArray to int8Type, + symbols.charArray to int16Type, + symbols.shortArray to int16Type, + symbols.intArray to int32Type, + symbols.longArray to int64Type, + symbols.floatArray to floatType, + symbols.doubleArray to doubleType, + symbols.booleanArray to int8Type + ) + + override fun allocArray(irClass: IrClass, count: LLVMValueRef) = with(functionGenerationContext) { + val stackLocal = appendingTo(bbInitStackLocals) { + val constCount = extractConstUnsignedInt(count).toInt() + val arrayType = localArrayType(irClass, constCount) + val typeInfo = codegen.typeInfoValue(irClass) + val arraySlot = LLVMBuildAlloca(builder, arrayType, "")!! + // Set array size in ArrayHeader. + val arrayHeaderSlot = structGep(arraySlot, 0, "arrayHeader") + setTypeInfoForLocalObject(arrayHeaderSlot, typeInfo) + val sizeField = structGep(arrayHeaderSlot, 1, "count_") + store(count, sizeField) + + call(context.llvm.memsetFunction, + listOf(bitcast(kInt8Ptr, structGep(arraySlot, 1, "arrayBody")), + Int8(0).llvm, + Int32(constCount * LLVMSizeOfTypeInBits(codegen.llvmTargetData, + arrayToElementType[irClass.symbol]).toInt() / 8).llvm, + Int1(0).llvm)) + StackLocal(true, irClass, arraySlot, arrayHeaderSlot) + } + + stackLocals += stackLocal + bitcast(kObjHeaderPtr, stackLocal.objHeaderPtr) + } + + override fun clean(refsOnly: Boolean) = stackLocals.forEach { clean(it, refsOnly) } + + private fun clean(stackLocal: StackLocal, refsOnly: Boolean) = with(functionGenerationContext) { + if (stackLocal.isArray) { + if (stackLocal.irClass.symbol == context.ir.symbols.array) + call(context.llvm.zeroArrayRefsFunction, listOf(stackLocal.objHeaderPtr)) + } else { + val type = context.llvmDeclarations.forClass(stackLocal.irClass).bodyType + for (field in context.getLayoutBuilder(stackLocal.irClass).fields) { + val fieldIndex = context.llvmDeclarations.forField(field).index + val fieldType = LLVMStructGetTypeAtIndex(type, fieldIndex)!! + + if (isObjectType(fieldType)) { + val fieldPtr = LLVMBuildStructGEP(builder, stackLocal.bodyPtr, fieldIndex, "")!! + if (refsOnly) + storeHeapRef(kNullObjHeaderPtr, fieldPtr) + else + call(context.llvm.zeroHeapRefFunction, listOf(fieldPtr)) + } + } + + if (!refsOnly) { + val bodyPtr = ptrToInt(stackLocal.bodyPtr, codegen.intPtrType) + val bodySize = LLVMSizeOfTypeInBits(codegen.llvmTargetData, type).toInt() / 8 + val serviceInfoSize = runtime.pointerSize + val serviceInfoSizeLlvm = LLVMConstInt(codegen.intPtrType, serviceInfoSize.toLong(), 1)!! + val bodyWithSkippedServiceInfoPtr = intToPtr(add(bodyPtr, serviceInfoSizeLlvm), kInt8Ptr) + call(context.llvm.memsetFunction, + listOf(bodyWithSkippedServiceInfoPtr, + Int8(0).llvm, + Int32(bodySize - serviceInfoSize).llvm, + Int1(0).llvm)) + } + } + } + + private fun setTypeInfoForLocalObject(objectHeader: LLVMValueRef, typeInfoPointer: LLVMValueRef) = with(functionGenerationContext) { + val typeInfo = structGep(objectHeader, 0, "typeInfoOrMeta_") + // Set tag OBJECT_TAG_PERMANENT_CONTAINER | OBJECT_TAG_NONTRIVIAL_CONTAINER. + val typeInfoValue = intToPtr(or(ptrToInt(typeInfoPointer, codegen.intPtrType), + codegen.immThreeIntPtrType), kTypeInfoPtr) + store(typeInfoValue, typeInfo) + } +} + internal class FunctionGenerationContext(val function: LLVMValueRef, val codegen: CodeGenerator, startLocation: LocationInfo?, @@ -192,10 +328,13 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, private val prologueBb = basicBlockInFunction("prologue", startLocation) private val localsInitBb = basicBlockInFunction("locals_init", startLocation) + private val stackLocalsInitBb = basicBlockInFunction("stack_locals_init", startLocation) private val entryBb = basicBlockInFunction("entry", startLocation) private val epilogueBb = basicBlockInFunction("epilogue", endLocation) private val cleanupLandingpad = basicBlockInFunction("cleanup_landingpad", endLocation) + val stackLocalsManager = StackLocalsManagerImpl(this, stackLocalsInitBb) + /** * TODO: consider merging this with [ExceptionHandler]. */ @@ -348,7 +487,7 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, // This allows appropriate rootset accounting by just looking at the stack slots, // along with ability to allocate in appropriate arena. val resultSlot = when (resultLifetime.slotType) { - SlotType.ARENA -> { + SlotType.STACK -> { localAllocs++ // Case of local call. Use memory allocated on stack. val type = LLVMGetReturnType(LLVMGetElementType(llvmFunction.type))!! @@ -426,84 +565,29 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, fun allocInstance(typeInfo: LLVMValueRef, lifetime: Lifetime): LLVMValueRef = call(context.llvm.allocInstanceFunction, listOf(typeInfo), lifetime) - fun allocInstance(irClass: IrClass, lifetime: Lifetime): LLVMValueRef { - val typeInfo = codegen.typeInfoForAllocation(irClass) - return if (lifetime == Lifetime.LOCAL) { - val stackSlot = alloca(context.llvmDeclarations.forClass(irClass).bodyType) - val objectHeader = structGep(stackSlot, 0, "objHeader") - setTypeInfoForLocalObject(objectHeader, typeInfo) - objectHeader - } else { - allocInstance(typeInfo, lifetime) - } - } + fun allocInstance(irClass: IrClass, lifetime: Lifetime, stackLocalsManager: StackLocalsManager) = + if (lifetime == Lifetime.STACK) + stackLocalsManager.alloc(irClass, + // In case the allocation is not from the root scope, fields must be cleaned up explicitly, + // as the object might be being reused. + cleanFieldsExplicitly = stackLocalsManager != this.stackLocalsManager) + else + allocInstance(codegen.typeInfoForAllocation(irClass), lifetime) - // TODO: find better place? - val arrayToElementType = mapOf( - "kotlin.ByteArray" to int8Type, - "kotlin.CharArray" to int16Type, - "kotlin.ShortArray" to int16Type, - "kotlin.IntArray" to int32Type, - "kotlin.LongArray" to int64Type, - "kotlin.FloatArray" to floatType, - "kotlin.DoubleArray" to doubleType, - "kotlin.BooleanArray" to int8Type - ) - - // Returns generated special type for local array. - // It's needed to prevent changing variables order on stack. - // TODO: add alignment calculation. Now it isn't needed, because of working only with primitive types. - private fun localArrayType(className: String, count: Int): LLVMTypeRef { - val name = "local#${className}${count}#internal" - // Create new type or get already created. - return context.declaredLocalArrays.getOrPut(name) { - val fieldTypes = listOf(kArrayHeader, LLVMArrayType(arrayToElementType[className]!!, count)) - val classType = LLVMStructCreateNamed(LLVMGetModuleContext(context.llvmModule), name)!! - LLVMStructSetBody(classType, fieldTypes.toCValues(), fieldTypes.size, 1) - classType - } - } - - fun allocArray(irClass: IrClass, - count: LLVMValueRef, - lifetime: Lifetime, - exceptionHandler: ExceptionHandler): LLVMValueRef { + fun allocArray( + irClass: IrClass, + count: LLVMValueRef, + lifetime: Lifetime, + exceptionHandler: ExceptionHandler + ): LLVMValueRef { val typeInfo = codegen.typeInfoValue(irClass) - return if (lifetime == Lifetime.LOCAL) { - val className = irClass.fqNameForIrSerialization.asString() - assert(className in arrayToElementType) - allocArrayOnStack(className, count, typeInfo) + return if (lifetime == Lifetime.STACK) { + stackLocalsManager.allocArray(irClass, count) } else { call(context.llvm.allocArrayFunction, listOf(typeInfo, count), lifetime, exceptionHandler) } } - fun setTypeInfoForLocalObject(objectHeader: LLVMValueRef, typeInfoPointer: LLVMValueRef) { - val typeInfo = structGep(objectHeader, 0, "typeInfoOrMeta_") - // Set tag OBJECT_TAG_PERMANENT_CONTAINER | OBJECT_TAG_NONTRIVIAL_CONTAINER. - val typeInfoValue = intToPtr(or(ptrToInt(typeInfoPointer, codegen.intPtrType), - codegen.immThreeIntPtrType), kTypeInfoPtr) - store(typeInfoValue, typeInfo) - } - - fun allocArrayOnStack(arrayTypeName: String, count: LLVMValueRef, typeInfo: LLVMValueRef): LLVMValueRef { - val constCount = extractConstUnsignedInt(count).toInt() - val arrayType = localArrayType(arrayTypeName, constCount) - val arraySlot = alloca(arrayType) - // Set array size in ArrayHeader. - val arrayHeaderSlot = structGep(arraySlot, 0, "arrayHeader") - setTypeInfoForLocalObject(arrayHeaderSlot, typeInfo) - val sizeField = structGep(arrayHeaderSlot, 1, "count_") - store(count, sizeField) - call(context.llvm.memsetFunction, - listOf(bitcast(kInt8Ptr, structGep(arraySlot, 1, "arrayBody")), - Int8(0).llvm, - Int32(constCount * LLVMSizeOfTypeInBits(codegen.llvmTargetData, - arrayToElementType[arrayTypeName]).toInt() / 8).llvm, - Int1(0).llvm)) - return bitcast(kObjHeaderPtr, arrayHeaderSlot) - } - fun unreachable(): LLVMValueRef? { if (context.config.debug) { call(context.llvm.llvmTrap, emptyList()) @@ -1112,6 +1196,10 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, } appendingTo(localsInitBb) { + br(stackLocalsInitBb) + } + + appendingTo(stackLocalsInitBb) { br(entryBb) } @@ -1293,6 +1381,7 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, call(context.llvm.leaveFrameFunction, listOf(slotsPhi!!, Int32(vars.skipSlots).llvm, Int32(slotCount).llvm)) } + stackLocalsManager.clean(refsOnly = true) // Only bother about not leaving any dangling references. } } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt index 8218bd2c872..2f03641df97 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt @@ -34,6 +34,9 @@ import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty internal sealed class SlotType { + // An object is statically allocated on stack. + object STACK : SlotType() + // Frame local arena slot can be used. object ARENA : SlotType() @@ -58,6 +61,12 @@ internal sealed class SlotType { // Lifetimes class of reference, computed by escape analysis. internal sealed class Lifetime(val slotType: SlotType) { + object STACK : Lifetime(SlotType.STACK) { + override fun toString(): String { + return "STACK" + } + } + // If reference is frame-local (only obtained from some call and never leaves). object LOCAL : Lifetime(SlotType.ARENA) { override fun toString(): String { @@ -66,7 +75,7 @@ internal sealed class Lifetime(val slotType: SlotType) { } // If reference is only returned. - object RETURN_VALUE : Lifetime(SlotType.RETURN) { + object RETURN_VALUE : Lifetime(SlotType.ANONYMOUS) { override fun toString(): String { return "RETURN_VALUE" } @@ -492,8 +501,11 @@ internal class Llvm(val context: Context, val llvmModule: LLVMModuleRef) { val initInstanceFunction = importModelSpecificRtFunction("InitInstance") val initSharedInstanceFunction = importModelSpecificRtFunction("InitSharedInstance") val updateHeapRefFunction = importModelSpecificRtFunction("UpdateHeapRef") + val releaseHeapRefFunction = importModelSpecificRtFunction("ReleaseHeapRef") val updateStackRefFunction = importModelSpecificRtFunction("UpdateStackRef") val updateReturnRefFunction = importModelSpecificRtFunction("UpdateReturnRef") + val zeroHeapRefFunction = importRtFunction("ZeroHeapRef") + val zeroArrayRefsFunction = importRtFunction("ZeroArrayRefs") val enterFrameFunction = importModelSpecificRtFunction("EnterFrame") val leaveFrameFunction = importModelSpecificRtFunction("LeaveFrame") val lookupOpenMethodFunction = importRtFunction("LookupOpenMethod") diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IntrinsicGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IntrinsicGenerator.kt index af3b3237e39..c3613e4438a 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IntrinsicGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IntrinsicGenerator.kt @@ -103,6 +103,8 @@ internal interface IntrinsicGeneratorEnvironment { val exceptionHandler: ExceptionHandler + val stackLocalsManager: StackLocalsManager + fun calculateLifetime(element: IrElement): Lifetime fun evaluateCall(function: IrFunction, args: List, resultLifetime: Lifetime, superClass: IrClass? = null): LLVMValueRef @@ -303,7 +305,7 @@ internal class IntrinsicGenerator(private val environment: IntrinsicGeneratorEnv val typeParameterT = context.ir.symbols.createUninitializedInstance.descriptor.typeParameters[0] val enumClass = callSite.getTypeArgument(typeParameterT)!! val enumIrClass = enumClass.getClass()!! - return allocInstance(enumIrClass, environment.calculateLifetime(callSite)) + return allocInstance(enumIrClass, environment.calculateLifetime(callSite), environment.stackLocalsManager) } private fun FunctionGenerationContext.emitGetPointerSize(): LLVMValueRef = diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt index 73da41f9216..0b3aef5223a 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt @@ -134,6 +134,8 @@ internal interface CodeContext { fun genThrow(exception: LLVMValueRef) + val stackLocalsManager: StackLocalsManager + /** * Declares the variable. * @return index of declared variable. @@ -218,6 +220,9 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map, resultLifetime: Lifetime, superClass: IrClass?) = evaluateSimpleFunctionCall(function, args, resultLifetime, superClass) @@ -248,6 +253,8 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map error("Call should've been lowered: ${callee.dump()}") - else -> functionGenerationContext.allocInstance(constructedClass, resultLifetime(callee)) + else -> functionGenerationContext.allocInstance(constructedClass, resultLifetime(callee), + currentCodeContext.stackLocalsManager) } evaluateSimpleFunctionCall(callee.symbol.owner, listOf(thisValue) + args, Lifetime.IRRELEVANT /* constructor doesn't return anything */) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt index 030b7786125..2e787f030d5 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt @@ -857,7 +857,7 @@ private fun ObjCExportCodeGenerator.generateObjCImp( val coroutineSuspended = callFromBridge( codegen.llvmFunction(context.ir.symbols.objCExportGetCoroutineSuspended.owner), emptyList(), - Lifetime.LOCAL + Lifetime.STACK ) ifThen(icmpNe(targetResult!!, coroutineSuspended)) { // Callee haven't suspended, so it isn't going to call the completion. Call it here: diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/CallGraphBuilder.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/CallGraphBuilder.kt index fdb6e08c0d0..1d22117b390 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/CallGraphBuilder.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/CallGraphBuilder.kt @@ -11,18 +11,20 @@ import org.jetbrains.kotlin.backend.konan.DirectedGraph import org.jetbrains.kotlin.backend.konan.DirectedGraphNode import org.jetbrains.kotlin.backend.konan.Context -internal class CallGraphNode(val graph: CallGraph, val symbol: DataFlowIR.FunctionSymbol) - : DirectedGraphNode { +internal class CallGraphNode(val graph: CallGraph, val symbol: DataFlowIR.FunctionSymbol.Declared) + : DirectedGraphNode { override val key get() = symbol - override val directEdges: List by lazy { + override val directEdges: List by lazy { graph.directEdges[symbol]!!.callSites + .filter { !it.isVirtual } .map { it.actualCallee } - .filter { graph.reversedEdges.containsKey(it) } + .filterIsInstance() + .filter { graph.directEdges.containsKey(it) } } - override val reversedEdges: List by lazy { + override val reversedEdges: List by lazy { graph.reversedEdges[symbol]!! } @@ -31,26 +33,31 @@ internal class CallGraphNode(val graph: CallGraph, val symbol: DataFlowIR.Functi val callSites = mutableListOf() } -internal class CallGraph(val directEdges: Map, - val reversedEdges: Map>) - : DirectedGraph { +internal class CallGraph(val directEdges: Map, + val reversedEdges: Map>, + val rootExternalFunctions: List) + : DirectedGraph { override val nodes get() = directEdges.values - override fun get(key: DataFlowIR.FunctionSymbol) = directEdges[key]!! + override fun get(key: DataFlowIR.FunctionSymbol.Declared) = directEdges[key]!! - fun addEdge(caller: DataFlowIR.FunctionSymbol, callSite: CallGraphNode.CallSite) { + fun addEdge(caller: DataFlowIR.FunctionSymbol.Declared, callSite: CallGraphNode.CallSite) { directEdges[caller]!!.callSites += callSite - reversedEdges[callSite.actualCallee]?.add(caller) } + fun addReversedEdge(caller: DataFlowIR.FunctionSymbol.Declared, callee: DataFlowIR.FunctionSymbol.Declared) { + reversedEdges[callee]!!.add(caller) + } } -internal class CallGraphBuilder(val context: Context, - val moduleDFG: ModuleDFG, - val externalModulesDFG: ExternalModulesDFG, - val devirtualizationAnalysisResult: Devirtualization.AnalysisResult, - val gotoExternal: Boolean) { +internal class CallGraphBuilder( + val context: Context, + val moduleDFG: ModuleDFG, + val externalModulesDFG: ExternalModulesDFG, + val devirtualizationAnalysisResult: Devirtualization.AnalysisResult, + val nonDevirtualizedCallSitesUnfoldFactor: Int +) { private val DEBUG = 0 @@ -66,47 +73,45 @@ internal class CallGraphBuilder(val context: Context, return this } - private val visitedFunctions = mutableSetOf() - private val directEdges = mutableMapOf() - private val reversedEdges = mutableMapOf>() - private val callGraph = CallGraph(directEdges, reversedEdges) - private val functionStack = mutableListOf() + private val directEdges = mutableMapOf() + private val reversedEdges = mutableMapOf>() + private val externalRootFunctions = mutableListOf() + private val callGraph = CallGraph(directEdges, reversedEdges, externalRootFunctions) + + private data class HandleFunctionParams(val caller: DataFlowIR.FunctionSymbol.Declared?, + val calleeFunction: DataFlowIR.Function) + private val functionStack = mutableListOf() fun build(): CallGraph { val rootSet = Devirtualization.computeRootSet(context, moduleDFG, externalModulesDFG) - @Suppress("LoopToCallChain") - for (symbol in rootSet) - functionStack.push(symbol) + for (symbol in rootSet) { + val function = moduleDFG.functions[symbol] + if (function == null) + externalRootFunctions.add(symbol) + else + functionStack.push(HandleFunctionParams(null, function)) + } while (functionStack.isNotEmpty()) { - val symbol = functionStack.pop() - if (symbol !in visitedFunctions) - handleFunction(symbol) + val (caller, calleeFunction) = functionStack.pop() + val callee = calleeFunction.symbol as DataFlowIR.FunctionSymbol.Declared + val gotoCallee = !directEdges.containsKey(callee) + if (gotoCallee) + addNode(callee) + if (caller != null) + callGraph.addReversedEdge(caller, callee) + if (gotoCallee) + handleFunction(callee, calleeFunction) } - - DEBUG_OUTPUT(0) { - println("DirectEdges: ${directEdges.size}") - println("ReversedEdges: ${reversedEdges.size}") - println("Sum: ${directEdges.values.sumBy { it.callSites.size }}") - } - return callGraph } - private fun addNode(symbol: DataFlowIR.FunctionSymbol) { - if (directEdges.containsKey(symbol)) - return - val node = CallGraphNode(callGraph, symbol) - directEdges.put(symbol, node) - val list = mutableListOf() - reversedEdges.put(symbol, list) + private fun addNode(symbol: DataFlowIR.FunctionSymbol.Declared) { + directEdges[symbol] = CallGraphNode(callGraph, symbol) + reversedEdges[symbol] = mutableListOf() } - private val symbols = context.ir.symbols - private val arrayGet = symbols.arrayGet[symbols.array]!!.owner - private val arraySet = symbols.arraySet[symbols.array]!!.owner - - private inline fun DataFlowIR.FunctionBody.forEachCallSite(block: (DataFlowIR.Node.Call) -> Unit) = + private inline fun DataFlowIR.FunctionBody.forEachCallSite(block: (DataFlowIR.Node.Call) -> Unit): Unit = forEachNonScopeNode { node -> when (node) { is DataFlowIR.Node.Call -> block(node) @@ -137,92 +142,63 @@ internal class CallGraphBuilder(val context: Context, returnType = node.symbol.returnParameter.type, irCallSite = null )) + + else -> { } } } - private fun staticCall(caller: DataFlowIR.FunctionSymbol, call: DataFlowIR.Node.Call, callee: DataFlowIR.FunctionSymbol) { - callGraph.addEdge(caller, CallGraphNode.CallSite(call, false, callee)) - if (callee is DataFlowIR.FunctionSymbol.Declared - && !directEdges.containsKey(callee)) - functionStack.push(callee) + private fun staticCall(caller: DataFlowIR.FunctionSymbol.Declared, call: DataFlowIR.Node.Call, callee: DataFlowIR.FunctionSymbol) { + val resolvedCallee = callee.resolved() + val callSite = CallGraphNode.CallSite(call, false, resolvedCallee) + val function = moduleDFG.functions[resolvedCallee] + callGraph.addEdge(caller, callSite) + if (function != null) + functionStack.push(HandleFunctionParams(caller, function)) } - private fun handleFunction(symbol: DataFlowIR.FunctionSymbol) { - visitedFunctions += symbol - if (gotoExternal) { - addNode(symbol) - val function = moduleDFG.functions[symbol] ?: externalModulesDFG.functionDFGs[symbol] ?: return - val body = function.body - body.forEachCallSite { call -> - val devirtualizedCallSite = (call as? DataFlowIR.Node.VirtualCall)?.let { devirtualizedCallSites[it] } - when { - call !is DataFlowIR.Node.VirtualCall -> staticCall(symbol, call, call.callee.resolved()) + private fun handleFunction(symbol: DataFlowIR.FunctionSymbol.Declared, function: DataFlowIR.Function) { + val body = function.body + body.forEachCallSite { call -> + val devirtualizedCallSite = (call as? DataFlowIR.Node.VirtualCall)?.let { devirtualizedCallSites[it] } + when { + call !is DataFlowIR.Node.VirtualCall -> staticCall(symbol, call, call.callee) - devirtualizedCallSite != null -> { - devirtualizedCallSite.possibleCallees.forEach { - staticCall(symbol, call, it.callee.resolved()) - } - } - - call.receiverType == DataFlowIR.Type.Virtual -> { - // Skip callsite. This can only be for invocations Any's methods on instances of ObjC classes. - } - - else -> { - // Callsite has not been devirtualized - conservatively assume the worst: - // any inheritor of the receiver type is possible here. - val typeHierarchy = devirtualizationAnalysisResult.typeHierarchy - typeHierarchy.inheritorsOf(call.receiverType as DataFlowIR.Type.Declared).forEachBit { - val receiverType = typeHierarchy.allTypes[it] - if (receiverType.isAbstract) return@forEachBit - // TODO: Unconservative way - when we can use it? - //.filter { devirtualizationAnalysisResult.instantiatingClasses.contains(it) } - val actualCallee = when (call) { - is DataFlowIR.Node.VtableCall -> - receiverType.vtable[call.calleeVtableIndex] - - is DataFlowIR.Node.ItableCall -> - receiverType.itable[call.calleeHash]!! - - else -> error("Unreachable") - } - staticCall(symbol, call, actualCallee.resolved()) - } + devirtualizedCallSite != null -> { + devirtualizedCallSite.possibleCallees.forEach { + staticCall(symbol, call, it.callee) } } - } - } else { - var function = moduleDFG.functions[symbol] - var local = true - if (function != null) - addNode(symbol) - else { - function = externalModulesDFG.functionDFGs[symbol]!! - local = false - } - val body = function.body - body.forEachCallSite { call -> - val devirtualizedCallSite = (call as? DataFlowIR.Node.VirtualCall)?.let { devirtualizedCallSites.get(it) } - if (devirtualizedCallSite == null) { - val callee = call.callee.resolved() - if (moduleDFG.functions.containsKey(callee)) - addNode(callee) - if (local) - callGraph.addEdge(symbol, CallGraphNode.CallSite(call, call is DataFlowIR.Node.VirtualCall, callee)) - if (callee is DataFlowIR.FunctionSymbol.Declared - && call !is DataFlowIR.Node.VirtualCall - && !visitedFunctions.contains(callee)) - functionStack.push(callee) - } else { - devirtualizedCallSite.possibleCallees.forEach { - val callee = it.callee.resolved() - if (moduleDFG.functions.containsKey(callee)) - addNode(callee) - if (local) - callGraph.addEdge(symbol, CallGraphNode.CallSite(call, false, callee)) - if (callee is DataFlowIR.FunctionSymbol.Declared - && !visitedFunctions.contains(callee)) - functionStack.push(callee) + + call.receiverType == DataFlowIR.Type.Virtual -> { + // Skip callsite. This can only be for invocations Any's methods on instances of ObjC classes. + } + + else -> { + // Callsite has not been devirtualized - conservatively assume the worst: + // any inheritor of the receiver type is possible here. + val typeHierarchy = devirtualizationAnalysisResult.typeHierarchy + val allPossibleCallees = mutableListOf() + typeHierarchy.inheritorsOf(call.receiverType as DataFlowIR.Type.Declared).forEachBit { + val receiverType = typeHierarchy.allTypes[it] + if (receiverType.isAbstract) return@forEachBit + // TODO: Unconservative way - when we can use it? + //.filter { devirtualizationAnalysisResult.instantiatingClasses.contains(it) } + val actualCallee = when (call) { + is DataFlowIR.Node.VtableCall -> + receiverType.vtable[call.calleeVtableIndex] + + is DataFlowIR.Node.ItableCall -> + receiverType.itable[call.calleeHash]!! + + else -> error("Unreachable") + } + allPossibleCallees.add(actualCallee) + } + if (allPossibleCallees.size <= nonDevirtualizedCallSitesUnfoldFactor) + allPossibleCallees.forEach { staticCall(symbol, call, it) } + else { + val callSite = CallGraphNode.CallSite(call, true, call.callee) + callGraph.addEdge(symbol, callSite) } } } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DFGBuilder.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DFGBuilder.kt index 688d8799c49..bdf267c3c75 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DFGBuilder.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DFGBuilder.kt @@ -63,7 +63,7 @@ private class VariableValues { } fun add(variable: IrVariable, element: IrExpression) = - elementData[variable]!!.values.add(element) + elementData[variable]?.values?.add(element) private fun add(variable: IrVariable, elements: Set) = elementData[variable]?.values?.addAll(elements) @@ -736,7 +736,7 @@ internal class ModuleDFGBuilder(val context: Context, val irModule: IrModuleFrag createUninitializedInstanceSymbol -> DataFlowIR.Node.AllocInstance(symbolTable.mapClassReferenceType( value.getTypeArgument(0)!!.getClass()!! - )) + ), value) reinterpret -> getNode(value.extensionReceiver!!).value diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DataFlowIR.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DataFlowIR.kt index 2f80da3c49e..ec29cee2f3f 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DataFlowIR.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DataFlowIR.kt @@ -251,7 +251,7 @@ internal object DataFlowIR { class Singleton(val type: Type, val constructor: FunctionSymbol?) : Node() - class AllocInstance(val type: Type) : Node() + class AllocInstance(val type: Type, val irCallSite: IrCall?) : Node() class FunctionReference(val symbol: FunctionSymbol, val type: Type, val returnType: Type) : Node() @@ -647,10 +647,15 @@ internal object DataFlowIR { && !irClass.isNonGeneratedAnnotation() && (it.isOverridableOrOverrides || bridgeTarget != null || function.isSpecial || !irClass.isFinal()) val symbolTableIndex = if (placeToFunctionsTable) module.numberOfFunctions++ else -1 - if (it.isExported()) + val frozen = it is IrConstructor && irClass!!.annotations.findAnnotation(KonanFqNames.frozen) != null + val functionSymbol = if (it.isExported()) FunctionSymbol.Public(name.localHash.value, module, symbolTableIndex, attributes, it, bridgeTargetSymbol, takeName { name }) else FunctionSymbol.Private(privateFunIndex++, module, symbolTableIndex, attributes, it, bridgeTargetSymbol, takeName { name }) + if (frozen) { + functionSymbol.escapes = 0b1 // Assume instances of frozen classes escape. + } + functionSymbol } } functionMap[it] = symbol diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/EscapeAnalysis.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/EscapeAnalysis.kt index 694142cc5f9..8f815027a4a 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/EscapeAnalysis.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/EscapeAnalysis.kt @@ -5,36 +5,103 @@ package org.jetbrains.kotlin.backend.konan.optimizations +import org.jetbrains.kotlin.backend.common.atMostOne +import org.jetbrains.kotlin.backend.common.pop +import org.jetbrains.kotlin.backend.common.push import org.jetbrains.kotlin.backend.konan.DirectedGraphCondensationBuilder import org.jetbrains.kotlin.backend.konan.DirectedGraphMultiNode import org.jetbrains.kotlin.backend.konan.llvm.Lifetime import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.backend.konan.Context +import org.jetbrains.kotlin.ir.declarations.IrClass +import kotlin.math.min + +private val DataFlowIR.Node.isAlloc + get() = this is DataFlowIR.Node.NewObject || this is DataFlowIR.Node.AllocInstance + +private val DataFlowIR.Node.ir + get() = when (this) { + is DataFlowIR.Node.Call -> irCallSite + is DataFlowIR.Node.AllocInstance -> irCallSite + is DataFlowIR.Node.ArrayRead -> irCallSite + is DataFlowIR.Node.FieldRead -> ir + else -> null + } internal object EscapeAnalysis { /* * The goal of escape analysis is to estimate lifetimes of all expressions in a program. * Possible lifetimes are: - * 1. Local - an object is used only within a function. - * 2. Return value - an object is either returned or set to a field of an object being returned. - * 3. Parameter - an object is set to a field of exactly one parameter of a function. - * 4. Global - otherwise. + * 0. Stack - an object is used only within its visibility scope within a function. + * 1. Local - an object is used only within a function. + * 2. Return value - an object is either returned or set to a field of an object being returned. + * 3. Parameter - an object is set to a field of some parameters of a function. + * 4. Global - otherwise. + * For now only Stack and Global lifetimes are supported by the codegen, so others will be pulled up to Global. * - * The analysis is performed in two main steps - intraprocedural and interprocedural. + * The analysis is performed in two main stages - intraprocedural and interprocedural. * During intraprocedural analysis we remove all control flow related expressions and compute all possible * values of all variables within a function. - * The goal of interprocedural analysis is to build points-to graph (an edge is created from A to B iff A holds - * a reference to B). This is done by building call graph (using devirtualization for more precise result). + * The goal of interprocedural analysis is to build points-to graph (object A references object B if and only if + * there is a path from the node A to the node B). This is done by building call graph (using devirtualization + * for more precise result). But in practice holding this condition both ways can be difficult and bad in terms of + * performance, so the algorithm tries to ensure only one part: if object A references object B then there must be + * a path from the node A to the node B, with that none of the constraints from the original program will be lost. + * It is ok to add some additional constraints, as long as there are not too many of those. * - * How do exactly we build the points-to graph out of the call graph? + * How do we exactly build the points-to graph out of the call graph? * 1. Build condensation of the call graph. - * 2. Handle vertices of the resulting DAG in topological order (ensuring that all functions being called + * 2. Handle vertices of the resulting DAG in the reversed topological order (ensuring that all functions being called * are already handled). * 3. For a strongly connected component build the points-to graph iteratively starting with empty graph - * (since edges can only be added the process will end eventually). + * (if the process is seemed to not be converging for some function, assume the pessimistic result). * - * When we have the points-to graph it is easy to compute lifetimes - using DFS compute the graph's closure. + * Escape analysis result of a function is not only lifetimes for all allocations of that function + * but also a snippet of its points-to graph (it's a reduced version, basically, subgraph reachable from + * the function's parameters). + * Assuming the function has parameters P0, P1, .., Pn, where the last parameter is the return parameter, + * it turns out that the snippet can be described as an array of relations of form + * v.f0.f1...fk -> w.g0.g1...gl where v, w - either one of the function's parameters or special + * additional nodes called drains which will be introduced later; and f0, f1, .., fk, g0, g1, .., gl - fields. + * + * Building points-to graph: + * 1. Seed it from the function's DataFlowIR. + * There are two kinds of edges: + * 1) field. The subgraph for [a.f]: + * [a] + * | f + * V + * [a.f] + * Notice the label [f] on the edge. + * 2) assignment. The subgraph for [a = b]: + * [a] + * | + * V + * [b] + * No labels on the edge. + * When calling a function, take its points-to graph snippet and embed it at the call site, + * replacing parameters with actual node arguments. + * 2. Build the closure. + * Consider an assignment [a = b], and a usage of [a.f] somewhere. Since there is no order on nodes + * of DataFlowIR (sea of nodes), the conservative assumption has to be made - [b.f] is also being used + * at the same place as [a.f] is. Same applies for usages of [b.f]. + * This reasoning leads to the following algorithm: + * Consider for the time being all assignment edges undirected and build connected components. + * Now, every field usage of any node within a component implies the same usage of any other node + * from that component, so the following transformation will be performed: + * 1) Consider components one by one. Select a node which has no outgoing assignment edges, + * if there is no such a node, create additional node and add assignment edges from every node + * to it. Call this node a drain. Then move all beginnings of field edges from all nodes to + * the drain leaving the ends as is (this reflects the above consideration - any field usage + * can be applied to any node within a component). + * 2) After drains creation and field edges moving there might emerge multi-edges (more than one + * field edge with the same label going to different components). The components these + * multi-edges are pointing at must be coalesced together (this is done either by creating + * a new drain or connecting one component's drain to the other). This operation must be + * performed until there are no more multi-edges. + * After the above transformation has been made, finally, simple lifetime propagation can be performed, + * seeing all edges directed. */ private val DEBUG = 0 @@ -43,19 +110,34 @@ internal object EscapeAnalysis { if (DEBUG > severity) block() } - // Roles in which particular object reference is being used. Lifetime is computed from all roles reference. + // A special marker field for external types implemented in the runtime (mainly, arrays). + // The types being passed to the constructor are not used in the analysis - just put there anything. + private val intestinesField = DataFlowIR.Field(null, DataFlowIR.Type.Virtual, 1L, "inte\$tines") + + // A special marker field for return values. + // Basically we substitute [return x] with [ret.v@lue = x]. + // This is done in order to not handle return parameter somewhat specially. + private val returnsValueField = DataFlowIR.Field(null, DataFlowIR.Type.Virtual, 2L, "v@lue") + + // Roles in which particular object reference is being used. private enum class Role { - // If reference is being returned. RETURN_VALUE, - // If reference is being thrown. THROW_VALUE, - // If reference's field is being written to. - FIELD_WRITTEN, - // If reference is being written to the global. - WRITTEN_TO_GLOBAL + WRITE_FIELD, + READ_FIELD, + WRITTEN_TO_GLOBAL, + ASSIGNED, } - private class RoleInfoEntry(val data: DataFlowIR.Node? = null) + // The less the higher an object escapes. + object Depths { + val INFINITY = 1_000_000 + val ROOT_SCOPE = 0 + val RETURN_VALUE = -1 + val PARAMETER = -2 + } + + private class RoleInfoEntry(val node: DataFlowIR.Node? = null, val field: DataFlowIR.Field?) private open class RoleInfo { val entries = mutableListOf() @@ -63,7 +145,7 @@ internal object EscapeAnalysis { open fun add(entry: RoleInfoEntry) = entries.add(entry) } - private class Roles { + private class NodeInfo(val depth: Int = Depths.INFINITY) { val data = HashMap() fun add(role: Role, info: RoleInfoEntry?) { @@ -80,13 +162,13 @@ internal object EscapeAnalysis { } private class FunctionAnalysisResult(val function: DataFlowIR.Function, - val nodesRoles: Map) + val nodesRoles: Map) private class IntraproceduralAnalysis(val context: Context, val moduleDFG: ModuleDFG, val externalModulesDFG: ExternalModulesDFG, val callGraph: CallGraph) { - val functions = moduleDFG.functions// TODO: use for cross-module analysis: + externalModulesDFG.functionDFGs + val functions = moduleDFG.functions private fun DataFlowIR.Type.resolved(): DataFlowIR.Type.Declared { if (this is DataFlowIR.Type.Declared) return this @@ -96,82 +178,61 @@ internal object EscapeAnalysis { fun analyze(): Map { val nothing = moduleDFG.symbolTable.mapClassReferenceType(context.ir.symbols.nothing.owner).resolved() - return callGraph.nodes.associateBy({ it.symbol }) { - val function = functions[it.symbol]!! + return callGraph.nodes.filter { functions[it.symbol] != null }.associateBy({ it.symbol }) { callGraphNode -> + val function = functions[callGraphNode.symbol]!! val body = function.body - val nodesRoles = mutableMapOf() - body.forEachNonScopeNode { nodesRoles[it] = Roles() } + val nodesRoles = mutableMapOf() + + fun computeDepths(node: DataFlowIR.Node, depth: Int) { + if (node is DataFlowIR.Node.Scope) + node.nodes.forEach { computeDepths(it, depth + 1) } + else + nodesRoles[node] = NodeInfo(depth) + } + computeDepths(body.rootScope, Depths.ROOT_SCOPE - 1) fun assignRole(node: DataFlowIR.Node, role: Role, infoEntry: RoleInfoEntry?) { nodesRoles[node]!!.add(role, infoEntry) } - body.returns.values.forEach { assignRole(it.node, Role.RETURN_VALUE, null /* TODO */) } - body.throws.values.forEach { assignRole(it.node, Role.THROW_VALUE, null /* TODO */) } + body.returns.values.forEach { assignRole(it.node, Role.RETURN_VALUE, null) } + body.throws.values.forEach { assignRole(it.node, Role.THROW_VALUE, null) } + body.forEachNonScopeNode { node -> when (node) { is DataFlowIR.Node.FieldWrite -> { val receiver = node.receiver - if (receiver == null) { - // Global field. - assignRole(node.value.node, Role.WRITTEN_TO_GLOBAL, null /* TODO */) - } else { - assignRole(receiver.node, Role.FIELD_WRITTEN, RoleInfoEntry(node.value.node)) - - // TODO: make more precise analysis and differentiate fields from receivers. - // See test escape2.kt, why we need these edges. - assignRole(node.value.node, Role.FIELD_WRITTEN, RoleInfoEntry(receiver.node)) - } + if (receiver == null) + assignRole(node.value.node, Role.WRITTEN_TO_GLOBAL, null) + else + assignRole(receiver.node, Role.WRITE_FIELD, RoleInfoEntry(node.value.node, node.field)) } is DataFlowIR.Node.Singleton -> { val type = node.type.resolved() if (type != nothing) - assignRole(node, Role.WRITTEN_TO_GLOBAL, null /* TODO */) + assignRole(node, Role.WRITTEN_TO_GLOBAL, null) } is DataFlowIR.Node.FieldRead -> { val receiver = node.receiver - if (receiver == null) { - // Global field. - assignRole(node, Role.WRITTEN_TO_GLOBAL, null /* TODO */) - } else { - // Receiver holds reference to all its fields. - assignRole(receiver.node, Role.FIELD_WRITTEN, RoleInfoEntry(node)) - - /* - * The opposite (a field points to its receiver) is also kind of true. - * Here is an example why we need these edges: - * - * class B - * class A { val b = B() } - * fun foo(): B { - * val a = A() <- here [a] is created and so does [a.b], therefore they have the same lifetime. - * return a.b <- a.b escapes to return value. If there were no edge from [a.b] to [a], - * then [a] would've been considered local and since [a.b] has the same lifetime as [a], - * [a.b] would be local as well. - * } - * - */ - assignRole(node, Role.FIELD_WRITTEN, RoleInfoEntry(receiver.node)) - } + if (receiver == null) + assignRole(node, Role.WRITTEN_TO_GLOBAL, null) + else + assignRole(receiver.node, Role.READ_FIELD, RoleInfoEntry(node, node.field)) } is DataFlowIR.Node.ArrayWrite -> { - assignRole(node.array.node, Role.FIELD_WRITTEN, RoleInfoEntry(node.value.node)) - assignRole(node.value.node, Role.FIELD_WRITTEN, RoleInfoEntry(node.array.node)) + assignRole(node.array.node, Role.WRITE_FIELD, RoleInfoEntry(node.value.node, intestinesField)) } is DataFlowIR.Node.ArrayRead -> { - assignRole(node.array.node, Role.FIELD_WRITTEN, RoleInfoEntry(node)) - assignRole(node, Role.FIELD_WRITTEN, RoleInfoEntry(node.array.node)) + assignRole(node.array.node, Role.READ_FIELD, RoleInfoEntry(node, intestinesField)) } is DataFlowIR.Node.Variable -> { - for (value in node.values) { - assignRole(node, Role.FIELD_WRITTEN, RoleInfoEntry(value.node)) - assignRole(value.node, Role.FIELD_WRITTEN, RoleInfoEntry(node)) - } + for (value in node.values) + assignRole(node, Role.ASSIGNED, RoleInfoEntry(value.node, null)) } } } @@ -180,72 +241,232 @@ internal object EscapeAnalysis { } } - private class ParameterEscapeAnalysisResult(val escapes: Boolean, val pointsTo: IntArray) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ParameterEscapeAnalysisResult) return false - - if (escapes != other.escapes) return false - if (pointsTo.size != other.pointsTo.size) return false - return pointsTo.indices.all { pointsTo[it] == other.pointsTo[it] } - } - - override fun hashCode(): Int { - var result = escapes.hashCode() - pointsTo.forEach { result = 31 * result + it.hashCode() } - return result - } - - override fun toString() = "${if (escapes) "ESCAPES" else "LOCAL"}, POINTS TO: ${pointsTo.contentToString()}" + private inline fun > Array.sortedAndDistinct(): Array { + this.sort() + if (this.isEmpty()) return this + val unique = mutableListOf(this[0]) + for (i in 1 until this.size) + if (this[i] != this[i - 1]) + unique.add(this[i]) + return unique.toTypedArray() } - private class FunctionEscapeAnalysisResult(val parameters: Array) { + private class CompressedPointsToGraph(edges: Array) { + val edges = edges.sortedAndDistinct() + + sealed class NodeKind { + abstract val absoluteIndex: Int + + object Return : NodeKind() { + override val absoluteIndex = 0 + + override fun equals(other: Any?) = other === this + + override fun toString() = "RET" + } + + class Param(val index: Int) : NodeKind() { + override val absoluteIndex: Int + get() = -1_000_000 + index + + override fun equals(other: Any?) = index == (other as? Param)?.index + + override fun toString() = "P$index" + } + + class Drain(val index: Int) : NodeKind() { + override val absoluteIndex: Int + get() = index + 1 + + override fun equals(other: Any?) = index == (other as? Drain)?.index + + override fun toString() = "D$index" + } + + companion object { + fun parameter(index: Int, total: Int) = + if (index == total - 1) + Return + else + Param(index) + } + } + + class Node(val kind: NodeKind, val path: Array) : Comparable { + override fun compareTo(other: Node): Int { + if (kind.absoluteIndex != other.kind.absoluteIndex) + return kind.absoluteIndex.compareTo(other.kind.absoluteIndex) + for (i in path.indices) { + if (i >= other.path.size) + return 1 + if (path[i].hash != other.path[i].hash) + return path[i].hash.compareTo(other.path[i].hash) + } + if (path.size < other.path.size) return -1 + return 0 + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Node) return false + if (kind != other.kind || path.size != other.path.size) + return false + for (i in path.indices) + if (path[i] != other.path[i]) + return false + return true + } + + override fun toString() = debugOutput(null) + + fun debugOutput(root: String?): String { + val result = StringBuilder() + result.append(root ?: kind.toString()) + path.forEach { + result.append('.') + result.append(it.name ?: "") + } + return result.toString() + } + + fun goto(field: DataFlowIR.Field?) = when (field) { + null -> this + else -> Node(kind, Array(path.size + 1) { if (it < path.size) path[it] else field }) + } + + companion object { + fun parameter(index: Int, total: Int) = Node(NodeKind.parameter(index, total), path = emptyArray()) + fun drain(index: Int) = Node(NodeKind.Drain(index), path = emptyArray()) + } + } + + class Edge(val from: Node, val to: Node) : Comparable { + override fun compareTo(other: Edge): Int { + val fromCompareResult = from.compareTo(other.from) + if (fromCompareResult != 0) + return fromCompareResult + return to.compareTo(other.to) + } + + override fun equals(other: Any?): Boolean { + if (other === this) return true + if (other !is Edge) return false + return from == other.from && to == other.to + } + + override fun toString() = "$from -> $to" + + companion object { + fun pointsTo(param1: Int, param2: Int, totalParams: Int, kind: Int): Edge { + /* + * Values extracted from @PointsTo annotation. + * kind edge + * 1 p1 -> p2 + * 2 p1 -> p2.intestines + * 3 p1.intestines -> p2 + * 4 p1.intestines -> p2.intestines + */ + if (kind <= 0 || kind > 4) + error("Invalid pointsTo kind: $kind") + val from = if (kind < 3) + Node.parameter(param1, totalParams) + else + Node(NodeKind.parameter(param1, totalParams), Array(1) { intestinesField }) + val to = if (kind % 2 == 1) + Node.parameter(param2, totalParams) + else + Node(NodeKind.parameter(param2, totalParams), Array(1) { intestinesField }) + return Edge(from, to) + } + } + } + } + + private class FunctionEscapeAnalysisResult( + val numberOfDrains: Int, + val pointsTo: CompressedPointsToGraph, + escapes: Array + ) { + val escapes = escapes.sortedAndDistinct() override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is FunctionEscapeAnalysisResult) return false - if (parameters.size != other.parameters.size) return false - return parameters.indices.all { parameters[it] == other.parameters[it] } - } + if (escapes.size != other.escapes.size) return false + for (i in escapes.indices) + if (escapes[i] != other.escapes[i]) return false - override fun hashCode(): Int { - var result = 0 - parameters.forEach { result = 31 * result + it.hashCode() } - return result + if (pointsTo.edges.size != other.pointsTo.edges.size) + return false + for (i in pointsTo.edges.indices) + if (pointsTo.edges[i] != other.pointsTo.edges[i]) + return false + return true } override fun toString(): String { - return parameters.withIndex().joinToString("\n") { - if (it.index < parameters.size - 1) - "PARAM#${it.index}: ${it.value}" - else "RETURN: ${it.value}" + val result = StringBuilder() + result.appendLine("PointsTo:") + pointsTo.edges.forEach { result.appendLine(" $it") } + result.append("Escapes:") + escapes.forEach { + result.append(' ') + result.append(it) } + return result.toString() } - val isTrivial get() = parameters.all { !it.escapes && it.pointsTo.isEmpty() } - companion object { - fun fromBits(escapesMask: Int, pointsToMasks: List) = FunctionEscapeAnalysisResult( - pointsToMasks.indices.map { parameterIndex -> - val escapes = escapesMask and (1 shl parameterIndex) != 0 - val curPointsToMask = pointsToMasks[parameterIndex] - val pointsTo = (0..31).filter { curPointsToMask and (1 shl it) != 0 }.toIntArray() - ParameterEscapeAnalysisResult(escapes, pointsTo) - }.toTypedArray() - ) + fun fromBits(escapesMask: Int, pointsToMasks: List): FunctionEscapeAnalysisResult { + val paramCount = pointsToMasks.size + val edges = mutableListOf() + val escapes = mutableListOf() + for (param1 in pointsToMasks.indices) { + if (escapesMask and (1 shl param1) != 0) + escapes.add(CompressedPointsToGraph.Node.parameter(param1, paramCount)) + val curPointsToMask = pointsToMasks[param1] + for (param2 in pointsToMasks.indices) { + // Read a nibble at position [param2]. + val pointsTo = (curPointsToMask shr (4 * param2)) and 15 + if (pointsTo != 0) + edges.add(CompressedPointsToGraph.Edge.pointsTo(param1, param2, paramCount, pointsTo)) + } + } + return FunctionEscapeAnalysisResult( + 0, CompressedPointsToGraph(edges.toTypedArray()), escapes.toTypedArray()) + } + + fun optimistic() = + FunctionEscapeAnalysisResult(0, CompressedPointsToGraph(emptyArray()), emptyArray()) + + fun pessimistic(numberOfParameters: Int) = + FunctionEscapeAnalysisResult(0, CompressedPointsToGraph(emptyArray()), + Array(numberOfParameters + 1) { CompressedPointsToGraph.Node.parameter(it, numberOfParameters + 1) }) } } - private class InterproceduralAnalysis(val callGraph: CallGraph, - val intraproceduralAnalysisResult: Map, - val externalModulesDFG: ExternalModulesDFG, - val lifetimes: MutableMap) { + private class InterproceduralAnalysis( + context: Context, + val callGraph: CallGraph, + val intraproceduralAnalysisResults: Map, + val externalModulesDFG: ExternalModulesDFG, + val lifetimes: MutableMap, + val propagateExiledToHeapObjects: Boolean + ) { - val escapeAnalysisResults = mutableMapOf() + private val symbols = context.ir.symbols + + private fun DataFlowIR.Type.resolved(): DataFlowIR.Type.Declared { + if (this is DataFlowIR.Type.Declared) return this + val hash = (this as DataFlowIR.Type.External).hash + return externalModulesDFG.publicTypes[hash] ?: error("Unable to resolve exported type $hash") + } + + val escapeAnalysisResults = mutableMapOf() fun analyze() { + DEBUG_OUTPUT(0) { println("CALL GRAPH") callGraph.directEdges.forEach { t, u -> @@ -268,6 +489,13 @@ internal object EscapeAnalysis { DEBUG_OUTPUT(0) { println("CONDENSATION") + condensation.topologicalOrder.forEach { multiNode -> + println(" MULTI-NODE") + multiNode.nodes.forEach { + println(" $it") + } + } + println("CONDENSATION(DETAILED)") condensation.topologicalOrder.forEach { multiNode -> println(" MULTI-NODE") multiNode.nodes.forEach { @@ -283,24 +511,32 @@ internal object EscapeAnalysis { } for (functionSymbol in callGraph.directEdges.keys) { - val numberOfParameters = functionSymbol.parameters.size - escapeAnalysisResults[functionSymbol] = FunctionEscapeAnalysisResult( - // Assume no edges at the beginning. - // Then iteratively add needed. - Array(numberOfParameters + 1) { ParameterEscapeAnalysisResult(false, IntArray(0)) } - ) + if (!intraproceduralAnalysisResults.containsKey(functionSymbol)) continue + // Assume trivial result at the beginning - then iteratively specify it. + escapeAnalysisResults[functionSymbol] = FunctionEscapeAnalysisResult.optimistic() } for (multiNode in condensation.topologicalOrder.reversed()) analyze(callGraph, multiNode) + + DEBUG_OUTPUT(1) { + println("Managed to alloc on stack: ${stackAllocsCount * 100.0 / (globalAllocsCount + stackAllocsCount)}%") + println("Total graph size: $totalGraphSize") + } } - private fun analyze(callGraph: CallGraph, multiNode: DirectedGraphMultiNode) { + var globalAllocsCount = 0 + var stackAllocsCount = 0 + var totalGraphSize = 0 + + private fun analyze(callGraph: CallGraph, multiNode: DirectedGraphMultiNode) { + val nodes = multiNode.nodes.filter { intraproceduralAnalysisResults.containsKey(it) }.toMutableSet() + DEBUG_OUTPUT(0) { - println("Analyzing multiNode:\n ${multiNode.nodes.joinToString("\n ") { it.toString() }}") - multiNode.nodes.forEach { from -> + println("Analyzing multiNode:\n ${nodes.joinToString("\n ") { it.toString() }}") + nodes.forEach { from -> println("DataFlowIR") - intraproceduralAnalysisResult[from]!!.function.debugOutput() + intraproceduralAnalysisResults[from]!!.function.debugOutput() callGraph.directEdges[from]!!.callSites.forEach { to -> println("CALL") println(" from $from") @@ -309,64 +545,97 @@ internal object EscapeAnalysis { } } - val pointsToGraphs = multiNode.nodes.associateBy({ it }, { PointsToGraph(it) }) - val toAnalyze = mutableSetOf() - toAnalyze.addAll(multiNode.nodes) + val pointsToGraphs = nodes.associateBy({ it }, { PointsToGraph(it) }) + val toAnalyze = mutableSetOf() + toAnalyze.addAll(nodes) + val numberOfRuns = nodes.associateWith { 0 }.toMutableMap() while (toAnalyze.isNotEmpty()) { val function = toAnalyze.first() toAnalyze.remove(function) + numberOfRuns[function] = numberOfRuns[function]!! + 1 DEBUG_OUTPUT(0) { println("Processing function $function") } - val startResult = escapeAnalysisResults[callGraph.directEdges[function]!!.symbol]!! + val startResult = escapeAnalysisResults[function]!! DEBUG_OUTPUT(0) { println("Start escape analysis result:\n$startResult") } analyze(callGraph, pointsToGraphs[function]!!, function) - val endResult = escapeAnalysisResults[callGraph.directEdges[function]!!.symbol]!! + val endResult = escapeAnalysisResults[function]!! if (startResult == endResult) { + DEBUG_OUTPUT(0) { println("Escape analysis is not changed") } + } else { + DEBUG_OUTPUT(0) { println("Escape analysis was refined:\n$endResult") } + if (numberOfRuns[function]!! > 1) { + + DEBUG_OUTPUT(0) { + println("WARNING: Escape analysis for $function seems not to be converging." + + " Assuming conservative results.") + } + + escapeAnalysisResults[function] = FunctionEscapeAnalysisResult.pessimistic(function.parameters.size) + nodes.remove(function) + } + callGraph.reversedEdges[function]?.forEach { - if (multiNode.nodes.contains(it)) + if (nodes.contains(it)) toAnalyze.add(it) } } } - multiNode.nodes.forEach { - val escapeAnalysisResult = escapeAnalysisResults[it]!! - var escapes = 0 - val pointsTo = escapeAnalysisResult.parameters.withIndex().map { (index, parameterEAResult) -> - if (parameterEAResult.escapes) - escapes = escapes or (1 shl index) - var pointsToMask = 0 - parameterEAResult.pointsTo.forEach { - pointsToMask = pointsToMask or (1 shl it) - } - pointsToMask - }.toIntArray() - it.escapes = escapes - it.pointsTo = pointsTo - } + for (graph in pointsToGraphs.values) { for (node in graph.nodes.keys) { - val ir = when (node) { - is DataFlowIR.Node.Call -> node.irCallSite - is DataFlowIR.Node.ArrayRead -> node.irCallSite - is DataFlowIR.Node.FieldRead -> node.ir - else -> null + node.ir?.let { + val lifetime = graph.lifetimeOf(node) + + if (node.isAlloc) { + if (lifetime == Lifetime.GLOBAL) + ++globalAllocsCount + if (lifetime == Lifetime.STACK) + ++stackAllocsCount + + lifetimes[it] = lifetime + } } - ir?.let { lifetimes.put(it, graph.lifetimeOf(node)) } } } } - private fun analyze(callGraph: CallGraph, pointsToGraph: PointsToGraph, function: DataFlowIR.FunctionSymbol) { + private fun arrayLengthOf(node: DataFlowIR.Node): Int? = + (node as? DataFlowIR.Node.SimpleConst<*>)?.value as? Int + // In case of several possible values, it's unknown what is used. + // TODO: if all values are constants which are less limit? + ?: (node as? DataFlowIR.Node.Variable) + ?.values?.singleOrNull()?.let { arrayLengthOf(it.node) } + + private val pointerSize = context.llvm.runtime.pointerSize + + private fun arrayItemSizeOf(irClass: IrClass): Int? = when (irClass.symbol) { + symbols.array -> pointerSize + symbols.booleanArray -> 1 + symbols.byteArray -> 1 + symbols.charArray -> 2 + symbols.shortArray -> 2 + symbols.intArray -> 4 + symbols.floatArray -> 4 + symbols.longArray -> 8 + symbols.doubleArray -> 8 + else -> null + } + + private fun arraySize(itemSize: Int, length: Int) = + pointerSize /* typeinfo */ + 4 /* size */ + itemSize * length + + private fun analyze(callGraph: CallGraph, pointsToGraph: PointsToGraph, function: DataFlowIR.FunctionSymbol.Declared) { DEBUG_OUTPUT(0) { println("Before calls analysis") pointsToGraph.print() + pointsToGraph.printDigraph(false) } callGraph.directEdges[function]!!.callSites.forEach { @@ -382,6 +651,7 @@ internal object EscapeAnalysis { DEBUG_OUTPUT(0) { println("After calls analysis") pointsToGraph.print() + pointsToGraph.printDigraph(false) } // Build transitive closure. @@ -390,19 +660,12 @@ internal object EscapeAnalysis { DEBUG_OUTPUT(0) { println("After closure building") pointsToGraph.print() + pointsToGraph.printDigraph(true) } - escapeAnalysisResults[callGraph.directEdges[function]!!.symbol] = eaResult - } + escapeAnalysisResults[function] = eaResult - private fun getConservativeFunctionEAResult(symbol: DataFlowIR.FunctionSymbol): FunctionEscapeAnalysisResult { - val numberOfParameters = symbol.parameters.size - return FunctionEscapeAnalysisResult((0..numberOfParameters).map { - ParameterEscapeAnalysisResult( - escapes = true, - pointsTo = IntArray(0) - ) - }.toTypedArray()) + totalGraphSize += eaResult.numberOfDrains + eaResult.escapes.size + eaResult.pointsTo.edges.size } private fun DataFlowIR.FunctionSymbol.resolved(): DataFlowIR.FunctionSymbol { @@ -418,15 +681,27 @@ internal object EscapeAnalysis { DEBUG_OUTPUT(0) { println("A virtual call: $callee") } - getConservativeFunctionEAResult(callee) + FunctionEscapeAnalysisResult.pessimistic(callee.parameters.size) } else { DEBUG_OUTPUT(0) { println("An external call: $callee") } - FunctionEscapeAnalysisResult.fromBits( - callee.escapes ?: 0, - (0..callee.parameters.size).map { callee.pointsTo?.elementAtOrNull(it) ?: 0 } - ) + if (callee.name?.startsWith("kfun:kotlin.") == true + // TODO: Is it possible to do it in a more fine-grained fashion? + && !callee.name.startsWith("kfun:kotlin.native.concurrent")) { + + DEBUG_OUTPUT(0) { println("A function from K/N runtime - can use annotations") } + + FunctionEscapeAnalysisResult.fromBits( + callee.escapes ?: 0, + (0..callee.parameters.size).map { callee.pointsTo?.elementAtOrNull(it) ?: 0 } + ) + } else { + + DEBUG_OUTPUT(0) { println("An unknown function - assume pessimistic result") } + + FunctionEscapeAnalysisResult.pessimistic(callee.parameters.size) + } } DEBUG_OUTPUT(0) { @@ -438,36 +713,99 @@ internal object EscapeAnalysis { return calleeEAResult } - private enum class PointsToGraphNodeKind(val weight: Int) { - LOCAL(0), - RETURN_VALUE(1), - ESCAPES(2) + private enum class PointsToGraphNodeKind { + STACK, + LOCAL, + PARAMETER, + RETURN_VALUE } - private class PointsToGraphNode(roles: Roles) { - val edges = mutableSetOf() + private sealed class PointsToGraphEdge(val node: PointsToGraphNode) { + class Assignment(node: PointsToGraphNode) : PointsToGraphEdge(node) - var kind = when { - roles.escapes() -> PointsToGraphNodeKind.ESCAPES - roles.has(Role.RETURN_VALUE) -> PointsToGraphNodeKind.RETURN_VALUE - else -> PointsToGraphNodeKind.LOCAL + class Field(node: PointsToGraphNode, val field: DataFlowIR.Field) : PointsToGraphEdge(node) + } + + private class PointsToGraphNode(val nodeInfo: NodeInfo, val node: DataFlowIR.Node?) { + val edges = mutableListOf() + val reversedEdges = mutableListOf() + + fun addAssignmentEdge(to: PointsToGraphNode) { + edges += PointsToGraphEdge.Assignment(to) + to.reversedEdges += PointsToGraphEdge.Assignment(this) } - val beingReturned = roles.has(Role.RETURN_VALUE) + private val fields = mutableMapOf() - val parametersPointingOnUs = mutableSetOf() + fun getFieldNode(field: DataFlowIR.Field, graph: PointsToGraph) = + fields.getOrPut(field) { graph.newNode().also { edges += PointsToGraphEdge.Field(it, field) } } - fun addIncomingParameter(parameter: Int) { - if (kind == PointsToGraphNodeKind.ESCAPES) return - parametersPointingOnUs += parameter + var depth = when { + node is DataFlowIR.Node.Parameter -> Depths.PARAMETER + nodeInfo.has(Role.RETURN_VALUE) -> Depths.RETURN_VALUE + else -> nodeInfo.depth } + + val kind get() = when { + depth == Depths.PARAMETER -> PointsToGraphNodeKind.PARAMETER + depth == Depths.RETURN_VALUE -> PointsToGraphNodeKind.RETURN_VALUE + depth != nodeInfo.depth -> PointsToGraphNodeKind.LOCAL + else -> PointsToGraphNodeKind.STACK + } + + var forcedLifetime: Lifetime? = null + + lateinit var drain: PointsToGraphNode + val isDrain get() = this == drain + + val actualDrain: PointsToGraphNode + get() = drain.let { + if (it.isDrain) it + // Flip to the real drain as it is done in the disjoint sets algorithm, + // to reduce the time spent in this function. + else it.actualDrain.also { drain = it } + } + val isActualDrain get() = this == actualDrain + + val beingReturned get() = nodeInfo.has(Role.RETURN_VALUE) + } + + private data class ArrayStaticAllocation(val node: PointsToGraphNode, val irClass: IrClass, val size: Int) + + private enum class EdgeDirection { + FORWARD, + BACKWARD } private inner class PointsToGraph(val functionSymbol: DataFlowIR.FunctionSymbol) { - val functionAnalysisResult = intraproceduralAnalysisResult[functionSymbol]!! + val functionAnalysisResult = intraproceduralAnalysisResults[functionSymbol]!! val nodes = mutableMapOf() + val allNodes = mutableListOf() + + fun newNode(nodeInfo: NodeInfo, node: DataFlowIR.Node?) = + PointsToGraphNode(nodeInfo, node).also { allNodes.add(it) } + fun newNode() = newNode(NodeInfo(), null) + fun newDrain() = newNode().also { it.drain = it } + + val returnsNode = newNode(NodeInfo().also { it.data[Role.RETURN_VALUE] = RoleInfo() }, null) + + /* + * Of all escaping nodes there are some "starting" - call them origins. + * Consider a variable [v], which is assigned with two values - [a] and [b]. + * Now assume [a] escapes (written to a global, for instance). This implies that [v] also escapes, + * but [b] doesn't, albeit [v] (an escaping node) references it. It's because [v] is not an escape origin. + */ + // The origins of escaping. + val escapeOrigins = mutableSetOf() + // Nodes reachable from either of escape origins going along all edges (assignment and/or field). + val reachableFromEscapeOrigins = mutableSetOf() + // Nodes referencing any escape origin only by assignment edges. + val referencingEscapeOrigins = mutableSetOf() + + fun escapes(node: PointsToGraphNode) = node in reachableFromEscapeOrigins || node in referencingEscapeOrigins + val ids = if (DEBUG > 0) (listOf(functionAnalysisResult.function.body.rootScope) + functionAnalysisResult.function.body.allScopes.flatMap { it.nodes } @@ -475,104 +813,148 @@ internal object EscapeAnalysis { .withIndex().associateBy({ it.value }, { it.index }) else null - fun lifetimeOf(node: DataFlowIR.Node) = nodes[node]!!.let { - when (it.kind) { - PointsToGraphNodeKind.ESCAPES -> Lifetime.GLOBAL + fun lifetimeOf(node: DataFlowIR.Node) = nodes[node]!!.let { it.forcedLifetime ?: lifetimeOf(it) } - PointsToGraphNodeKind.LOCAL -> { - if (it.parametersPointingOnUs.isEmpty()) { + fun lifetimeOf(node: PointsToGraphNode) = + if (escapes(node)) + Lifetime.GLOBAL + else when (node.kind) { + PointsToGraphNodeKind.PARAMETER -> Lifetime.ARGUMENT + + PointsToGraphNodeKind.STACK -> { + // A value doesn't escape from its scope - it can be allocated on the stack. + Lifetime.STACK + } + + PointsToGraphNodeKind.LOCAL -> { // A value is neither stored into a global nor into any parameter nor into the return value - // it can be allocated locally. Lifetime.LOCAL - } else { - if (it.parametersPointingOnUs.size == 1) { // TODO: remove. - // A value is stored into a parameter field. - Lifetime.PARAMETER_FIELD(it.parametersPointingOnUs.first()) - } else { - // A value is stored into several parameters fields. - Lifetime.PARAMETERS_FIELD(it.parametersPointingOnUs.toIntArray(), false) + } + + PointsToGraphNodeKind.RETURN_VALUE -> { + when { + // If a value is explicitly returned. + node.node?.let { it in returnValues } == true -> Lifetime.RETURN_VALUE + + // A value is stored into a field of the return value. + else -> Lifetime.INDIRECT_RETURN_VALUE } } } - PointsToGraphNodeKind.RETURN_VALUE -> { - when { - // If a value is explicitly returned. - returnValues.contains(node) -> Lifetime.RETURN_VALUE - - it.parametersPointingOnUs.isNotEmpty() -> Lifetime.PARAMETERS_FIELD(it.parametersPointingOnUs.toIntArray(), true) - - // A value is stored into a field of the return value. - else -> Lifetime.INDIRECT_RETURN_VALUE - } - } - } - } + private val returnValues: Set init { + DEBUG_OUTPUT(0) { println("Building points-to graph for function $functionSymbol") println("Results of preliminary function analysis") } - functionAnalysisResult.nodesRoles.forEach { node, roles -> + functionAnalysisResult.nodesRoles.forEach { (node, roles) -> + DEBUG_OUTPUT(0) { println("NODE ${nodeToString(node)}: $roles") } - nodes.put(node, PointsToGraphNode(roles)) + nodes[node] = newNode(roles, node) } - functionAnalysisResult.nodesRoles.forEach { node, roles -> - addEdges(node, roles) + val returnValues = mutableListOf() + functionAnalysisResult.nodesRoles.forEach { (node, roles) -> + val ptgNode = nodes[node]!! + addEdges(ptgNode, roles) + if (ptgNode.beingReturned) { + returnsNode.getFieldNode(returnsValueField, this).addAssignmentEdge(ptgNode) + returnValues += node + } + if (roles.escapes()) + escapeOrigins += ptgNode + } + + this.returnValues = returnValues.toSet() + + val escapes = functionSymbol.escapes + if (escapes != null) { + // Parameters are declared in the root scope + val parameters = functionAnalysisResult.function.body.rootScope.nodes + .filterIsInstance() + for (parameter in parameters) + if (escapes and (1 shl parameter.index) != 0) + escapeOrigins += nodes[parameter]!! + if (escapes and (1 shl parameters.size) != 0) + escapeOrigins += returnsNode } } - private val returnValues = nodes.filter { it.value.beingReturned } - .map { it.key } - .toSet() - - private fun addEdges(from: DataFlowIR.Node, roles: Roles) { - val pointsToEdge = roles.data[Role.FIELD_WRITTEN] - ?: return - pointsToEdge.entries.forEach { - val to = it.data!! - if (nodes.containsKey(to)) { - nodes[from]!!.edges.add(to) - - DEBUG_OUTPUT(0) { - println("EDGE: ") - println(" FROM: ${nodeToString(from)}") - println(" TO: ${nodeToString(to)}") - } - } + private fun addEdges(from: PointsToGraphNode, roles: NodeInfo) { + val assigned = roles.data[Role.ASSIGNED] + assigned?.entries?.forEach { + val to = nodes[it.node!!]!! + from.addAssignmentEdge(to) + } + roles.data[Role.WRITE_FIELD]?.entries?.forEach { roleInfo -> + val value = nodes[roleInfo.node!!]!! + val field = roleInfo.field!! + from.getFieldNode(field, this).addAssignmentEdge(value) + } + roles.data[Role.READ_FIELD]?.entries?.forEach { roleInfo -> + val result = nodes[roleInfo.node!!]!! + val field = roleInfo.field!! + result.addAssignmentEdge(from.getFieldNode(field, this)) } } private fun nodeToStringWhole(node: DataFlowIR.Node) = DataFlowIR.Function.nodeToString(node, ids!!) - private fun nodeToString(node: DataFlowIR.Node) = ids!![node] + private fun nodeToString(node: DataFlowIR.Node) = ids!![node].toString() fun print() { println("POINTS-TO GRAPH") println("NODES") - nodes.forEach { t, _ -> - println(" ${lifetimeOf(t)} ${nodeToString(t)}") - print(nodeToStringWhole(t)) + val tempIds = mutableMapOf() + var tempIndex = 0 + allNodes.forEach { + if (it.node == null) + tempIds[it] = tempIndex++ } - println("EDGES") - nodes.forEach { t, u -> - u.edges.forEach { - println(" FROM ${nodeToString(t)}") - println(" TO ${nodeToString(it)}") - } + allNodes.forEach { + val tempId = tempIds[it] + println(" ${lifetimeOf(it)} ${it.depth} ${if (it in escapeOrigins) "ESCAPES" else ""} ${it.node?.let { nodeToString(it) } ?: "t$tempId"}") + print(it.node?.let { nodeToStringWhole(it) } ?: " t$tempId\n") } } - fun print_digraph() { + fun printDigraph( + markDrains: Boolean, + nodeFilter: (PointsToGraphNode) -> Boolean = { true }, + nodeLabel: ((PointsToGraphNode) -> String)? = null + ) { println("digraph {") val ids = ids!! - nodes.forEach { t, u -> - u.edges.forEach { - println(" ${ids[t]} -> ${ids[it]};") + val tempIds = mutableMapOf() + var tempIndex = 0 + allNodes.forEach { + if (it.node == null) + tempIds[it] = tempIndex++ + } + + fun PointsToGraphNode.format() = + (nodeLabel?.invoke(this) ?: + (if (markDrains && isDrain) "d" else "") + + (node?.let { "n${ids[it]!!}" } ?: "t${tempIds[this]}")) + + "[d=$depth,${if (this in escapeOrigins) "eo" else if (escapes(this)) "e" else ""}]" + + for (from in allNodes) { + if (!nodeFilter(from)) continue + for (it in from.edges) { + val to = it.node + if (!nodeFilter(to)) continue + when (it) { + is PointsToGraphEdge.Assignment -> + println(" \"${from.format()}\" -> \"${to.format()}\";") + is PointsToGraphEdge.Field -> + println(" \"${from.format()}\" -> \"${to.format()}\" [ label=\"${it.field.name}\"];") + } } } println("}") @@ -580,6 +962,7 @@ internal object EscapeAnalysis { fun processCall(callSite: CallGraphNode.CallSite, calleeEscapeAnalysisResult: FunctionEscapeAnalysisResult) { val call = callSite.call + DEBUG_OUTPUT(0) { println("Processing callSite") println(nodeToStringWhole(call)) @@ -598,48 +981,72 @@ internal object EscapeAnalysis { } } - for (index in 0..call.arguments.size) { - val parameterEAResult = calleeEscapeAnalysisResult.parameters[index] - val from = arguments[index] - if (parameterEAResult.escapes) { - nodes[from]!!.kind = PointsToGraphNodeKind.ESCAPES + val calleeDrains = Array(calleeEscapeAnalysisResult.numberOfDrains) { newNode() } - DEBUG_OUTPUT(0) { println("Node ${nodeToString(from)} escapes") } + fun mapNode(compressedNode: CompressedPointsToGraph.Node): Pair { + val (arg, rootNode) = when (val kind = compressedNode.kind) { + CompressedPointsToGraph.NodeKind.Return -> + if (call is DataFlowIR.Node.NewObject) // TODO: This better be an assertion. + DataFlowIR.Node.Null to null + else + arguments.last() to nodes[arguments.last()] + is CompressedPointsToGraph.NodeKind.Param -> arguments[kind.index] to nodes[arguments[kind.index]] + is CompressedPointsToGraph.NodeKind.Drain -> null to calleeDrains[kind.index] } - parameterEAResult.pointsTo.forEach { toIndex -> - val nodeFrom = nodes[from] - if (nodeFrom == null) { - DEBUG_OUTPUT(0) { - println("WARNING: There is no node") - println(" FROM ${nodeToString(from)}") - } - } else { - val to = arguments[toIndex] - val nodeTo = nodes[to] - if (nodeTo == null) { - DEBUG_OUTPUT(0) { - println("WARNING: There is no node") - println(" TO ${nodeToString(to)}") - } - } else { - DEBUG_OUTPUT(0) { - println("Adding edge") - println(" FROM ${nodeToString(from)}") - println(" TO ${nodeToString(to)}") - } - - nodeFrom.edges.add(to) - } + if (rootNode == null) + return arg to rootNode + val path = compressedNode.path + var node: PointsToGraphNode = rootNode + for (field in path) { + node = when (field) { + returnsValueField -> node + else -> node.getFieldNode(field, this) } } + return arg to node + } + + calleeEscapeAnalysisResult.escapes.forEach { escapingNode -> + val (arg, node) = mapNode(escapingNode) + if (node == null) { + + DEBUG_OUTPUT(0) { println("WARNING: There is no node ${nodeToString(arg!!)}") } + + return@forEach + } + escapeOrigins += node + + DEBUG_OUTPUT(0) { + println("Node ${escapingNode.debugOutput(arg?.let { nodeToString(it) })} escapes") + } + } + + calleeEscapeAnalysisResult.pointsTo.edges.forEach { edge -> + val (fromArg, fromNode) = mapNode(edge.from) + if (fromNode == null) { + + DEBUG_OUTPUT(0) { println("WARNING: There is no node ${nodeToString(fromArg!!)}") } + + return@forEach + } + val (toArg, toNode) = mapNode(edge.to) + if (toNode == null) { + + DEBUG_OUTPUT(0) { println("WARNING: There is no node ${nodeToString(toArg!!)}") } + + return@forEach + } + fromNode.addAssignmentEdge(toNode) + + DEBUG_OUTPUT(0) { + println("Adding edge") + println(" FROM ${edge.from.debugOutput(fromArg?.let { nodeToString(it) })}") + println(" TO ${edge.to.debugOutput(toArg?.let { nodeToString(it) })}") + } } } fun buildClosure(): FunctionEscapeAnalysisResult { - // Parameters are declared in the root scope. - val parameters = functionAnalysisResult.function.body.rootScope.nodes - .filterIsInstance() - val reachabilities = mutableListOf() DEBUG_OUTPUT(0) { println("BUILDING CLOSURE") @@ -649,80 +1056,664 @@ internal object EscapeAnalysis { } } - parameters.forEach { - val visited = mutableSetOf() - if (nodes[it] != null) - findReachable(it, visited) - visited -= it + buildDrains() - DEBUG_OUTPUT(0) { - println("Reachable from ${nodeToString(it)}") - visited.forEach { - println(" ${nodeToString(it)}") + DEBUG_OUTPUT(0) { printDigraph(true) } + + computeLifetimes() + + /* + * The next part determines the function's escape analysis result. + * Of course, the simplest way would be to just take the entire graph, but it might be big, + * and during call graph traversal these EA graphs will continue to grow (since they are + * being embedded at each call site). To overcome this, the graph must be reduced. + * Let us call nodes that will be part of the result "interesting", and, obviously, + * "interesting drains" - drains that are going to be in the result. + */ + val (numberOfDrains, nodeIds) = paintInterestingNodes() + + DEBUG_OUTPUT(0) { + printDigraph(true, { nodeIds[it] != null }, { nodeIds[it].toString() }) + } + + // TODO: Remove redundant edges. + val compressedEdges = mutableListOf() + val escapingNodes = mutableListOf() + for (from in allNodes) { + val fromCompressedNode = nodeIds[from] ?: continue + if (from in escapeOrigins) + escapingNodes += fromCompressedNode + for (edge in from.edges) { + val toCompressedNode = nodeIds[edge.node] ?: continue + when (edge) { + is PointsToGraphEdge.Assignment -> + compressedEdges += CompressedPointsToGraph.Edge(fromCompressedNode, toCompressedNode) + + is PointsToGraphEdge.Field -> + if (edge.node == from /* A loop */) { + compressedEdges += CompressedPointsToGraph.Edge( + fromCompressedNode.goto(edge.field), toCompressedNode) + } + } + } + } + + return FunctionEscapeAnalysisResult( + numberOfDrains, + CompressedPointsToGraph(compressedEdges.toTypedArray()), + escapingNodes.toTypedArray() + ) + } + + private fun buildDrains() { + // TODO: This is actually conservative. If a field is being read of some node, + // then here it is assumed that it might also be being read from any node reachable + // by assignment edges considering them undirected. But in reality it is enough to just + // merge two sets: reachable by assignment edges and reachable by reversed assignment edges. + // But, there will be a downside - drains will have to be created for each field access, + // thus increasing the graph size significantly. + val visited = mutableSetOf() + val drains = mutableListOf() + val createdDrains = mutableSetOf() + // Create drains. + for (node in allNodes.toTypedArray() /* Copy as [allNodes] might change inside */) { + if (node in visited) continue + val component = mutableListOf() + buildComponent(node, visited, component) + val drain = trySelectDrain(component)?.also { it.drain = it } + ?: newDrain().also { createdDrains += it } + drains += drain + component.forEach { + if (it == drain) return@forEach + it.drain = drain + val assignmentEdges = mutableListOf() + for (edge in it.edges) { + if (edge is PointsToGraphEdge.Assignment) + assignmentEdges += edge + else + drain.edges += edge + } + it.edges.clear() + it.edges += assignmentEdges + } + } + + fun PointsToGraphNode.flipTo(otherDrain: PointsToGraphNode) { + require(isDrain) + require(otherDrain.isDrain) + drain = otherDrain + otherDrain.edges += edges + edges.clear() + } + + // Merge the components multi-edges are pointing at. + // TODO: This looks very similar to the system of disjoint sets algorithm. + while (true) { + val toMerge = mutableListOf>() + for (drain in drains) { + val fields = drain.edges.groupBy { edge -> + (edge as? PointsToGraphEdge.Field)?.field + ?: error("A drain cannot have outgoing assignment edges") + } + for (nodes in fields.values) { + if (nodes.size == 1) continue + for (i in nodes.indices) { + val firstNode = nodes[i].node + val secondNode = if (i == nodes.size - 1) nodes[0].node else nodes[i + 1].node + if (firstNode.actualDrain != secondNode.actualDrain) + toMerge += Pair(firstNode, secondNode) + } + } + } + if (toMerge.isEmpty()) break + val possibleDrains = mutableListOf() + for ((first, second) in toMerge) { + // Merge components: try to flip one drain to the other if possible, + // otherwise just create a new one. + + val firstDrain = first.actualDrain + val secondDrain = second.actualDrain + when { + firstDrain == secondDrain -> continue + + firstDrain in createdDrains -> { + secondDrain.flipTo(firstDrain) + possibleDrains += firstDrain + } + + secondDrain in createdDrains -> { + firstDrain.flipTo(secondDrain) + possibleDrains += secondDrain + } + + else -> { + // Create a new drain in order to not create false constraints. + val newDrain = newDrain().also { createdDrains += it } + firstDrain.flipTo(newDrain) + secondDrain.flipTo(newDrain) + possibleDrains += newDrain + } + } + } + drains.clear() + possibleDrains.filterTo(drains) { it.isDrain } + } + + // Compute current drains. + drains.clear() + allNodes.filterTo(drains) { it.isActualDrain } + + // A validation. + for (drain in drains) { + val fields = mutableMapOf() + for (edge in drain.edges) { + val field = (edge as? PointsToGraphEdge.Field)?.field + ?: error("A drain cannot have outgoing assignment edges") + val node = edge.node.actualDrain + fields.getOrPut(field) { node }.also { + if (it != node) error("Drains have not been built correctly") + } + } + } + + // Coalesce multi-edges. + for (drain in drains) { + val actualDrain = drain.actualDrain + val fields = actualDrain.edges.groupBy { edge -> + (edge as? PointsToGraphEdge.Field)?.field + ?: error("A drain cannot have outgoing assignment edges") + } + actualDrain.edges.clear() + for (nodes in fields.values) { + if (nodes.size == 1) { + actualDrain.edges += nodes[0] + continue + } + // All nodes in [nodes] must be connected to each other, but a drain, by definition, + // cannot have outgoing assignment edges, thus a new drain must be created here. + nodes.atMostOne { it.node.isActualDrain } + ?.node?.actualDrain?.flipTo(newDrain()) + + for (i in nodes.indices) { + val firstNode = nodes[i].node + val secondNode = if (i == nodes.size - 1) nodes[0].node else nodes[i + 1].node + firstNode.addAssignmentEdge(secondNode) + } + // Can pick any. + actualDrain.edges += nodes[0] + } + } + + // Make sure every node within a component points to the component's drain. + for (node in allNodes) { + val drain = node.actualDrain + node.drain = drain + if (node != drain) + node.addAssignmentEdge(drain) + } + } + + // Drains, other than interesting, can be safely omitted from the result. + private fun findInterestingDrains(parameters: Array): Set { + // Starting with all reachable from the parameters. + val interestingDrains = mutableSetOf() + for (param in parameters) { + val drain = param.drain + if (drain !in interestingDrains) + findReachableDrains(drain, interestingDrains) + } + + // Then iteratively remove all drains forming kind of a "cactus" + // (picking a leaf drain with only one incoming edge at a time). + // They can be removed because they don't add any relations between the parameters. + val reversedEdges = interestingDrains.associateWith { + mutableListOf>() + } + val edgesCount = mutableMapOf() + val leaves = mutableListOf() + for (drain in interestingDrains) { + var count = 0 + for (edge in drain.edges) { + val nextDrain = edge.node.drain + reversedEdges[nextDrain]!! += drain to edge + if (nextDrain in interestingDrains) + ++count + } + edgesCount[drain] = count + if (count == 0) + leaves.push(drain) + } + val parameterDrains = parameters.map { it.drain }.toSet() + while (leaves.isNotEmpty()) { + val drain = leaves.pop() + val incomingEdges = reversedEdges[drain]!! + if (incomingEdges.isEmpty()) { + if (drain !in parameterDrains) + error("A drain with no incoming edges") + if (!parameters.any { it.isDrain && escapes(it) }) + interestingDrains.remove(drain) + continue + } + if (drain in parameterDrains) + continue + if (incomingEdges.size == 1 + && incomingEdges[0].let { (node, edge) -> escapes(node) || !escapes(edge.node) } + ) { + interestingDrains.remove(drain) + val prevDrain = incomingEdges[0].first + val count = edgesCount[prevDrain]!! - 1 + edgesCount[prevDrain] = count + if (count == 0) + leaves.push(prevDrain) + } + } + return interestingDrains + } + + private fun getParameterNodes(): Array { + // Put a dummyNode in order to not bother with nullability. Then rewrite it with actual values. + val dummyNode = returnsNode // Anything will do. + val parameters = Array(functionSymbol.parameters.size + 1) { dummyNode } + + // Parameters are declared in the root scope. + functionAnalysisResult.function.body.rootScope.nodes + .filterIsInstance() + .forEach { + if (parameters[it.index] != dummyNode) + error("Two parameters with the same index ${it.index}: $it, ${parameters[it.index].node}") + parameters[it.index] = nodes[it]!! + } + parameters[functionSymbol.parameters.size] = returnsNode + + return parameters + } + + private fun paintInterestingNodes(): Pair> { + var drainsCount = 0 + val drainFactory = { CompressedPointsToGraph.Node.drain(drainsCount++) } + + val parameters = getParameterNodes() + val interestingDrains = findInterestingDrains(parameters) + val nodeIds = paintNodes(parameters, interestingDrains, drainFactory) + buildComponentsClosures(nodeIds) + handleNotTakenEscapeOrigins(nodeIds, drainFactory) + restoreOptimizedAwayDrainsIfNeeded(interestingDrains, nodeIds, drainFactory) + + return Pair(drainsCount, nodeIds) + } + + private fun handleNotTakenEscapeOrigins( + nodeIds: MutableMap, + drainFactory: () -> CompressedPointsToGraph.Node + ) { + // We've marked reachable nodes from the parameters, also taking some of the escape origins. + // But there might be some escape origins that are not taken, yet referencing some of the + // marked nodes. Do the following: find all escaping nodes only taking marked escape origins + // into account, compare the result with all escaping nodes. Now for each non-marked escape origin + // find nodes escaping because of it, take those who aren't escaping through the marked origins, + // and add an additional node, pointing at those and mark it as an escape origin. + val reachableFromTakenEscapeOrigins = mutableSetOf() + val referencingTakenEscapeOrigins = mutableSetOf() + val reachableFromNotTakenEscapeOrigins = mutableSetOf() + val referencingNotTakenEscapeOrigins = mutableSetOf() + val reachableFringeFromNotTakenEscapeOrigins = mutableSetOf() + val fringeReferencingNotTakenEscapeOrigins = mutableSetOf() + for (escapeOrigin in escapeOrigins) { + if (nodeIds[escapeOrigin] == null) { + if (escapeOrigin !in reachableFromNotTakenEscapeOrigins) + findReachableFringe(escapeOrigin, reachableFromNotTakenEscapeOrigins, + reachableFringeFromNotTakenEscapeOrigins, nodeIds) + if (escapeOrigin !in referencingNotTakenEscapeOrigins) + findReferencingFringe(escapeOrigin, referencingNotTakenEscapeOrigins, + fringeReferencingNotTakenEscapeOrigins, nodeIds) + } else { + if (escapeOrigin !in reachableFromTakenEscapeOrigins) + findReachable(escapeOrigin, reachableFromTakenEscapeOrigins, false, null) + if (escapeOrigin !in referencingTakenEscapeOrigins) + findReferencing(escapeOrigin, referencingTakenEscapeOrigins) + } + } + + fun addAdditionalEscapeOrigins(escapingNodes: List, direction: EdgeDirection) { + escapingNodes + .groupBy { it.drain } + .forEach { (drain, nodes) -> + val tempNode = newNode() + nodeIds[tempNode] = drainFactory() + tempNode.drain = drain + tempNode.addAssignmentEdge(drain) + escapeOrigins += tempNode + for (node in nodes) + when (direction) { + EdgeDirection.FORWARD -> tempNode.addAssignmentEdge(node) + EdgeDirection.BACKWARD -> node.addAssignmentEdge(tempNode) + } + } + } + + addAdditionalEscapeOrigins( + reachableFringeFromNotTakenEscapeOrigins + .filterNot { it in reachableFromTakenEscapeOrigins }, + EdgeDirection.FORWARD + ) + addAdditionalEscapeOrigins( + fringeReferencingNotTakenEscapeOrigins + .filterNot { it in referencingTakenEscapeOrigins }, + EdgeDirection.BACKWARD + ) + } + + private fun restoreOptimizedAwayDrainsIfNeeded( + interestingDrains: Set, + nodeIds: MutableMap, + drainFactory: () -> CompressedPointsToGraph.Node + ) { + // Here we try to find this subgraph within one component: [v -> d; w -> d; v !-> w; w !-> v]. + // In most cases such a node [d] is just the drain of the component, + // but it may have been optimized away. + // This is needed because components are built with edges being considered undirected, so + // this implicit connection between [v] and [w] may be needed. Note, however, that the + // opposite subgraph: [d -> v; d -> w; v !-> w; w !-> v] is not interesting, because [d] + // can't hold both values simultaneously, but two references can hold the same value + // at the same time, that's the difference. + val connectedNodes = mutableSetOf>() + allNodes.filter { nodeIds[it] != null && nodeIds[it.drain] == null /* The drain has been optimized away */ } + .forEach { node -> + val referencingNodes = findReferencing(node).filter { nodeIds[it] != null } + for (i in referencingNodes.indices) + for (j in i + 1 until referencingNodes.size) { + val firstNode = referencingNodes[i] + val secondNode = referencingNodes[j] + connectedNodes.add(Pair(firstNode, secondNode)) + connectedNodes.add(Pair(secondNode, firstNode)) + } + } + allNodes.filter { nodeIds[it] == null && it.drain in interestingDrains && nodeIds[it.drain] == null } + .forEach { node -> + val referencingNodes = findReferencing(node).filter { nodeIds[it] != null } + for (i in referencingNodes.indices) + for (j in i + 1 until referencingNodes.size) { + val firstNode = referencingNodes[i] + val secondNode = referencingNodes[j] + val pair = Pair(firstNode, secondNode) + if (pair in connectedNodes) continue + + // It is not an actual drain, but a temporary node to reflect the found constraint. + val additionalDrain = newDrain() + // For consistency. + additionalDrain.depth = min(firstNode.depth, secondNode.depth) + + firstNode.addAssignmentEdge(additionalDrain) + secondNode.addAssignmentEdge(additionalDrain) + nodeIds[additionalDrain] = drainFactory() + connectedNodes.add(pair) + connectedNodes.add(Pair(secondNode, firstNode)) + } + } + } + + private fun findReferencing(node: PointsToGraphNode, visited: MutableSet) { + visited += node + for (edge in node.reversedEdges) { + val nextNode = edge.node + if (nextNode !in visited) + findReferencing(nextNode, visited) + } + } + + private fun findReferencing(node: PointsToGraphNode): Set { + val visited = mutableSetOf() + findReferencing(node, visited) + return visited + } + + private fun trySelectDrain(component: MutableList) = + component.firstOrNull { node -> + if (node.edges.any { it is PointsToGraphEdge.Assignment }) + false + else + findReferencing(node).size == component.size + } + + private fun buildComponent( + node: PointsToGraphNode, + visited: MutableSet, + component: MutableList + ) { + visited += node + component += node + for (edge in node.edges) { + if (edge is PointsToGraphEdge.Assignment && edge.node !in visited) + buildComponent(edge.node, visited, component) + } + for (edge in node.reversedEdges) { + if (edge.node !in visited) + buildComponent(edge.node, visited, component) + } + } + + private fun findReachable(node: PointsToGraphNode, visited: MutableSet, + assignmentOnly: Boolean, + nodeIds: MutableMap?) { + visited += node + node.edges.forEach { + val next = it.node + if ((it is PointsToGraphEdge.Assignment || !assignmentOnly) + && next !in visited && nodeIds?.containsKey(next) != false) + findReachable(next, visited, assignmentOnly, nodeIds) + } + } + + private fun findFringe(node: PointsToGraphNode, visited: MutableSet, + fringe: MutableSet, direction: EdgeDirection, + nodeIds: MutableMap) { + visited += node + if (nodeIds[node] != null) { + fringe += node + return + } + val edges = when (direction) { + EdgeDirection.FORWARD -> node.edges + EdgeDirection.BACKWARD -> node.reversedEdges + } + for (edge in edges) { + val next = edge.node + if (next !in visited) + findFringe(next, visited, fringe, direction, nodeIds) + } + } + + private fun findReachableFringe( + node: PointsToGraphNode, visited: MutableSet, + fringe: MutableSet, + nodeIds: MutableMap + ) = findFringe(node, visited, fringe, EdgeDirection.FORWARD, nodeIds) + + private fun findReferencingFringe( + node: PointsToGraphNode, visited: MutableSet, + fringe: MutableSet, + nodeIds: MutableMap + ) = findFringe(node, visited, fringe, EdgeDirection.BACKWARD, nodeIds) + + private fun buildComponentsClosures(nodeIds: MutableMap) { + for (node in allNodes) { + if (node !in nodeIds) continue + val visited = mutableSetOf() + findReachable(node, visited, true, null) + val visitedInInterestingSubgraph = mutableSetOf() + findReachable(node, visitedInInterestingSubgraph, true, nodeIds) + visited.removeAll(visitedInInterestingSubgraph) + for (reachable in visited) + if (reachable in nodeIds) + node.addAssignmentEdge(reachable) + } + } + + private fun propagateLifetimes() { + val visited = mutableSetOf() + + fun propagate(node: PointsToGraphNode) { + visited += node + val depth = node.depth + for (edge in node.edges) { + val nextNode = edge.node + if (nextNode !in visited && nextNode.depth >= depth) { + nextNode.depth = depth + propagate(nextNode) + } + } + } + + for (node in allNodes.sortedBy { it.depth }) { + if (node !in visited) + propagate(node) + } + } + + private fun propagateEscapeOrigin(node: PointsToGraphNode) { + if (node !in reachableFromEscapeOrigins) + findReachable(node, reachableFromEscapeOrigins, false, null) + if (node !in referencingEscapeOrigins) + findReferencing(node, referencingEscapeOrigins) + } + + private fun computeLifetimes() { + propagateLifetimes() + + escapeOrigins.forEach { propagateEscapeOrigin(it) } + + val stackArrayCandidates = mutableListOf() + for ((node, ptgNode) in nodes) { + if (node.ir == null) continue + + val computedLifetime = lifetimeOf(node) + var lifetime = computedLifetime + + if (lifetime != Lifetime.STACK) { + // TODO: Support other lifetimes - requires arenas. + lifetime = Lifetime.GLOBAL + } + + if (lifetime == Lifetime.STACK && node is DataFlowIR.Node.NewObject) { + val constructedType = node.constructedType.resolved() + constructedType.irClass?.let { irClass -> + val itemSize = arrayItemSizeOf(irClass) + if (itemSize != null) { + val sizeArgument = node.arguments.first().node + val arrayLength = arrayLengthOf(sizeArgument) + if (arrayLength != null) { + stackArrayCandidates += + ArrayStaticAllocation(ptgNode, irClass, arraySize(itemSize, arrayLength)) + } else { + // Can be placed into the local arena. + // TODO. Support Lifetime.LOCAL + lifetime = Lifetime.GLOBAL + } + } } } - val reachable = mutableListOf() - parameters.forEach { - if (visited.contains(it)) - reachable += it.index - } - if (returnValues.any { visited.contains(it) }) - reachable += parameters.size - reachabilities.add(reachable.toIntArray()) - visited.forEach { node -> - if (node !is DataFlowIR.Node.Parameter) - nodes[node]!!.addIncomingParameter(it.index) - } - } - val visitedFromReturnValues = mutableSetOf() - returnValues.forEach { - if (!visitedFromReturnValues.contains(it)) { - findReachable(it, visitedFromReturnValues) - } - } - reachabilities.add( - parameters.filter { visitedFromReturnValues.contains(it) } - .map { it.index }.toIntArray() - ) + if (lifetime != computedLifetime) { + if (propagateExiledToHeapObjects && node.isAlloc) { - propagate(PointsToGraphNodeKind.ESCAPES) - propagate(PointsToGraphNodeKind.RETURN_VALUE) - - return FunctionEscapeAnalysisResult(reachabilities.withIndex().map { (index, reachability) -> - val escapes = - if (index == parameters.size) // Return value. - returnValues.any { nodes[it]!!.kind == PointsToGraphNodeKind.ESCAPES } - else { - /*runtimeAware.isInteresting(parameters[index].value.type) TODO: is it really needed? - && */nodes[parameters[index]]!!.kind == PointsToGraphNodeKind.ESCAPES + DEBUG_OUTPUT(0) { + println("Forcing node ${nodeToString(node)} to escape") } - ParameterEscapeAnalysisResult(escapes, reachability) - }.toTypedArray()) - } - private fun findReachable(node: DataFlowIR.Node, visited: MutableSet) { - visited += node - nodes[node]!!.edges.forEach { - if (!visited.contains(it)) { - findReachable(it, visited) + escapeOrigins += ptgNode + propagateEscapeOrigin(ptgNode) + } else { + ptgNode.forcedLifetime = lifetime + } + } + } + + stackArrayCandidates.sortBy { it.size } + // TODO: To a setting? + var allowedToAlloc = 65536 + for ((ptgNode, irClass, size) in stackArrayCandidates) { + if (lifetimeOf(ptgNode) != Lifetime.STACK) continue + if (size <= allowedToAlloc) + allowedToAlloc -= size + else { + allowedToAlloc = 0 + // Do not exile primitive arrays - they ain't reference no object. + if (irClass.symbol == symbols.array && propagateExiledToHeapObjects) { + + DEBUG_OUTPUT(0) { + println("Forcing node ${nodeToString(ptgNode.node!!)} to escape") + } + + escapeOrigins += ptgNode + propagateEscapeOrigin(ptgNode) + } else { + ptgNode.forcedLifetime = Lifetime.GLOBAL // TODO: Change to LOCAL when supported. + } } } } - private fun propagate(kind: PointsToGraphNodeKind) { - val visited = mutableSetOf() - nodes.filter { it.value.kind == kind } - .forEach { node, _ -> propagate(node, kind, visited) } + private fun findReachableDrains(drain: PointsToGraphNode, visitedDrains: MutableSet) { + visitedDrains += drain + for (edge in drain.edges) { + if (edge is PointsToGraphEdge.Assignment) + error("A drain cannot have outgoing assignment edges") + val nextDrain = edge.node.drain + if (nextDrain !in visitedDrains) + findReachableDrains(nextDrain, visitedDrains) + } } - private fun propagate(node: DataFlowIR.Node, kind: PointsToGraphNodeKind, visited: MutableSet) { - if (visited.contains(node)) return - visited.add(node) - val nodeInfo = nodes[node]!! - if (nodeInfo.kind.weight < kind.weight) - nodeInfo.kind = kind - nodeInfo.edges.forEach { propagate(it, kind, visited) } + private fun paintNodes( + parameters: Array, + interestingDrains: Set, + drainFactory: () -> CompressedPointsToGraph.Node + ): MutableMap { + val nodeIds = mutableMapOf() + + for (index in parameters.indices) + nodeIds[parameters[index]] = CompressedPointsToGraph.Node.parameter(index, parameters.size) + + val standAloneDrains = interestingDrains.toMutableSet() + for (drain in interestingDrains) + for (edge in drain.edges) { + val node = edge.node + if (node.isDrain && node != drain /* Skip loops */) + standAloneDrains.remove(node) + } + for (drain in standAloneDrains) { + if (nodeIds[drain] == null + // A little optimization - skip leaf drains. + && drain.edges.any { it.node.drain in interestingDrains }) + nodeIds[drain] = drainFactory() + } + + var front = nodeIds.keys.toList() + while (front.isNotEmpty()) { + val nextFront = mutableListOf() + for (node in front) { + val nodeId = nodeIds[node]!! + node.edges.filterIsInstance().forEach { edge -> + val field = edge.field + val nextNode = edge.node + if (nextNode.drain in interestingDrains && nextNode != node /* Skip loops */) { + val nextNodeId = nodeId.goto(field) + if (nodeIds[nextNode] != null) + error("Expected only one incoming field edge. ${nodeIds[nextNode]} != $nextNodeId") + nodeIds[nextNode] = nextNodeId + if (nextNode.isDrain) + nextFront += nextNode + } + } + } + front = nextFront + } + for (drain in interestingDrains) { + if (nodeIds[drain] == null && drain.edges.any { it.node.drain in interestingDrains }) + error("Drains have not been painted properly") + } + + return nodeIds } } } @@ -733,6 +1724,11 @@ internal object EscapeAnalysis { val intraproceduralAnalysisResult = IntraproceduralAnalysis(context, moduleDFG, externalModulesDFG, callGraph).analyze() - InterproceduralAnalysis(callGraph, intraproceduralAnalysisResult, externalModulesDFG, lifetimes).analyze() + InterproceduralAnalysis(context, callGraph, intraproceduralAnalysisResult, externalModulesDFG, lifetimes, + // TODO: This is a bit conservative, but for more aggressive option some support from runtime is + // needed (namely, determining that a pointer is from the stack; this is easy for x86 or x64, + // but what about all other platforms?). + propagateExiledToHeapObjects = true + ).analyze() } } \ No newline at end of file diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/LocalEscapeAnalysis.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/LocalEscapeAnalysis.kt index 8b08ebf6116..c2494f22ea7 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/LocalEscapeAnalysis.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/LocalEscapeAnalysis.kt @@ -9,7 +9,6 @@ import org.jetbrains.kotlin.backend.konan.llvm.Lifetime import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.backend.konan.Context import org.jetbrains.kotlin.backend.konan.descriptors.isArrayWithFixedSizeItems -import org.jetbrains.kotlin.backend.konan.descriptors.isBuiltInOperator internal object LocalEscapeAnalysis { private val DEBUG = 0 @@ -187,7 +186,7 @@ internal object LocalEscapeAnalysis { else -> null } ir?.let { - lifetimes.put(it, Lifetime.LOCAL) + lifetimes.put(it, Lifetime.STACK) DEBUG_OUTPUT(3) { println("${ir} does not escape") } } } diff --git a/runtime/src/main/cpp/Memory.cpp b/runtime/src/main/cpp/Memory.cpp index 0a6967ff2cc..118258586e1 100644 --- a/runtime/src/main/cpp/Memory.cpp +++ b/runtime/src/main/cpp/Memory.cpp @@ -3252,6 +3252,13 @@ void UpdateReturnRefRelaxed(ObjHeader** returnSlot, const ObjHeader* value) { updateReturnRef(returnSlot, value); } +void ZeroArrayRefs(ArrayHeader* array) { + for (uint32_t index = 0; index < array->count_; ++index) { + ObjHeader** location = ArrayAddressOfElementAt(array, index); + zeroHeapRef(location); + } +} + void UpdateHeapRefIfNull(ObjHeader** location, const ObjHeader* object) { updateHeapRefIfNull(location, object); } @@ -3426,6 +3433,7 @@ void FreezeSubgraph(ObjHeader* root) { // This function is called from field mutators to check if object's header is frozen. // If object is frozen or permanent, an exception is thrown. void MutationCheck(ObjHeader* obj) { + if (obj->local()) return; auto* container = obj->container(); if (container == nullptr || container->frozen()) ThrowInvalidMutabilityException(obj); diff --git a/runtime/src/main/cpp/Memory.h b/runtime/src/main/cpp/Memory.h index e973433d8d8..cffc8a6e69c 100644 --- a/runtime/src/main/cpp/Memory.h +++ b/runtime/src/main/cpp/Memory.h @@ -501,7 +501,9 @@ MODEL_VARIANTS(void, SetStackRef, ObjHeader** location, const ObjHeader* object) // Sets heap location. MODEL_VARIANTS(void, SetHeapRef, ObjHeader** location, const ObjHeader* object); // Zeroes heap location. -void ZeroHeapRef(ObjHeader** location); +void ZeroHeapRef(ObjHeader** location) RUNTIME_NOTHROW; +// Zeroes an array. +void ZeroArrayRefs(ArrayHeader* array) RUNTIME_NOTHROW; // Zeroes stack location. MODEL_VARIANTS(void, ZeroStackRef, ObjHeader** location); // Updates stack location. diff --git a/runtime/src/main/kotlin/kotlin/Array.kt b/runtime/src/main/kotlin/kotlin/Array.kt index 6c48e9f0297..58eecb7e931 100644 --- a/runtime/src/main/kotlin/kotlin/Array.kt +++ b/runtime/src/main/kotlin/kotlin/Array.kt @@ -54,7 +54,7 @@ public final class Array { * If the [index] is out of bounds of this array, throws an [IndexOutOfBoundsException]. */ @SymbolName("Kotlin_Array_get") - @PointsTo(0b0100, 0, 0b0001) // points to , points to . + @PointsTo(0x000, 0x000, 0x002) // ret -> this.intestines external public operator fun get(index: Int): T /** @@ -67,7 +67,7 @@ public final class Array { * If the [index] is out of bounds of this array, throws an [IndexOutOfBoundsException]. */ @SymbolName("Kotlin_Array_set") - @PointsTo(0b0100, 0, 0b0001) // points to , points to . + @PointsTo(0x300, 0x000, 0x000) // this.intestines -> value external public operator fun set(index: Int, value: T): Unit /** diff --git a/runtime/src/main/kotlin/kotlin/Arrays.kt b/runtime/src/main/kotlin/kotlin/Arrays.kt index d0eb2ad4f67..638019f3594 100644 --- a/runtime/src/main/kotlin/kotlin/Arrays.kt +++ b/runtime/src/main/kotlin/kotlin/Arrays.kt @@ -528,7 +528,7 @@ public inline fun arrayOfNulls(size: Int): Array * Returns an array containing the specified elements. */ @TypedIntrinsic(IntrinsicType.IDENTITY) -@PointsTo(0, 1) // points to argument. +@PointsTo(0x00, 0x01) // ret -> elements public external inline fun arrayOf(vararg elements: T): Array @SymbolName("Kotlin_emptyArray") diff --git a/runtime/src/main/kotlin/kotlin/collections/ArrayUtil.kt b/runtime/src/main/kotlin/kotlin/collections/ArrayUtil.kt index 5e1fdb2c2f0..ad19f96b15b 100644 --- a/runtime/src/main/kotlin/kotlin/collections/ArrayUtil.kt +++ b/runtime/src/main/kotlin/kotlin/collections/ArrayUtil.kt @@ -14,7 +14,7 @@ import kotlin.native.internal.ExportForCppRuntime * either throwing exception or returning some kind of implementation-specific default value. */ @PublishedApi -internal fun arrayOfUninitializedElements(size: Int): Array { +internal inline fun arrayOfUninitializedElements(size: Int): Array { // TODO: special case for size == 0? require(size >= 0) { "capacity must be non-negative." } @Suppress("TYPE_PARAMETER_AS_REIFIED") @@ -76,7 +76,7 @@ internal fun Array.resetAt(index: Int) { } @SymbolName("Kotlin_Array_fillImpl") -@PointsTo(0b01000, 0, 0, 0b00001) // points to , points to . +@PointsTo(0x3000, 0x0000, 0x0000, 0x0000) // array.intestines -> value internal external fun arrayFill(array: Array, fromIndex: Int, toIndex: Int, value: T) @SymbolName("Kotlin_ByteArray_fillImpl") @@ -125,7 +125,7 @@ internal fun Array.resetRange(fromIndex: Int, toIndex: Int) { } @SymbolName("Kotlin_Array_copyImpl") -@PointsTo(0b000100, 0, 0b000001) // points to , points to . +@PointsTo(0x00000, 0x00000, 0x00004, 0x00000, 0x00000) // destination.intestines -> array.intestines internal external fun arrayCopy(array: Array, fromIndex: Int, destination: Array, toIndex: Int, count: Int) @SymbolName("Kotlin_ByteArray_copyImpl") diff --git a/runtime/src/main/kotlin/kotlin/native/Runtime.kt b/runtime/src/main/kotlin/kotlin/native/Runtime.kt index 8336dec2af4..4722db26c4f 100644 --- a/runtime/src/main/kotlin/kotlin/native/Runtime.kt +++ b/runtime/src/main/kotlin/kotlin/native/Runtime.kt @@ -6,6 +6,7 @@ package kotlin.native import kotlin.native.concurrent.isFrozen import kotlin.native.concurrent.InvalidMutabilityException +import kotlin.native.internal.Escapes /** * Initializes Kotlin runtime for the current thread, if not inited already. @@ -51,6 +52,7 @@ public fun setUnhandledExceptionHook(hook: ReportUnhandledExceptionHook): Report } @SymbolName("Kotlin_setUnhandledExceptionHook") +@Escapes(0b01) // escapes external private fun setUnhandledExceptionHook0(hook: ReportUnhandledExceptionHook): ReportUnhandledExceptionHook? /** diff --git a/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt b/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt index fc2a7154685..7483192b71a 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt @@ -90,6 +90,7 @@ internal annotation class FixmeConcurrency @Retention(AnnotationRetention.BINARY) internal annotation class Escapes(val who: Int) +// Decyphering of binary values can be found in EscapeAnalysis.kt @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.BINARY) internal annotation class PointsTo(vararg val onWhom: Int) diff --git a/runtime/src/main/kotlin/kotlin/native/ref/WeakPrivate.kt b/runtime/src/main/kotlin/kotlin/native/ref/WeakPrivate.kt index 994b682bc80..b5c3fb32319 100644 --- a/runtime/src/main/kotlin/kotlin/native/ref/WeakPrivate.kt +++ b/runtime/src/main/kotlin/kotlin/native/ref/WeakPrivate.kt @@ -9,6 +9,7 @@ import kotlinx.cinterop.COpaquePointer import kotlin.native.internal.ExportForCppRuntime import kotlin.native.internal.Frozen import kotlin.native.internal.NoReorderFields +import kotlin.native.internal.Escapes /** * Theory of operations: @@ -53,6 +54,7 @@ internal abstract class WeakReferenceImpl { // Get a counter from non-null object. @SymbolName("Konan_getWeakReferenceImpl") +@Escapes(0b01) // referent escapes. external internal fun getWeakReferenceImpl(referent: Any): WeakReferenceImpl // Create a counter object.