[K/N] Devirtualization: virtual calls from external world fix
The fix deals with the situation where a virtual function potentially from the external world (because of the receiver) returns a final type (if it isn't final, then it's conservatively assumed that no devirtualization could be done on it). #KT-57791 Fixed
This commit is contained in:
+75
-22
@@ -173,6 +173,8 @@ internal object DevirtualizationAnalysis {
|
|||||||
|
|
||||||
class Function(val symbol: DataFlowIR.FunctionSymbol, val parameters: Array<Node>, val returns: Node, val throws: Node)
|
class Function(val symbol: DataFlowIR.FunctionSymbol, val parameters: Array<Node>, val returns: Node, val throws: Node)
|
||||||
|
|
||||||
|
class ExternalVirtualCall(val receiverNode: Node, val returnsNode: Node, val returnType: DataFlowIR.Type.Declared)
|
||||||
|
|
||||||
inner class ConstraintGraph {
|
inner class ConstraintGraph {
|
||||||
|
|
||||||
private var nodesCount = 0
|
private var nodesCount = 0
|
||||||
@@ -186,6 +188,7 @@ internal object DevirtualizationAnalysis {
|
|||||||
val externalFunctions = mutableMapOf<Pair<DataFlowIR.FunctionSymbol, DataFlowIR.Type>, Node>()
|
val externalFunctions = mutableMapOf<Pair<DataFlowIR.FunctionSymbol, DataFlowIR.Type>, Node>()
|
||||||
val fields = mutableMapOf<DataFlowIR.Field, Node>() // Do not distinguish receivers.
|
val fields = mutableMapOf<DataFlowIR.Field, Node>() // Do not distinguish receivers.
|
||||||
val virtualCallSiteReceivers = mutableMapOf<DataFlowIR.Node.VirtualCall, Node>()
|
val virtualCallSiteReceivers = mutableMapOf<DataFlowIR.Node.VirtualCall, Node>()
|
||||||
|
val externalVirtualCalls = mutableListOf<ExternalVirtualCall>()
|
||||||
|
|
||||||
private fun nextId(): Int = nodesCount++
|
private fun nextId(): Int = nodesCount++
|
||||||
|
|
||||||
@@ -597,6 +600,9 @@ internal object DevirtualizationAnalysis {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entryPoint == null)
|
||||||
|
propagateFinalTypesFromExternalVirtualCalls(directEdges)
|
||||||
|
|
||||||
context.logMultiple {
|
context.logMultiple {
|
||||||
topologicalOrder.forEachIndexed { index, multiNode ->
|
topologicalOrder.forEachIndexed { index, multiNode ->
|
||||||
+"Types of multi-node #$index"
|
+"Types of multi-node #$index"
|
||||||
@@ -623,6 +629,7 @@ internal object DevirtualizationAnalysis {
|
|||||||
if (receiverNode.types[VIRTUAL_TYPE_ID]) {
|
if (receiverNode.types[VIRTUAL_TYPE_ID]) {
|
||||||
context.logMultiple {
|
context.logMultiple {
|
||||||
+"Unable to devirtualize callsite ${virtualCall.debugString()}"
|
+"Unable to devirtualize callsite ${virtualCall.debugString()}"
|
||||||
|
+"from ${function.symbol}"
|
||||||
+" receiver is Virtual"
|
+" receiver is Virtual"
|
||||||
logPathToType(reversedEdges, receiverNode, VIRTUAL_TYPE_ID)
|
logPathToType(reversedEdges, receiverNode, VIRTUAL_TYPE_ID)
|
||||||
+""
|
+""
|
||||||
@@ -630,7 +637,10 @@ internal object DevirtualizationAnalysis {
|
|||||||
return@forEachNonScopeNode
|
return@forEachNonScopeNode
|
||||||
}
|
}
|
||||||
|
|
||||||
context.log { "Devirtualized callsite ${virtualCall.debugString()}" }
|
context.logMultiple {
|
||||||
|
+"Devirtualized callsite ${virtualCall.debugString()}"
|
||||||
|
+"from ${function.symbol}"
|
||||||
|
}
|
||||||
val receiverType = virtualCall.receiverType.resolved()
|
val receiverType = virtualCall.receiverType.resolved()
|
||||||
val possibleReceivers = mutableListOf<DataFlowIR.Type.Declared>()
|
val possibleReceivers = mutableListOf<DataFlowIR.Type.Declared>()
|
||||||
forEachBitInBoth(receiverNode.types, typeHierarchy.inheritorsOf(receiverType)) {
|
forEachBitInBoth(receiverNode.types, typeHierarchy.inheritorsOf(receiverType)) {
|
||||||
@@ -688,6 +698,50 @@ internal object DevirtualizationAnalysis {
|
|||||||
return AnalysisResult(result.asSequence().associateBy({ it.key }, { it.value.first }), typeHierarchy)
|
return AnalysisResult(result.asSequence().associateBy({ it.key }, { it.value.first }), typeHierarchy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If a virtual function is called on a receiver coming from external world and
|
||||||
|
* the return type of the function is a final class, then we conservatively assume
|
||||||
|
* that instance of this class could have been created by the call.
|
||||||
|
*/
|
||||||
|
private fun propagateFinalTypesFromExternalVirtualCalls(directEdges: IntArray) {
|
||||||
|
val nodesCount = constraintGraph.nodes.size
|
||||||
|
constraintGraph.externalVirtualCalls
|
||||||
|
.groupBy { it.returnType }
|
||||||
|
.forEach { (type, list) ->
|
||||||
|
val visited = BitSet(nodesCount)
|
||||||
|
val stack = mutableListOf<Node>()
|
||||||
|
list.forEach { call ->
|
||||||
|
val returnsNode = call.returnsNode
|
||||||
|
if (call.receiverNode.types[VIRTUAL_TYPE_ID] // Called from external world.
|
||||||
|
&& !returnsNode.types[type.index] && !visited[returnsNode.id]
|
||||||
|
) {
|
||||||
|
returnsNode.types.set(type.index)
|
||||||
|
stack.push(returnsNode)
|
||||||
|
visited.set(returnsNode.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (stack.isNotEmpty()) {
|
||||||
|
val node = stack.pop()
|
||||||
|
directEdges.forEachEdge(node.id) { distNodeId ->
|
||||||
|
val distNode = constraintGraph.nodes[distNodeId]
|
||||||
|
if (!distNode.types[type.index] && !visited[distNode.id]) {
|
||||||
|
distNode.types.set(type.index)
|
||||||
|
visited.set(distNode.id)
|
||||||
|
stack.push(distNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.directCastEdges?.forEach { edge ->
|
||||||
|
val distNode = edge.node
|
||||||
|
if (!distNode.types[type.index] && !visited[distNode.id] && edge.suitableTypes[type.index]) {
|
||||||
|
distNode.types.set(type.index)
|
||||||
|
visited.set(distNode.id)
|
||||||
|
stack.push(distNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Both [directEdges] and [reversedEdges] are the array representation of a graph:
|
// Both [directEdges] and [reversedEdges] are the array representation of a graph:
|
||||||
// for each node v the edges of that node are stored in edges[edges[v] until edges[v + 1]].
|
// for each node v the edges of that node are stored in edges[edges[v] until edges[v + 1]].
|
||||||
private data class ConstraintGraphBuildResult(val instantiatingClasses: BitSet,
|
private data class ConstraintGraphBuildResult(val instantiatingClasses: BitSet,
|
||||||
@@ -939,22 +993,6 @@ internal object DevirtualizationAnalysis {
|
|||||||
it?.and(instantiatingClasses)
|
it?.and(instantiatingClasses)
|
||||||
it?.set(VIRTUAL_TYPE_ID)
|
it?.set(VIRTUAL_TYPE_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entryPoint == null) {
|
|
||||||
for (list in typesVirtualCallSites)
|
|
||||||
for (virtualCall in list) {
|
|
||||||
val returnType = virtualCall.virtualCall.returnType.resolved()
|
|
||||||
val totalIncomingEdgesToReturnsNode =
|
|
||||||
if (virtualCall.returnsNode.id >= reversedEdgesCount.size)
|
|
||||||
0
|
|
||||||
else reversedEdgesCount[virtualCall.returnsNode.id]
|
|
||||||
if (returnType.isFinal && totalIncomingEdgesToReturnsNode == 0) {
|
|
||||||
// If we are in a library and facing final return type with no possible callees -
|
|
||||||
// this type still can be returned by some user of this library, so propagate it explicitly.
|
|
||||||
addEdge(concreteClass(returnType), virtualCall.returnsNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFunctionConstraintGraph(symbol: DataFlowIR.FunctionSymbol, isRoot: Boolean): Function? {
|
private fun createFunctionConstraintGraph(symbol: DataFlowIR.FunctionSymbol, isRoot: Boolean): Function? {
|
||||||
@@ -1202,12 +1240,16 @@ internal object DevirtualizationAnalysis {
|
|||||||
val actualCallee = allTypes[it].calleeAt(node)
|
val actualCallee = allTypes[it].calleeAt(node)
|
||||||
addEdge(doCall(actualCallee, arguments, actualCallee.returnParameter.type.resolved()), returnsNode)
|
addEdge(doCall(actualCallee, arguments, actualCallee.returnParameter.type.resolved()), returnsNode)
|
||||||
}
|
}
|
||||||
// Add cast to [Virtual] edge from receiver to returns, if return type is not final.
|
if (entryPoint == null) {
|
||||||
// With this we're reflecting the fact that unknown function can return anything.
|
// Add cast to [Virtual] edge from receiver to returns, if return type is not final.
|
||||||
if (!returnType.isFinal && entryPoint == null) {
|
// With this we're reflecting the fact that unknown function can return anything.
|
||||||
receiverNode.addCastEdge(Node.CastEdge(returnsNode, virtualTypeFilter))
|
if (!returnType.isFinal) {
|
||||||
|
receiverNode.addCastEdge(Node.CastEdge(returnsNode, virtualTypeFilter))
|
||||||
|
} else {
|
||||||
|
constraintGraph.externalVirtualCalls.add(ExternalVirtualCall(receiverNode, returnsNode, returnType))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// And throw anything.
|
// An external function can throw anything.
|
||||||
receiverNode.addCastEdge(Node.CastEdge(function.throws, virtualTypeFilter))
|
receiverNode.addCastEdge(Node.CastEdge(function.throws, virtualTypeFilter))
|
||||||
|
|
||||||
constraintGraph.virtualCallSiteReceivers[node] = receiverNode
|
constraintGraph.virtualCallSiteReceivers[node] = receiverNode
|
||||||
@@ -1437,6 +1479,9 @@ internal object DevirtualizationAnalysis {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var callSitesCount = 0
|
||||||
|
var devirtualizedCallSitesCount = 0
|
||||||
|
var actuallyDevirtualizedCallSitesCount = 0
|
||||||
irModule.transformChildren(object : IrElementTransformer<IrDeclarationParent?> {
|
irModule.transformChildren(object : IrElementTransformer<IrDeclarationParent?> {
|
||||||
override fun visitDeclaration(declaration: IrDeclarationBase, data: IrDeclarationParent?) =
|
override fun visitDeclaration(declaration: IrDeclarationBase, data: IrDeclarationParent?) =
|
||||||
super.visitDeclaration(declaration, declaration as? IrDeclarationParent ?: data)
|
super.visitDeclaration(declaration, declaration as? IrDeclarationParent ?: data)
|
||||||
@@ -1444,6 +1489,8 @@ internal object DevirtualizationAnalysis {
|
|||||||
override fun visitCall(expression: IrCall, data: IrDeclarationParent?): IrExpression {
|
override fun visitCall(expression: IrCall, data: IrDeclarationParent?): IrExpression {
|
||||||
expression.transformChildren(this, data)
|
expression.transformChildren(this, data)
|
||||||
|
|
||||||
|
if (expression.superQualifierSymbol == null && expression.symbol.owner.isOverridable)
|
||||||
|
++callSitesCount
|
||||||
val devirtualizedCallSite = devirtualizedCallSites[expression]
|
val devirtualizedCallSite = devirtualizedCallSites[expression]
|
||||||
val possibleCallees = devirtualizedCallSite?.possibleCallees
|
val possibleCallees = devirtualizedCallSite?.possibleCallees
|
||||||
if (possibleCallees == null
|
if (possibleCallees == null
|
||||||
@@ -1458,10 +1505,12 @@ internal object DevirtualizationAnalysis {
|
|||||||
val classMaxUnfoldFactor = 3
|
val classMaxUnfoldFactor = 3
|
||||||
val interfaceMaxUnfoldFactor = 3
|
val interfaceMaxUnfoldFactor = 3
|
||||||
val maxUnfoldFactor = if (owner.isInterface) interfaceMaxUnfoldFactor else classMaxUnfoldFactor
|
val maxUnfoldFactor = if (owner.isInterface) interfaceMaxUnfoldFactor else classMaxUnfoldFactor
|
||||||
|
++devirtualizedCallSitesCount
|
||||||
if (possibleCallees.size > maxUnfoldFactor) {
|
if (possibleCallees.size > maxUnfoldFactor) {
|
||||||
// Callsite too complicated to devirtualize.
|
// Callsite too complicated to devirtualize.
|
||||||
return expression
|
return expression
|
||||||
}
|
}
|
||||||
|
++actuallyDevirtualizedCallSitesCount
|
||||||
|
|
||||||
val startOffset = expression.startOffset
|
val startOffset = expression.startOffset
|
||||||
val endOffset = expression.endOffset
|
val endOffset = expression.endOffset
|
||||||
@@ -1556,5 +1605,9 @@ internal object DevirtualizationAnalysis {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, null)
|
}, null)
|
||||||
|
context.logMultiple {
|
||||||
|
+"Devirtualized: ${devirtualizedCallSitesCount * 100.0 / callSitesCount}%"
|
||||||
|
+"Actually devirtualized: ${actuallyDevirtualizedCallSitesCount * 100.0 / callSitesCount}%"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user