[ 
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)

Reply via email to