[K/N] Allow to optimize runtime bitcode separately

Since runtime is split into several LLVM modules there is no LTO
optimizations between them during debug compilation because the latter
performs almost no optimizations to preserve debug info.
Of course, it affects runtime performance of debug binaries. To improve
it, we can link and aggressively optimize runtime modules and then link
optimized module into unoptimized Kotlin LLVM module.
This commit also introduces `linkRuntime` binary option that forces the
way we link runtime modules.
This commit is contained in:
Sergey Bogolepov
2022-02-18 10:19:19 +03:00
committed by Space
parent e0dcb0975a
commit 3723e2af52
6 changed files with 341 additions and 125 deletions
@@ -27,6 +27,8 @@ object BinaryOptions : BinaryOptionRegistry() {
val unitSuspendFunctionObjCExport by option<UnitSuspendFunctionObjCExport>()
val gcSchedulerType by option<GCSchedulerType>()
val linkRuntime by option<RuntimeLinkageStrategyBinaryOption>()
}
open class BinaryOption<T : Any>(
@@ -78,15 +78,15 @@ internal fun produceCStubs(context: Context) {
private fun linkAllDependencies(context: Context, generatedBitcodeFiles: List<String>) {
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<St
bitcodeFiles += exceptionsSupportNativeLibrary
val llvmModule = context.llvmModule!!
runtimeModules.forEach {
val failed = llvmLinkModules2(context, llvmModule, it)
if (failed != 0) {
error("Failed to link ${it.getName()}")
}
}
bitcodeFiles.forEach {
parseAndLinkBitcodeFile(context, llvmModule, it)
}
@@ -205,14 +211,14 @@ internal fun produceOutput(context: Context) {
}
}
private fun parseAndLinkBitcodeFile(context: Context, llvmModule: LLVMModuleRef, path: String) {
internal fun parseAndLinkBitcodeFile(context: Context, llvmModule: LLVMModuleRef, path: String) {
val parsedModule = parseBitcodeFile(path)
if (!context.shouldUseDebugInfoFromNativeLibs()) {
LLVMStripModuleDebugInfo(parsedModule)
}
val failed = llvmLinkModules2(context, llvmModule, parsedModule)
if (failed != 0) {
throw Error("failed to link $path") // TODO: retrieve error message from LLVM.
throw Error("failed to link $path")
}
}
@@ -1,12 +1,11 @@
package org.jetbrains.kotlin.backend.konan
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.value
import kotlinx.cinterop.*
import llvm.*
import org.jetbrains.kotlin.backend.common.LoggingContext
import org.jetbrains.kotlin.backend.konan.llvm.*
import org.jetbrains.kotlin.konan.target.*
import java.io.Closeable
private fun initializeLlvmGlobalPassRegistry() {
val passRegistry = LLVMGetGlobalPassRegistry()
@@ -25,79 +24,244 @@ private fun initializeLlvmGlobalPassRegistry() {
LLVMInitializeObjCARCOpts(passRegistry)
}
enum class LlvmOptimizationLevel(val value: Int) {
NONE(0),
DEFAULT(1),
AGGRESSIVE(3)
}
private class LlvmPipelineConfiguration(context: Context) {
enum class LlvmSizeLevel(val value: Int) {
NONE(0),
DEFAULT(1),
AGGRESSIVE(2)
}
private val target = context.config.target
private val configurables: Configurables = context.config.platform.configurables
/**
* Incorporates everything that is used to tune a [LlvmOptimizationPipeline].
*/
data class LlvmPipelineConfig(
val targetTriple: String,
val cpuModel: String,
val cpuFeatures: String,
val optimizationLevel: LlvmOptimizationLevel,
val sizeLevel: LlvmSizeLevel,
val codegenOptimizationLevel: LLVMCodeGenOptLevel,
val relocMode: LLVMRelocMode,
val codeModel: LLVMCodeModel,
val globalDce: Boolean,
val internalize: Boolean,
val makeDeclarationsHidden: Boolean,
val objCPasses: Boolean,
val inlineThreshold: Int?,
)
val targetTriple: String = context.llvm.targetTriple
val cpuModel: String = configurables.targetCpu ?: run {
private fun getCpuModel(context: Context): String {
val target = context.config.target
val configurables: Configurables = context.config.platform.configurables
return configurables.targetCpu ?: run {
context.reportCompilationWarning("targetCpu for target $target was not set. Targeting `generic` cpu.")
"generic"
}
}
val cpuFeatures: String = configurables.targetCpuFeatures ?: ""
private fun getCpuFeatures(context: Context): String =
context.config.platform.configurables.targetCpuFeatures ?: ""
/**
* Null value means that LLVM should use default inliner params
* for the provided optimization and size level.
*/
val customInlineThreshold: Int? = when {
context.shouldOptimize() -> 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<LLVMTargetRefVar>()
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<LLVMTargetRefVar>()
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
}
}
private fun RelocationModeFlags.Mode.translateToLlvmRelocMode() = when (this) {
RelocationModeFlags.Mode.PIC -> LLVMRelocMode.LLVMRelocPIC
RelocationModeFlags.Mode.STATIC -> LLVMRelocMode.LLVMRelocStatic
RelocationModeFlags.Mode.DEFAULT -> LLVMRelocMode.LLVMRelocDefault
}
@@ -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<LLVMModuleRef>
/**
* Link runtime "as is", without any optimizations. Doable for "release" because LTO
* in this mode is quite aggressive.
*/
class Raw(private val runtimeNativeLibraries: List<LLVMModuleRef>) : RuntimeLinkageStrategy() {
override fun run(): List<LLVMModuleRef> =
runtimeNativeLibraries
}
/**
* Links all runtime modules into a single one and optimizes it.
*/
class LinkAndOptimize(
private val context: Context,
private val runtimeNativeLibraries: List<LLVMModuleRef>
) : RuntimeLinkageStrategy() {
override fun run(): List<LLVMModuleRef> {
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<LLVMModuleRef> = 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
}
@@ -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(
@@ -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<size_tVar>()
LLVMGetModuleIdentifier(this@getName, sizeVar.ptr)!!.toKStringFromUtf8()
}