From 879c43e1a4c5e3ec3daf77069989d68090e5aedd Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Mon, 14 Aug 2023 17:25:33 -0400 Subject: [PATCH] Integer fast-path for hashCode calculation (#216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An integer is its own hashCode, and the function is documented to simply return the input on the JVM. For JS and native the behavior is the same: https://github.com/JetBrains/kotlin/blob/1.9.0/kotlin-native/runtime/src/main/kotlin/kotlin/Primitives.kt#L1353. Saves at least 3 bytes per property within the function bytecode, not counting any effects on things like the constant pool (for JVM). Before: public int hashCode(); Code: 0: aload_0 1: getfield #23 // Field int:I 4: invokestatic #50 // Method java/lang/Integer.hashCode:(I)I 7: istore_1 8: iload_1 9: bipush 31 11: imul ⋮ 46: ireturn After: public int hashCode(); Code: 0: aload_0 1: getfield #23 // Field int:I 4: istore_1 5: iload_1 6: bipush 31 8: imul ⋮ 43: ireturn --- gradle/libs.versions.toml | 1 + poko-compiler-plugin/build.gradle.kts | 1 + .../drewhamilton/poko/ir/hashCodeGeneration.kt | 6 ++++++ .../drewhamilton/poko/PokoCompilerPluginTest.kt | 17 +++++++++++++++++ .../test/kotlin/dev/drewhamilton/poko/asm.kt | 16 ++++++++++++++++ 5 files changed, 41 insertions(+) create mode 100644 poko-compiler-plugin/src/test/kotlin/dev/drewhamilton/poko/asm.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0637ffe7..9bad0175 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl kotlin-gradleApi = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" } assertk = "com.willowtreeapps.assertk:assertk:0.26.1" +asm-util = "org.ow2.asm:asm-util:9.5" [plugins] diff --git a/poko-compiler-plugin/build.gradle.kts b/poko-compiler-plugin/build.gradle.kts index 61928bdb..f718f370 100644 --- a/poko-compiler-plugin/build.gradle.kts +++ b/poko-compiler-plugin/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { testImplementation(libs.kotlin.compileTesting) testImplementation(libs.junit) testImplementation(libs.assertk) + testImplementation(libs.asm.util) } tasks.withType().configureEach { diff --git a/poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/hashCodeGeneration.kt b/poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/hashCodeGeneration.kt index 73397137..60bfb6e3 100644 --- a/poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/hashCodeGeneration.kt +++ b/poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/hashCodeGeneration.kt @@ -31,6 +31,7 @@ import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol import org.jetbrains.kotlin.ir.symbols.impl.IrVariableSymbolImpl import org.jetbrains.kotlin.ir.types.classifierOrFail import org.jetbrains.kotlin.ir.types.classifierOrNull +import org.jetbrains.kotlin.ir.types.isInt import org.jetbrains.kotlin.ir.types.isNullable import org.jetbrains.kotlin.ir.util.functions import org.jetbrains.kotlin.ir.util.render @@ -139,6 +140,11 @@ private fun IrBlockBodyBuilder.getHashCodeOf( value: IrExpression, messageCollector: MessageCollector, ): IrExpression { + // Fast path for integers which are already their own hashCode value. + if (property.type.isInt()) { + return value + } + val hasArrayContentBasedAnnotation = property.hasArrayContentBasedAnnotation() val classifier = property.type.classifierOrNull diff --git a/poko-compiler-plugin/src/test/kotlin/dev/drewhamilton/poko/PokoCompilerPluginTest.kt b/poko-compiler-plugin/src/test/kotlin/dev/drewhamilton/poko/PokoCompilerPluginTest.kt index 73b6e500..01eef74f 100644 --- a/poko-compiler-plugin/src/test/kotlin/dev/drewhamilton/poko/PokoCompilerPluginTest.kt +++ b/poko-compiler-plugin/src/test/kotlin/dev/drewhamilton/poko/PokoCompilerPluginTest.kt @@ -1,7 +1,9 @@ package dev.drewhamilton.poko +import assertk.all import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.doesNotContain import assertk.assertions.isEqualTo import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.PluginOption @@ -122,6 +124,21 @@ class PokoCompilerPluginTest { } //endregion + //region Performance optimizations + @Test fun `int property does not emit hashCode method invocation`() { + testCompilation( + "api/Primitives", + ) { + val classFile = it.generatedFiles.single { it.name.endsWith(".class") } + val bytecode = bytecodeToText(classFile.readBytes()) + assertThat(bytecode).all { + contains("java/lang/Long.hashCode") + doesNotContain("java/lang/Integer.hashCode") + } + } + } + //endregion + private inline fun testCompilation( vararg sourceFileNames: String, pokoAnnotationName: String = Poko::class.java.name, diff --git a/poko-compiler-plugin/src/test/kotlin/dev/drewhamilton/poko/asm.kt b/poko-compiler-plugin/src/test/kotlin/dev/drewhamilton/poko/asm.kt new file mode 100644 index 00000000..2b1824e6 --- /dev/null +++ b/poko-compiler-plugin/src/test/kotlin/dev/drewhamilton/poko/asm.kt @@ -0,0 +1,16 @@ +package dev.drewhamilton.poko + +import java.io.PrintWriter +import java.io.StringWriter +import org.objectweb.asm.ClassReader +import org.objectweb.asm.util.Textifier +import org.objectweb.asm.util.TraceClassVisitor + +fun bytecodeToText(bytecode: ByteArray): String { + val textifier = Textifier() + ClassReader(bytecode).accept(TraceClassVisitor(null, textifier, null), 0) + + val writer = StringWriter() + textifier.print(PrintWriter(writer)) + return writer.toString().trim() +}