From 39c8939241f36b1d3532261ed732e5f7f4ab2096 Mon Sep 17 00:00:00 2001 From: pyos Date: Tue, 26 Mar 2019 15:17:42 +0100 Subject: [PATCH] JVM_IR: optimize SAM conversion of lambda literals Instead of materializing an invokable object and storing it in an instance of the implementation, create a specialized class that calls the lambda directly, resulting in one less nesting layer and one less class. --- .../jvm/lower/SingleAbstractMethodLowering.kt | 165 +++++++++++++----- 1 file changed, 120 insertions(+), 45 deletions(-) diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/lower/SingleAbstractMethodLowering.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/lower/SingleAbstractMethodLowering.kt index f5647436df0..4efad70079c 100644 --- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/lower/SingleAbstractMethodLowering.kt +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/lower/SingleAbstractMethodLowering.kt @@ -41,7 +41,8 @@ class SingleAbstractMethodLowering(val context: CommonBackendContext) : ClassLow override fun lower(irClass: IrClass) { // TODO use `IrType`s; unfortunately, they are currently not comparable. // (Can't use class symbols here, we want type parameters as well.) - val implementations = mutableMapOf, IrClass>() + val cachedImplementations = mutableMapOf, IrClass>() + val localImplementations = mutableListOf() irClass.transformChildrenVoid(object : IrElementTransformerVoidWithContext() { override fun visitCall(expression: IrCall): IrExpression { if (expression.symbol.owner.descriptor !is SamConstructorDescriptor) @@ -55,49 +56,51 @@ class SingleAbstractMethodLowering(val context: CommonBackendContext) : ClassLow if (expression.operator != IrTypeOperator.SAM_CONVERSION) return super.visitTypeOperator(expression) // Implicit conversion to an interface: - // val a: (Runnable) -> Unit = { it.run() } // pretend this is a Java method - // val b: () -> Unit = { print("Hello, World!") } - // a(b) + // // FILE: 1.java + // class A { public static void f(Runnable r) { r.run() } } + // // FILE: 2.kt + // A.f { print("Hello, World!") } return createInstance(expression.typeOperand, expression.argument.transform(this, null)) } // Replace a SAM-conversion of a callable object with an instance of an appropriate implementation. private fun createInstance(superType: IrType, invokable: IrExpression): IrExpression { - val implementation = implementations.getOrPut(superType.toKotlinType() to invokable.type.toKotlinType()) { - createClass(superType, invokable.type) - } // Running in the class context, so empty scope means this is a field/anonymous initializer. val scopeOwnerSymbol = currentScope?.scope?.scopeOwnerSymbol ?: irClass.symbol return context.createIrBuilder(scopeOwnerSymbol).irBlock(invokable, null, superType) { - // TODO if invokable is a callable reference, avoid materializing it? - if (superType.isNullable() && invokable.type.isNullable()) { - val invokableVariable = scope.createTemporaryVariable(invokable) - val instance = irCall(implementation.constructors.single()).apply { putValueArgument(0, irGet(invokableVariable)) } - +invokableVariable - +irIfNull(superType, irGet(invokableVariable), irNull(), instance) + // Not an exhaustive check, but enough to cover the most common cases. + if (invokable is IrFunctionReference) { + // Runnable(::function) + createFunctionProxyInstance(superType, invokable) + } else if (invokable is IrBlock && invokable.statements.last() is IrFunctionReference) { + // Runnable { lambda } + for (statement in invokable.statements.dropLast(1)) + +statement + createFunctionProxyInstance(superType, invokable.statements.last() as IrFunctionReference) } else { - +irCall(implementation.constructors.single()).apply { putValueArgument(0, invokable) } + // Fall back to materializing an invokable object. + createObjectProxyInstance(superType, invokable) } } } - // Construct a class that wraps an invokable object into an implementation of an interface: - // class sam$n(private val invokable: F) : Interface() { override fun method(...) = invokable(...) } - private fun createClass(superType: IrType, invokableType: IrType): IrClass { - val invokableClass = invokableType.classifierOrFail.owner as IrClass - val invoke = invokableClass.functions.single { it.name == OperatorNameConventions.INVOKE } - + // Construct a class that implements the specified SAM interface and contains a bunch of fields + // in which the arguments to the constructor are stored: + // class sam$n(private val arg0: T0, ...) { override fun method(...) = } + private fun implement( + superType: IrType, + parameters: List, + buildOverride: IrSimpleFunction.(List) -> IrBody + ): IrClass { val superClass = superType.classifierOrFail.owner as IrClass - val superConstructor = if (superClass.kind != ClassKind.INTERFACE) - superClass.constructors.single { it.valueParameters.size == 0 } - else - context.irBuiltIns.anyClass.owner.constructors.single { it.valueParameters.size == 0 } - val superMethod = superClass.functions.single { it.modality == Modality.ABSTRACT } + // The language documentation prohibits casting lambdas to classes, but if it was allowed, + // the `irDelegatingConstructorCall` in the constructor below would need to be modified. + assert(superClass.kind == ClassKind.INTERFACE) { "SAM conversion to an abstract class not allowed" } val superClassName = superClass.fqNameSafe.pathSegments().joinToString("_") { it.toString() } val subclass = buildClass { // TODO this is not the name some tests (e.g. kt11519) expect - name = Name.identifier("sam\$${superClassName}\$${implementations.size}") + name = Name.identifier("sam\$$superClassName\$${cachedImplementations.size + localImplementations.size}") origin = JvmLoweredDeclarationOrigin.GENERATED_SAM_IMPLEMENTATION }.apply { createImplicitParameterDeclarationWithWrappedDescriptor() @@ -106,31 +109,35 @@ class SingleAbstractMethodLowering(val context: CommonBackendContext) : ClassLow superTypes += superType } - val invokableField = subclass.addField { - name = Name.identifier("invokable") - type = invokableType - origin = subclass.origin - visibility = Visibilities.PRIVATE + val fields = parameters.mapIndexed { i, fieldType -> + subclass.addField { + name = Name.identifier("arg$i") + type = fieldType + origin = subclass.origin + visibility = Visibilities.PRIVATE + } } subclass.addConstructor { - returnType = subclass.defaultType origin = subclass.origin isPrimary = true }.apply { - addValueParameter { - name = invokableField.name - type = invokableType - index = 0 - origin = subclass.origin + for (field in fields) { + addValueParameter { + name = field.name + type = field.type + origin = subclass.origin + } } body = context.createIrBuilder(symbol).irBlockBody(startOffset, endOffset) { - +irDelegatingConstructorCall(superConstructor) - +irSetField(irGet(subclass.thisReceiver!!), invokableField, irGet(valueParameters[0])) - +IrInstanceInitializerCallImpl(startOffset, endOffset, superClass.symbol, context.irBuiltIns.unitType) + +irDelegatingConstructorCall(context.irBuiltIns.anyClass.owner.constructors.single()) + for ((field, parameter) in fields.zip(valueParameters)) + +irSetField(irGet(subclass.thisReceiver!!), field, irGet(parameter)) + +IrInstanceInitializerCallImpl(startOffset, endOffset, subclass.symbol, context.irBuiltIns.unitType) } } + val superMethod = superClass.functions.single { it.modality == Modality.ABSTRACT } subclass.addFunction { name = superMethod.name returnType = superMethod.returnType @@ -140,17 +147,85 @@ class SingleAbstractMethodLowering(val context: CommonBackendContext) : ClassLow overriddenSymbols.add(superMethod.symbol) dispatchReceiverParameter = subclass.thisReceiver!!.copyTo(this) superMethod.valueParameters.mapTo(valueParameters) { it.copyTo(this) } - body = context.createIrBuilder(symbol).irBlockBody(startOffset, endOffset) { - +irReturn(irCall(invoke.symbol).apply { - dispatchReceiver = irGetField(irGet(subclass.thisReceiver!!), invokableField) + body = buildOverride(fields) + } + + return subclass + } + + // Construct a class that wraps an invokable object into an implementation of an interface: + // class sam$n(private val invokable: F) : Interface { override fun method(...) = invokable(...) } + private fun createObjectProxy(superType: IrType, invokableType: IrType): IrClass = + implement(superType, listOf(invokableType)) { fields -> + context.createIrBuilder(symbol).irBlockBody(startOffset, endOffset) { + val invokableClass = invokableType.classifierOrFail.owner as IrClass + +irReturn(irCall(invokableClass.functions.single { it.name == OperatorNameConventions.INVOKE }).apply { + dispatchReceiver = irGetField(irGet(dispatchReceiverParameter!!), fields[0]) valueParameters.forEachIndexed { i, parameter -> putValueArgument(i, irGet(parameter)) } }) } } - return subclass + private fun IrBlockBuilder.createObjectProxyInstance(superType: IrType, invokable: IrExpression) { + val implementation = cachedImplementations.getOrPut(superType.toKotlinType() to invokable.type.toKotlinType()) { + createObjectProxy(superType, invokable.type) + } + if (superType.isNullable() && invokable.type.isNullable()) { + val invokableVariable = irTemporary(invokable) + val instance = irCall(implementation.constructors.single()).apply { putValueArgument(0, irGet(invokableVariable)) } + +irIfNull(superType, irGet(invokableVariable), irNull(), instance) + } else { + +irCall(implementation.constructors.single()).apply { putValueArgument(0, invokable) } + } + } + + // This lowering is located after LocalDeclarationsLowering. On the one hand, this allows us to cache + // object-wrapping classes without checking if the types are local declarations or not. On the other hand, + // this means we need to manually create fields for all bound arguments when wrapping callable references. + private val IrFunctionReference.arguments: List + get() = listOf(dispatchReceiver).filter { symbol.owner.dispatchReceiverParameter != null } + + listOf(extensionReceiver).filter { symbol.owner.extensionReceiverParameter != null } + + (0 until valueArgumentsCount).map(::getValueArgument) + + // Construct a class that forwards method calls to an existing function/method: + // class sam$n(private val receiver: R) : Interface { override fun method(...) = receiver.target(...) } + // + // Unlike the above variant, this avoids materializing an invokable KFunction representing + // the target in CallableReferenceLowering, thus producing one less class. This is actually very + // common, as `Interface { something }` is a local function + a SAM-conversion of a reference + // to it into an implementation. + private fun createFunctionProxy(superType: IrType, reference: IrFunctionReference): IrClass = + implement(superType, reference.arguments.mapNotNull { it?.type }) { fields -> + context.createIrBuilder(symbol).irBlockBody(startOffset, endOffset) { + +irReturn(irCall(reference.symbol).apply { + var boundOffset = 0 + var offset = 0 + val arguments = reference.arguments.map { + if (it != null) + irGetField(irGet(dispatchReceiverParameter!!), fields[boundOffset++]) + else + irGet(valueParameters[offset++]) + }.toMutableList() + if (reference.symbol.owner.dispatchReceiverParameter != null) { + dispatchReceiver = arguments.removeAt(0) + } + if (reference.symbol.owner.extensionReceiverParameter != null) { + extensionReceiver = arguments.removeAt(0) + } + arguments.forEachIndexed(::putValueArgument) + }) + } + } + + private fun IrBlockBuilder.createFunctionProxyInstance(superType: IrType, reference: IrFunctionReference) { + val implementation = createFunctionProxy(superType, reference) + localImplementations += implementation + +irCall(implementation.constructors.single()).apply { + reference.arguments.filterNotNull().forEachIndexed(::putValueArgument) + } } }) - irClass.declarations.addAll(implementations.values) + irClass.declarations += cachedImplementations.values + irClass.declarations += localImplementations } }