diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt index f829fce9985..bb98d68cdc7 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt @@ -27,6 +27,8 @@ object BinaryOptions : BinaryOptionRegistry() { val unitSuspendFunctionObjCExport by option() val gcSchedulerType by option() + + val linkRuntime by option() } open class BinaryOption( diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt index baeb4797253..151a329a3fe 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt @@ -78,15 +78,15 @@ internal fun produceCStubs(context: Context) { private fun linkAllDependencies(context: Context, generatedBitcodeFiles: List) { val config = context.config - val runtimeNativeLibraries = config.runtimeNativeLibraries - .takeIf { context.producedLlvmModuleContainsStdlib }.orEmpty() + // TODO: Possibly slow, maybe to a separate phase? + val runtimeModules = RuntimeLinkageStrategy.pick(context).run() val launcherNativeLibraries = config.launcherNativeLibraries .takeIf { config.produce == CompilerOutputKind.PROGRAM }.orEmpty() linkObjC(context) - val nativeLibraries = config.nativeLibraries + runtimeNativeLibraries + launcherNativeLibraries + val nativeLibraries = config.nativeLibraries + launcherNativeLibraries val bitcodeLibraries = context.llvm.bitcodeToLink.map { it.bitcodePaths }.flatten().filter { it.isBitcode } val additionalBitcodeFilesToLink = context.llvm.additionalProducedBitcodeFiles @@ -96,6 +96,12 @@ private fun linkAllDependencies(context: Context, generatedBitcodeFiles: List configurables.llvmInlineThreshold?.let { - it.toIntOrNull() ?: run { - context.reportCompilationWarning( - "`llvmInlineThreshold` should be an integer. Got `$it` instead. Using default value." - ) - null - } +private fun tryGetInlineThreshold(context: Context): Int? { + val configurables: Configurables = context.config.platform.configurables + return configurables.llvmInlineThreshold?.let { + it.toIntOrNull() ?: run { + context.reportCompilationWarning( + "`llvmInlineThreshold` should be an integer. Got `$it` instead. Using default value." + ) + null } - context.shouldContainDebugInfo() -> null - else -> null } +} +/** + * Creates [LlvmPipelineConfig] that is used for [RuntimeLinkageStrategy.LinkAndOptimize]. + * There is no DCE or internalization here because optimized module will be linked later. + * Still, runtime is not intended to be debugged by user, and we can optimize it pretty aggressively + * even in debug compilation. + */ +internal fun createLTOPipelineConfigForRuntime(context: Context): LlvmPipelineConfig { + val configurables: Configurables = context.config.platform.configurables + return LlvmPipelineConfig( + context.llvm.targetTriple, + getCpuModel(context), + getCpuFeatures(context), + LlvmOptimizationLevel.AGGRESSIVE, + LlvmSizeLevel.NONE, + LLVMCodeGenOptLevel.LLVMCodeGenLevelAggressive, + configurables.currentRelocationMode(context).translateToLlvmRelocMode(), + LLVMCodeModel.LLVMCodeModelDefault, + globalDce = false, + internalize = false, + objCPasses = configurables is AppleConfigurables, + makeDeclarationsHidden = false, + inlineThreshold = tryGetInlineThreshold(context) + ) +} + +/** + * In the end, Kotlin/Native generates a single LLVM module during compilation. + * It won't be linked with any other LLVM module, so we can hide and DCE unused symbols. + * + * The set of optimizations relies on current compiler configuration. + * In case of debug we do almost nothing (that's why we need [createLTOPipelineConfigForRuntime]), + * but for release binaries we rely on "closed" world and enable a lot of optimizations. + */ +internal fun createLTOFinalPipelineConfig(context: Context): LlvmPipelineConfig { + val target = context.config.target + val configurables: Configurables = context.config.platform.configurables + val cpuModel = getCpuModel(context) + val cpuFeatures = getCpuFeatures(context) val optimizationLevel: LlvmOptimizationLevel = when { context.shouldOptimize() -> LlvmOptimizationLevel.AGGRESSIVE context.shouldContainDebugInfo() -> LlvmOptimizationLevel.NONE else -> LlvmOptimizationLevel.DEFAULT } - val sizeLevel: LlvmSizeLevel = when { // We try to optimize code as much as possible on embedded targets. target is KonanTarget.ZEPHYR || - target == KonanTarget.WASM32 -> LlvmSizeLevel.AGGRESSIVE + target == KonanTarget.WASM32 -> LlvmSizeLevel.AGGRESSIVE context.shouldOptimize() -> LlvmSizeLevel.NONE context.shouldContainDebugInfo() -> LlvmSizeLevel.NONE else -> LlvmSizeLevel.NONE } - val codegenOptimizationLevel: LLVMCodeGenOptLevel = when { context.shouldOptimize() -> LLVMCodeGenOptLevel.LLVMCodeGenLevelAggressive context.shouldContainDebugInfo() -> LLVMCodeGenOptLevel.LLVMCodeGenLevelNone else -> LLVMCodeGenOptLevel.LLVMCodeGenLevelDefault } - val relocMode: LLVMRelocMode = configurables.currentRelocationMode(context).translateToLlvmRelocMode() - - private fun RelocationModeFlags.Mode.translateToLlvmRelocMode() = when (this) { - RelocationModeFlags.Mode.PIC -> LLVMRelocMode.LLVMRelocPIC - RelocationModeFlags.Mode.STATIC -> LLVMRelocMode.LLVMRelocStatic - RelocationModeFlags.Mode.DEFAULT -> LLVMRelocMode.LLVMRelocDefault - } - val codeModel: LLVMCodeModel = LLVMCodeModel.LLVMCodeModelDefault + val globalDce = true + // Since we are in a "closed world" internalization can be safely used + // to reduce size of a bitcode with global dce. + val internalize = context.llvmModuleSpecification.isFinal + // Hidden visibility makes symbols internal when linking the binary. + // When producing dynamic library, this enables stripping unused symbols from binary with -dead_strip flag, + // similar to DCE enabled by internalize but later: + // + // Important for binary size, workarounds references to undefined symbols from interop libraries. + val makeDeclarationsHidden = context.config.produce == CompilerOutputKind.STATIC_CACHE + val objcPasses = configurables is AppleConfigurables - enum class LlvmOptimizationLevel(val value: Int) { - NONE(0), - DEFAULT(1), - AGGRESSIVE(3) + // Null value means that LLVM should use default inliner params + // for the provided optimization and size level. + val inlineThreshold: Int? = when { + context.shouldOptimize() -> tryGetInlineThreshold(context) + context.shouldContainDebugInfo() -> null + else -> null } - enum class LlvmSizeLevel(val value: Int) { - NONE(0), - DEFAULT(1), - AGGRESSIVE(2) + return LlvmPipelineConfig( + context.llvm.targetTriple, + cpuModel, + cpuFeatures, + optimizationLevel, + sizeLevel, + codegenOptimizationLevel, + relocMode, + codeModel, + globalDce, + internalize, + makeDeclarationsHidden, + objcPasses, + inlineThreshold, + ) +} + +/** + * Prepares and executes LLVM LTO pipeline on the given [llvmModule]. + * + * Note: this class is intentionally uncoupled from [Context]. + * Please avoid depending on it. + */ +class LlvmOptimizationPipeline( + private val config: LlvmPipelineConfig, + private val llvmModule: LLVMModuleRef, + private val logger: LoggingContext? = null +) : Closeable { + private val arena = Arena() + + private val targetMachine: LLVMTargetMachineRef by lazy { + val target = arena.alloc() + val foundLlvmTarget = LLVMGetTargetFromTriple(config.targetTriple, target.ptr, null) == 0 + check(foundLlvmTarget) { "Cannot get target from triple ${config.targetTriple}." } + LLVMCreateTargetMachine( + target.value, + config.targetTriple, + config.cpuModel, + config.cpuFeatures, + config.codegenOptimizationLevel, + config.relocMode, + config.codeModel)!! + } + + private val modulePasses = LLVMCreatePassManager() + private val passBuilder = LLVMPassManagerBuilderCreate() + + private fun init() { + initLLVMTargets() + initializeLlvmGlobalPassRegistry() + } + + private fun populatePasses() { + LLVMPassManagerBuilderSetOptLevel(passBuilder, config.optimizationLevel.value) + LLVMPassManagerBuilderSetSizeLevel(passBuilder, config.sizeLevel.value) + LLVMKotlinAddTargetLibraryInfoWrapperPass(modulePasses, config.targetTriple) + // TargetTransformInfo pass. + LLVMAddAnalysisPasses(targetMachine, modulePasses) + if (config.internalize) { + LLVMAddInternalizePass(modulePasses, 0) + } + if (config.makeDeclarationsHidden) { + makeVisibilityHiddenLikeLlvmInternalizePass(llvmModule) + } + if (config.globalDce) { + LLVMAddGlobalDCEPass(modulePasses) + } + config.inlineThreshold?.let { threshold -> + LLVMPassManagerBuilderUseInlinerWithThreshold(passBuilder, threshold) + } + + // Pipeline that is similar to `llvm-lto`. + LLVMPassManagerBuilderPopulateLTOPassManager(passBuilder, modulePasses, Internalize = 0, RunInliner = 1) + + if (config.objCPasses) { + // Lower ObjC ARC intrinsics (e.g. `@llvm.objc.clang.arc.use(...)`). + // While Kotlin/Native codegen itself doesn't produce these intrinsics, they might come + // from cinterop "glue" bitcode. + // TODO: Consider adding other ObjC passes. + LLVMAddObjCARCContractPass(modulePasses) + } + } + + fun run() { + init() + populatePasses() + logger?.log { + """ + Running LLVM optimizations with the following parameters: + target_triple: ${config.targetTriple} + cpu_model: ${config.cpuModel} + cpu_features: ${config.cpuFeatures} + optimization_level: ${config.optimizationLevel.value} + size_level: ${config.sizeLevel.value} + inline_threshold: ${config.inlineThreshold ?: "default"} + """.trimIndent() + } + LLVMRunPassManager(modulePasses, llvmModule) + } + + override fun close() { + LLVMPassManagerBuilderDispose(passBuilder) + LLVMDisposeTargetMachine(targetMachine) + LLVMDisposePassManager(modulePasses) + arena.clear() } companion object { @@ -118,83 +282,15 @@ private class LlvmPipelineConfiguration(context: Context) { } } -internal fun runLlvmOptimizationPipeline(context: Context) { - val llvmModule = context.llvmModule!! - val config = LlvmPipelineConfiguration(context) - context.log { - """ - Running LLVM optimizations with the following parameters: - target_triple: ${config.targetTriple} - cpu_model: ${config.cpuModel} - cpu_features: ${config.cpuFeatures} - optimization_level: ${config.optimizationLevel.value} - size_level: ${config.sizeLevel.value} - inline_threshold: ${config.customInlineThreshold ?: "default"} - """.trimIndent() - } - - LlvmPipelineConfiguration.initLLVMTargets() - - memScoped { - initializeLlvmGlobalPassRegistry() - val passBuilder = LLVMPassManagerBuilderCreate() - val modulePasses = LLVMCreatePassManager() - LLVMPassManagerBuilderSetOptLevel(passBuilder, config.optimizationLevel.value) - LLVMPassManagerBuilderSetSizeLevel(passBuilder, config.sizeLevel.value) - // TODO: use LLVMGetTargetFromName instead. - val target = alloc() - val foundLlvmTarget = LLVMGetTargetFromTriple(config.targetTriple, target.ptr, null) == 0 - check(foundLlvmTarget) { "Cannot get target from triple ${config.targetTriple}." } - - val targetMachine = LLVMCreateTargetMachine( - target.value, - config.targetTriple, - config.cpuModel, - config.cpuFeatures, - config.codegenOptimizationLevel, - config.relocMode, - config.codeModel) - - LLVMKotlinAddTargetLibraryInfoWrapperPass(modulePasses, config.targetTriple) - // TargetTransformInfo pass. - LLVMAddAnalysisPasses(targetMachine, modulePasses) - if (context.llvmModuleSpecification.isFinal) { - // Since we are in a "closed world" internalization can be safely used - // to reduce size of a bitcode with global dce. - LLVMAddInternalizePass(modulePasses, 0) - } else if (context.config.produce == CompilerOutputKind.STATIC_CACHE) { - // Hidden visibility makes symbols internal when linking the binary. - // When producing dynamic library, this enables stripping unused symbols from binary with -dead_strip flag, - // similar to DCE enabled by internalize but later: - makeVisibilityHiddenLikeLlvmInternalizePass(llvmModule) - // Important for binary size, workarounds references to undefined symbols from interop libraries. - } - LLVMAddGlobalDCEPass(modulePasses) - - config.customInlineThreshold?.let { threshold -> - LLVMPassManagerBuilderUseInlinerWithThreshold(passBuilder, threshold) - } - - // Pipeline that is similar to `llvm-lto`. - LLVMPassManagerBuilderPopulateLTOPassManager(passBuilder, modulePasses, Internalize = 0, RunInliner = 1) - - // Lower ObjC ARC intrinsics (e.g. `@llvm.objc.clang.arc.use(...)`). - // While Kotlin/Native codegen itself doesn't produce these intrinsics, they might come - // from cinterop "glue" bitcode. - // TODO: Consider adding other ObjC passes. - LLVMAddObjCARCContractPass(modulePasses) - - LLVMRunPassManager(modulePasses, llvmModule) - - LLVMPassManagerBuilderDispose(passBuilder) - LLVMDisposeTargetMachine(targetMachine) - LLVMDisposePassManager(modulePasses) - } -} - internal fun RelocationModeFlags.currentRelocationMode(context: Context): RelocationModeFlags.Mode = when (determineLinkerOutput(context)) { LinkerOutputKind.DYNAMIC_LIBRARY -> dynamicLibraryRelocationMode LinkerOutputKind.STATIC_LIBRARY -> staticLibraryRelocationMode LinkerOutputKind.EXECUTABLE -> executableRelocationMode - } \ No newline at end of file + } + +private fun RelocationModeFlags.Mode.translateToLlvmRelocMode() = when (this) { + RelocationModeFlags.Mode.PIC -> LLVMRelocMode.LLVMRelocPIC + RelocationModeFlags.Mode.STATIC -> LLVMRelocMode.LLVMRelocStatic + RelocationModeFlags.Mode.DEFAULT -> LLVMRelocMode.LLVMRelocDefault +} \ No newline at end of file diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/RuntimeLinkageStrategy.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/RuntimeLinkageStrategy.kt new file mode 100644 index 00000000000..7ccead0f0bb --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/RuntimeLinkageStrategy.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.backend.konan + +import llvm.LLVMModuleCreateWithNameInContext +import llvm.LLVMModuleRef +import llvm.LLVMStripModuleDebugInfo +import org.jetbrains.kotlin.backend.konan.llvm.getName +import org.jetbrains.kotlin.backend.konan.llvm.llvmContext +import org.jetbrains.kotlin.backend.konan.llvm.llvmLinkModules2 +import org.jetbrains.kotlin.backend.konan.llvm.parseBitcodeFile + +/** + * To avoid combinatorial explosion, we split runtime into several LLVM modules. + * This approach might cause performance degradation in some compilation modes because + * there is no LTO between runtime modules. + * RuntimeLinkageStrategy allows to choose the way we link runtime into final application or cache + * and mitigate the problem above. + * + */ +internal sealed class RuntimeLinkageStrategy { + + abstract fun run(): List + + /** + * Link runtime "as is", without any optimizations. Doable for "release" because LTO + * in this mode is quite aggressive. + */ + class Raw(private val runtimeNativeLibraries: List) : RuntimeLinkageStrategy() { + + override fun run(): List = + runtimeNativeLibraries + } + + /** + * Links all runtime modules into a single one and optimizes it. + */ + class LinkAndOptimize( + private val context: Context, + private val runtimeNativeLibraries: List + ) : RuntimeLinkageStrategy() { + + override fun run(): List { + if (runtimeNativeLibraries.isEmpty()) { + return emptyList() + } + val runtimeModule = LLVMModuleCreateWithNameInContext("runtime", llvmContext)!! + runtimeNativeLibraries.forEach { + val failed = llvmLinkModules2(context, runtimeModule, it) + if (failed != 0) { + throw Error("Failed to link ${it.getName()}") + } + } + val config = createLTOPipelineConfigForRuntime(context) + LlvmOptimizationPipeline(config, runtimeModule, context).use { + it.run() + } + return listOf(runtimeModule) + } + } + + /** + * Used in cases when runtime is not linked directly, e.g. it is a part of stdlib cache. + */ + object None : RuntimeLinkageStrategy() { + override fun run(): List = emptyList() + } + + companion object { + /** + * Choose runtime linkage strategy based on current compiler configuration and [BinaryOptions.linkRuntime]. + */ + internal fun pick(context: Context): RuntimeLinkageStrategy { + val binaryOption = context.config.configuration.get(BinaryOptions.linkRuntime) + val runtimeNativeLibraries = context.config.runtimeNativeLibraries + .takeIf { context.producedLlvmModuleContainsStdlib } + val runtimeLlvmModules = runtimeNativeLibraries?.map { + val parsedModule = parseBitcodeFile(it) + if (!context.shouldUseDebugInfoFromNativeLibs()) { + LLVMStripModuleDebugInfo(parsedModule) + } + parsedModule + } + return when { + runtimeLlvmModules == null -> return None + binaryOption == RuntimeLinkageStrategyBinaryOption.Raw -> Raw(runtimeLlvmModules) + binaryOption == RuntimeLinkageStrategyBinaryOption.Optimize -> LinkAndOptimize(context, runtimeLlvmModules) + context.config.debug -> Raw(runtimeLlvmModules) + else -> Raw(runtimeLlvmModules) + } + + } + } +} + +enum class RuntimeLinkageStrategyBinaryOption { + Raw, + Optimize +} \ No newline at end of file diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt index e21ccc3a6d2..c1581983041 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt @@ -356,7 +356,12 @@ internal val rewriteExternalCallsCheckerGlobals = makeKonanModuleOpPhase( internal val bitcodeOptimizationPhase = makeKonanModuleOpPhase( name = "BitcodeOptimization", description = "Optimize bitcode", - op = { context, _ -> runLlvmOptimizationPipeline(context) } + op = { context, _ -> + val config = createLTOFinalPipelineConfig(context) + LlvmOptimizationPipeline(config, context.llvmModule!!, context).use { + it.run() + } + } ) internal val coveragePhase = makeKonanModuleOpPhase( diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/LlvmUtils.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/LlvmUtils.kt index 14a6cc8feb1..2c085139cc1 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/LlvmUtils.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/LlvmUtils.kt @@ -457,3 +457,8 @@ fun LLVMTypeRef.isVectorElementType(): Boolean = when (llvm.LLVMGetTypeKind(this LLVMTypeKind.LLVMDoubleTypeKind -> true else -> false } + +fun LLVMModuleRef.getName(): String = memScoped { + val sizeVar = alloc() + LLVMGetModuleIdentifier(this@getName, sizeVar.ptr)!!.toKStringFromUtf8() +} \ No newline at end of file