[
https://issues.apache.org/jira/browse/GROOVY-10307?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18058775#comment-18058775
]
ASF GitHub Bot commented on GROOVY-10307:
-----------------------------------------
jamesfredley commented on code in PR #2381:
URL: https://github.com/apache/groovy/pull/2381#discussion_r2810132447
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/LoopsBench.groovy:
##########
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.perf
+
+import org.openjdk.jmh.annotations.*
+import org.openjdk.jmh.infra.Blackhole
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Tests the overhead of repeated closure and method invocation within
+ * tight loops. Focuses on loop-specific patterns: closure-in-loop vs
+ * method-in-loop, nested iteration, and minimal vs complex loop bodies.
+ *
+ * Collection operation benchmarks (each/collect/findAll/inject on lists)
+ * are in {@link ClosureBench}.
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(3)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+class LoopsBench {
+ static final int LOOP_COUNT = 1_000_000
+
+ /**
+ * Loop with [1].each and toString() — exercises closure dispatch
+ * and virtual method call on each iteration.
+ */
+ @Benchmark
+ void originalEachToString(Blackhole bh) {
+ for (int i = 0; i < LOOP_COUNT; i++) {
+ [1].each { bh.consume(it.toString()) }
+ }
+ }
+
+ /**
+ * Minimal each loop — isolates closure dispatch overhead from toString()
cost.
+ */
+ @Benchmark
+ void eachIdentity(Blackhole bh) {
+ for (int i = 0; i < LOOP_COUNT; i++) {
+ [1].each { bh.consume(it) }
Review Comment:
Same as above — intentional. This is the inline-allocation variant; it pairs
with a pre-allocated-list variant so the two together isolate the allocation
cost from the iteration cost.
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/PropertyAccessBench.groovy:
##########
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.perf
+
+import org.openjdk.jmh.annotations.*
+import org.openjdk.jmh.infra.Blackhole
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Tests the performance of Groovy property access patterns including
+ * field read/write, getter/setter dispatch, dynamically-typed property
+ * access, map bracket and dot-property notation, and chained property
+ * resolution.
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(3)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+class PropertyAccessBench {
+ static final int ITERATIONS = 1_000_000
+
+ int instanceField = 42
+ String stringProperty = "hello"
+
+ // Explicit getter/setter for comparison
+ private int _backingField = 10
+ int getBackingField() { _backingField }
+ void setBackingField(int value) { _backingField = value }
+
+ /**
+ * Read/write a public field — the simplest property access path.
+ */
+ @Benchmark
+ void fieldReadWrite(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ instanceField = i
+ sum += instanceField
Review Comment:
Fixed — updated the javadoc to clarify this tests Groovy property access
(which generates getter/setter), not direct field access.
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/PropertyAccessBench.groovy:
##########
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.perf
+
+import org.openjdk.jmh.annotations.*
+import org.openjdk.jmh.infra.Blackhole
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Tests the performance of Groovy property access patterns including
+ * field read/write, getter/setter dispatch, dynamically-typed property
+ * access, map bracket and dot-property notation, and chained property
+ * resolution.
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(3)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+class PropertyAccessBench {
+ static final int ITERATIONS = 1_000_000
+
+ int instanceField = 42
+ String stringProperty = "hello"
+
+ // Explicit getter/setter for comparison
+ private int _backingField = 10
+ int getBackingField() { _backingField }
+ void setBackingField(int value) { _backingField = value }
+
+ /**
+ * Read/write a public field — the simplest property access path.
+ */
+ @Benchmark
+ void fieldReadWrite(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ instanceField = i
+ sum += instanceField
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Read/write through explicit getter/setter methods —
+ * tests the overhead of Groovy's property-to-getter/setter dispatch.
+ */
+ @Benchmark
+ void getterSetterAccess(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ backingField = i
+ sum += backingField
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Property access on a dynamically typed variable —
+ * tests the cost when the compiler cannot statically resolve the property.
+ */
+ @Benchmark
+ void dynamicTypedPropertyAccess(Blackhole bh) {
+ def obj = this
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ obj.instanceField = i
+ sum += obj.instanceField
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Map-style property access using bracket notation —
+ * tests Groovy's map-like property access on a POGO.
+ */
+ @Benchmark
+ void mapStyleAccess(Blackhole bh) {
+ Map<String, Integer> map = [a: 1, b: 2, c: 3]
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ map['a'] = i
+ sum += map['a']
+ }
Review Comment:
Fixed — updated the javadoc to correctly describe this as Map bracket
notation access.
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/PropertyAccessBench.groovy:
##########
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.perf
+
+import org.openjdk.jmh.annotations.*
+import org.openjdk.jmh.infra.Blackhole
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Tests the performance of Groovy property access patterns including
+ * field read/write, getter/setter dispatch, dynamically-typed property
+ * access, map bracket and dot-property notation, and chained property
+ * resolution.
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(3)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+class PropertyAccessBench {
+ static final int ITERATIONS = 1_000_000
+
+ int instanceField = 42
+ String stringProperty = "hello"
+
+ // Explicit getter/setter for comparison
+ private int _backingField = 10
+ int getBackingField() { _backingField }
+ void setBackingField(int value) { _backingField = value }
+
+ /**
+ * Read/write a public field — the simplest property access path.
+ */
+ @Benchmark
+ void fieldReadWrite(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ instanceField = i
+ sum += instanceField
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Read/write through explicit getter/setter methods —
+ * tests the overhead of Groovy's property-to-getter/setter dispatch.
+ */
+ @Benchmark
+ void getterSetterAccess(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ backingField = i
+ sum += backingField
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Property access on a dynamically typed variable —
+ * tests the cost when the compiler cannot statically resolve the property.
+ */
+ @Benchmark
+ void dynamicTypedPropertyAccess(Blackhole bh) {
+ def obj = this
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ obj.instanceField = i
+ sum += obj.instanceField
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Map-style property access using bracket notation —
+ * tests Groovy's map-like property access on a POGO.
+ */
+ @Benchmark
+ void mapStyleAccess(Blackhole bh) {
+ Map<String, Integer> map = [a: 1, b: 2, c: 3]
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ map['a'] = i
+ sum += map['a']
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Dot-property access on a Map — Groovy allows map.key syntax.
+ */
+ @Benchmark
+ void mapDotPropertyAccess(Blackhole bh) {
+ Map<String, Integer> map = [a: 1, b: 2, c: 3]
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ map.a = i
+ sum += map.a
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Chained property access — tests multiple property resolutions
+ * in a single expression.
+ */
+ @Benchmark
+ void chainedPropertyAccess(Blackhole bh) {
+ List<String> list = ["hello", "world"]
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ sum += list.first().length()
Review Comment:
Fixed — replaced the method-chaining implementation with actual nested map
property access: `root.level1.level2.value`.
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/GroovyIdiomBench.groovy:
##########
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.perf
+
+import org.openjdk.jmh.annotations.*
+import org.openjdk.jmh.infra.Blackhole
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Tests performance of Groovy-specific language idioms: safe navigation
+ * (?.), spread-dot (*.), elvis (?:), with/tap scoping, range creation
+ * and iteration, and 'as' type coercion.
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(3)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+class GroovyIdiomBench {
+ static final int ITERATIONS = 1_000_000
+
+ // Helper class for safe-nav / spread-dot / with tests
+ static class Person {
+ String name
+ Address address
+ }
+
+ static class Address {
+ String city
+ String zip
+ }
+
+ // Pre-built test data
+ Person personWithAddress
+ Person personNullAddress
+ List<Person> people
+
+ @Setup(Level.Trial)
+ void setup() {
+ personWithAddress = new Person(name: "Alice", address: new
Address(city: "Springfield", zip: "62704"))
+ personNullAddress = new Person(name: "Bob", address: null)
+ people = (1..100).collect { new Person(name: "Person$it", address: new
Address(city: "City$it", zip: "${10000 + it}")) }
+ }
+
+ // ===== SAFE NAVIGATION (?.) =====
+
+ /**
+ * Safe navigation on non-null chain — obj?.prop?.prop.
+ */
+ @Benchmark
+ void safeNavNonNull(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ sum += personWithAddress?.address?.city?.length() ?: 0
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Safe navigation hitting null — tests the short-circuit path.
+ */
+ @Benchmark
+ void safeNavNull(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ sum += personNullAddress?.address?.city?.length() ?: 0
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Safe navigation vs normal access — baseline for comparison.
+ */
+ @Benchmark
+ void normalNavBaseline(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ sum += personWithAddress.address.city.length()
+ }
+ bh.consume(sum)
+ }
+
+ // ===== SPREAD-DOT (*.) =====
+
+ /**
+ * Spread-dot operator — list*.property collects a property from all
elements.
+ */
+ @Benchmark
+ void spreadDotProperty(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS / 100; i++) {
+ bh.consume(people*.name)
+ }
+ }
+
+ /**
+ * Spread-dot with method call — list*.method().
+ */
+ @Benchmark
+ void spreadDotMethod(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS / 100; i++) {
+ bh.consume(people*.getName())
+ }
+ }
+
+ /**
+ * Spread-dot vs collect — baseline comparison.
+ */
+ @Benchmark
+ void collectBaseline(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS / 100; i++) {
+ bh.consume(people.collect { it.name })
+ }
+ }
+
+ // ===== ELVIS (?:) =====
+
+ /**
+ * Elvis operator with non-null value — takes the left side.
+ */
+ @Benchmark
+ void elvisNonNull(Blackhole bh) {
+ String value = "hello"
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ sum += (value ?: "default").length()
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Elvis operator with null value — takes the right side.
+ */
+ @Benchmark
+ void elvisNull(Blackhole bh) {
+ String value = null
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ sum += (value ?: "default").length()
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * Elvis with empty string (Groovy truth: empty string is falsy).
+ */
+ @Benchmark
+ void elvisEmptyString(Blackhole bh) {
+ String value = ""
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ sum += (value ?: "default").length()
+ }
+ bh.consume(sum)
+ }
+
+ // ===== WITH / TAP =====
+
+ /**
+ * with {} — executes closure with object as delegate, returns closure
result.
+ */
+ @Benchmark
+ void withScope(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ sum += personWithAddress.with {
+ name.length() + address.city.length()
+ }
+ }
+ bh.consume(sum)
+ }
+
+ /**
+ * tap {} — executes closure with object as delegate, returns the object.
+ */
+ @Benchmark
+ void tapScope(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ bh.consume(new Person().tap {
+ name = "Test"
+ address = new Address(city: "City", zip: "12345")
+ })
+ }
Review Comment:
Intentional — `tap` is specifically designed for object initialization, so
allocation is inherent to what's being benchmarked. A `tap` without object
construction wouldn't represent real-world usage.
> Groovy 4 runtime performance on average 2.4x slower than Groovy 3
> -----------------------------------------------------------------
>
> Key: GROOVY-10307
> URL: https://issues.apache.org/jira/browse/GROOVY-10307
> Project: Groovy
> Issue Type: Bug
> Components: bytecode, performance
> Affects Versions: 4.0.0-beta-1, 3.0.9
> Environment: OpenJDK Runtime Environment AdoptOpenJDK-11.0.11+9
> (build 11.0.11+9)
> OpenJDK 64-Bit Server VM AdoptOpenJDK-11.0.11+9 (build 11.0.11+9, mixed mode)
> WIN10 (tests) / REL 8 (web application)
> IntelliJ 2021.2
> Reporter: mgroovy
> Priority: Major
> Attachments: groovy_3_0_9_gc.png, groovy_3_0_9_loop2.png,
> groovy_3_0_9_loop4.png, groovy_3_0_9_mem.png, groovy_4_0_0_b1_loop2.png,
> groovy_4_0_0_b1_loop4.png, groovy_4_0_0_b1_loop4_gc.png,
> groovy_4_0_0_b1_loop4_mem.png,
> groovysql_performance_groovy4_2_xx_yy_zzzz.groovy, loops.groovy,
> profile3.txt, profile4-loops.txt, profile4.txt, profile4d.txt
>
>
> Groovy 4.0.0-beta-1 runtime performance in our framework is on average 2 to 3
> times slower compared to using Groovy 3.0.9 (regular i.e. non-INDY)
> * Our complete framework and application code is completely written in
> Groovy, spread over multiple IntelliJ modules
> ** mixed @CompileDynamic/@TypeChecked and @CompileStatic
> ** No Java classes left in project, i.e. no cross compilation occurs
> * We build using IntelliJ 2021.2 Groovy build process, then run / deploy the
> compiled class files
> ** We do _not_ use a Groovy based DSL, nor do we execute Groovy scripts
> during execution
> * Performance degradation when using Groovy 4.0.0-beta-1 instead of Groovy
> 3.0.9 (non-INDY):
> ** The performance of the largest of our web applications has dropped 3x
> (startup) / 2x (table refresh) respectively
> *** Stack: Tomcat/Vaadin/Ebean plus framework generated SQL
> ** Our test suite runs about 2.4 times as long as before (120 min when using
> G4, compared to about 50 min with G3)
> *** JUnit 5
> *** test suite also contains no scripts / dynamic code execution
> *** Individual test performance varies: A small number of tests runs faster,
> but the majority is slower, with some extreme cases taking nearly 10x as long
> to finish
> * Using Groovy 3.0.9 INDY displays nearly identical performance degradation,
> so it seems that the use of invoke dynamic is somehow at fault
--
This message was sent by Atlassian Jira
(v8.20.10#820010)