From 1dbe48707794d6fe61525edea1197ad5aa7c4b34 Mon Sep 17 00:00:00 2001 From: Jeffrey van Gogh Date: Fri, 7 Jun 2019 15:51:13 -0700 Subject: [PATCH] Reduce compiler runtime overhead of MaxStackFrameSizeAndLocalsCalculator Profiling the compilation of kotlinx.serialization, MaxStackFrameSizeAndLocalsCalculator causes ~7% of the runtime to be spent in java.lang.Object.hashCode This is through two uses: - visitMaxs(..) has a pushed hashSet that causes ~2% - labelWrappersMap used to attach additional data to asm Labels, causes ~ 5% visitMaxs can use the existing SmartSet (not to be confused with SmartHashSet) Analysis of the visitMaxs HashSet creation & sizes: | What | Amount | | calls to visitMaxs | 4416 | | max pushed | 158 | | median pushed | 4 | | average pushed | 5.20 | | stddev pushed | 7.66 | | 90 percentile | 10 | Analysis of labelWrappersMap creation & sizes: | What | Amount | | ------------------ | ------ | | hashtables created | 4006 | | max entries | 175 | | median entries | 5 | | average entries | 6.10 | | stdev entries | 8.28 | | 90 percentile | 11 | testing with a non hash based map using an array for keys and an array for values showed that the cost of MaxStackFrameSizeAndLocalsCalculator became neglible to the overall running time. SmartIdentityTable is a Map like structure that uses reference identity for keys. It uses 2 arrays to store keys & values until the number of entries stored is larger than 10. At that point it switches to using an IdentityHashMap. This structure can be used instead of HashMap when reference identity can be used and the number of entries inserted is small (<= 10) on average, drastically reducing the overhead of calls to Object.hashCode Between the two changes, compilation of kotlinx.serialization through kotlinc commandline decreased from 14 seconds to 11 seconds on my machine --- .../MaxStackFrameSizeAndLocalsCalculator.java | 9 +- .../kotlin/util/SmartIdentityTableTest.kt | 107 ++++++++++++++++++ .../kotlin/utils/SmartIdentityTable.kt | 86 ++++++++++++++ 3 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 compiler/tests/org/jetbrains/kotlin/util/SmartIdentityTableTest.kt create mode 100644 compiler/util/src/org/jetbrains/kotlin/utils/SmartIdentityTable.kt diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MaxStackFrameSizeAndLocalsCalculator.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MaxStackFrameSizeAndLocalsCalculator.java index 2868d597c01..dd84f3336ba 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MaxStackFrameSizeAndLocalsCalculator.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MaxStackFrameSizeAndLocalsCalculator.java @@ -46,8 +46,9 @@ package org.jetbrains.kotlin.codegen.inline; -import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.kotlin.utils.SmartSet; +import org.jetbrains.kotlin.utils.SmartIdentityTable; import org.jetbrains.org.objectweb.asm.*; import java.util.*; @@ -106,7 +107,7 @@ public class MaxStackFrameSizeAndLocalsCalculator extends MaxLocalsCalculator { private int maxStack; private final Collection exceptionHandlers = new LinkedList<>(); - private final Map labelWrappersMap = new HashMap<>(); + private final SmartIdentityTable labelWrappersTable = new SmartIdentityTable<>(); public MaxStackFrameSizeAndLocalsCalculator(int api, int access, String descriptor, MethodVisitor mv) { super(api, access, descriptor, mv); @@ -336,7 +337,7 @@ public class MaxStackFrameSizeAndLocalsCalculator extends MaxLocalsCalculator { */ int max = 0; Stack stack = new Stack<>(); - Set pushed = new HashSet<>(); + Set pushed = SmartSet.create(); stack.push(firstLabel); pushed.add(firstLabel); @@ -434,7 +435,7 @@ public class MaxStackFrameSizeAndLocalsCalculator extends MaxLocalsCalculator { // ------------------------------------------------------------------------ private LabelWrapper getLabelWrapper(Label label) { - return ContainerUtil.getOrCreate(labelWrappersMap, label, () -> new LabelWrapper(label)); + return labelWrappersTable.getOrCreate(label, () -> new LabelWrapper(label)); } private void increaseStackSize(int variation) { diff --git a/compiler/tests/org/jetbrains/kotlin/util/SmartIdentityTableTest.kt b/compiler/tests/org/jetbrains/kotlin/util/SmartIdentityTableTest.kt new file mode 100644 index 00000000000..5bfafa09634 --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/util/SmartIdentityTableTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2010-2019 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.util + +import org.jetbrains.kotlin.utils.SmartIdentityTable +import org.junit.Assert +import org.junit.Test + +class SmartIdentityTableTest { + class Key(val number: Int) { + override fun equals(other: Any?): Boolean { + Assert.fail("equals Should not be called") + return false + } + + override fun hashCode(): Int { + Assert.fail("equals Should not be called") + return 0 + } + } + + class Value(val number: Int) { + override fun equals(other: Any?): Boolean { + Assert.fail("equals Should not be called") + return false + } + + override fun hashCode(): Int { + Assert.fail("equals Should not be called") + return 0 + } + } + + @Test + fun basicTest() { + val key1 = Key(1) + val key2 = Key(2) + val key3 = Key(3) + val val1 = Value(1) + val val2 = Value(2) + val val3 = Value(3) + + val table = SmartIdentityTable() + + // insert two keys and ensure that those are present + table[key1] = val1 + table[key2] = val2 + + Assert.assertEquals(2, table.size) + Assert.assertTrue(table[key1] === val1) + Assert.assertTrue(table[key2] === val2) + + // replace existing key's value + table[key1] = val3 + + // expect size to stay the same + Assert.assertEquals(2, table.size) + + // values should be updated for key1 and same for other key + Assert.assertTrue(table[key1] === val3) + Assert.assertTrue(table[key2] === val2) + + // add a new key with existing value + table[key3] = val2 + + // new key should be added and existing keys should maintain their values + Assert.assertEquals(3, table.size) + Assert.assertTrue(table[key1] === val3) + Assert.assertTrue(table[key2] === val2) + Assert.assertTrue(table[key3] === val2) + + // create a key that has the same contents but a different reference identity, it should not be found in the table. + val secondKey1 = Key(1) + Assert.assertTrue(table[secondKey1] === null) + } + + @Test + fun growToMapTest() { + val table = SmartIdentityTable() + + // insert enough data to trigger conversion of the Table to use a Map + val keys = mutableListOf() + for (i in 0 until 15) { + val key = Key(i) + val value = Value(i) + keys.add(key) + table[key] = value + } + + Assert.assertEquals(15, table.size) + for (key in keys) { + val value = table[key] + Assert.assertNotNull(value) + Assert.assertEquals(key.number, table[key]!!.number) + } + } + + @Test + fun getOrCreateTest() { + val table = SmartIdentityTable() + val f = { Value(5) } + table.getOrCreate(Key(5), f) + } +} diff --git a/compiler/util/src/org/jetbrains/kotlin/utils/SmartIdentityTable.kt b/compiler/util/src/org/jetbrains/kotlin/utils/SmartIdentityTable.kt new file mode 100644 index 00000000000..5d5de9b594f --- /dev/null +++ b/compiler/util/src/org/jetbrains/kotlin/utils/SmartIdentityTable.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2019 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.utils + +import java.util.IdentityHashMap + +/** + * [SmartIdentityTable] is a Map like structure that uses reference identity for keys. + * It uses 2 arrays to store keys & values until the number of entries stored is larger than [ARRAY_UNTIL_SIZE]. + * At that point it switches to using an IdentityHashMap. + * + * This structure can be used instead of [HashMap] when reference identity can be used and + * the number of entries inserted is small (<= [ARRAY_UNTIL_SIZE]) on average, drastically reducing the overhead + * of calls to [Object.hashCode]. + * + * The implementation of [SmartIdentityTable] is not synchronized. + */ +class SmartIdentityTable { + + private var keysArray: MutableList? = ArrayList(ARRAY_UNTIL_SIZE) + private var valuesArray: MutableList? = ArrayList(ARRAY_UNTIL_SIZE) + private var largeMap: IdentityHashMap? = null + + val size: Int + get() = keysArray?.size ?: largeMap!!.size + + operator fun get(key: K): V? { + return keysArray?.let { + for ((index, k) in it.withIndex()) { + if (k === key) { + return valuesArray!![index] + } + } + return null + } ?: largeMap!![key] + } + + operator fun set(key: K, value: V): V? { + val ka = keysArray + if (ka != null) { + val va = valuesArray!! + // scan for existing keys in array + for (i in 0 until ka.size) { + if (ka[i] === key) { + val tmp = va[i] + va[i] = value + return tmp + } + } + // if a new key, and array has room + if (ka.size < ARRAY_UNTIL_SIZE) { + ka.add(key) + va.add(value) + return null + } + convertToHashMap() + } + // all other cases, fallback to IdentityHashMap implementation + return largeMap!!.put(key, value) + } + + private fun convertToHashMap() { + val map = IdentityHashMap() + val ka = keysArray!! + val va = valuesArray!! + for (i in 0 until ka.size) { + map[ka[i]] = va[i] + } + largeMap = map + keysArray = null + valuesArray = null + } + + fun getOrCreate(key: K, factory: () -> V): V { + return this[key] ?: factory().also { + this[key] = it + } + } + + companion object { + private const val ARRAY_UNTIL_SIZE = 10 + } +}