This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 8024cfbaa89092e159b879639d552ff93b6c4767
Author: James Fredley <[email protected]>
AuthorDate: Sun Feb 15 18:35:19 2026 -0500

    GROOVY-10307: add JMH benchmarks for invokedynamic performance patterns
---
 .../org/apache/groovy/perf/ClosureBench.groovy     | 309 +++++++++++++++++++++
 .../org/apache/groovy/perf/GStringBench.groovy     | 109 ++++++++
 .../org/apache/groovy/perf/GroovyIdiomBench.groovy | 277 ++++++++++++++++++
 .../org/apache/groovy/perf/LoopsBench.groovy       | 104 +++++++
 .../groovy/perf/MethodInvocationBench.groovy       | 187 +++++++++++++
 .../org/apache/groovy/perf/OperatorBench.groovy    | 206 ++++++++++++++
 .../apache/groovy/perf/PropertyAccessBench.groovy  | 133 +++++++++
 7 files changed, 1325 insertions(+)

diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/ClosureBench.groovy
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/ClosureBench.groovy
new file mode 100644
index 0000000000..4e83c3ad57
--- /dev/null
+++ 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/ClosureBench.groovy
@@ -0,0 +1,309 @@
+/*
+ *  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 closure performance including creation, reuse, multi-parameter
+ * invocation, variable capture, delegation, nesting, method references,
+ * currying, composition, spread operator, trampoline recursion, and
+ * collection operations (each/collect/findAll/inject).
+ */
+@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 ClosureBench {
+    static final int ITERATIONS = 1_000_000
+
+    String instanceProperty = "instance"
+
+    /**
+     * Benchmark: Simple closure creation and invocation
+     */
+    @Benchmark
+    void benchmarkSimpleClosureCreation(Blackhole bh) {
+        for (int i = 0; i < ITERATIONS; i++) {
+            Closure c = { it * 2 }
+            bh.consume(c(i))
+        }
+    }
+
+    /**
+     * Benchmark: Reuse same closure (no creation overhead)
+     */
+    @Benchmark
+    void benchmarkClosureReuse(Blackhole bh) {
+        Closure c = { it * 2 }
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += c(i)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Closure with multiple parameters
+     */
+    @Benchmark
+    void benchmarkClosureMultiParams(Blackhole bh) {
+        Closure c = { a, b, c -> a + b + c }
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += c(i, i + 1, i + 2)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Closure accessing local variable (captured variable)
+     */
+    @Benchmark
+    void benchmarkClosureWithCapture(Blackhole bh) {
+        int captured = 100
+        Closure c = { it + captured }
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += c(i)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Closure modifying captured variable
+     */
+    @Benchmark
+    void benchmarkClosureModifyCapture(Blackhole bh) {
+        int counter = 0
+        Closure c = { counter++ }
+        for (int i = 0; i < ITERATIONS; i++) {
+            c()
+        }
+        bh.consume(counter)
+    }
+
+    /**
+     * Benchmark: Closure with owner delegation
+     */
+    @Benchmark
+    void benchmarkClosureDelegation(Blackhole bh) {
+        Closure c = { instanceProperty.length() }
+        c.delegate = this
+        c.resolveStrategy = Closure.DELEGATE_FIRST
+
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += c()
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Nested closures
+     */
+    @Benchmark
+    void benchmarkNestedClosures(Blackhole bh) {
+        Closure outer = { x ->
+            Closure inner = { y -> x + y }
+            inner(x)
+        }
+
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += outer(i)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Method reference as closure
+     */
+    @Benchmark
+    void benchmarkMethodReference(Blackhole bh) {
+        List<Integer> list = [1, 2, 3, 4, 5]
+        Closure sizeRef = list.&size
+
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += sizeRef()
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Curried closure
+     */
+    @Benchmark
+    void benchmarkCurriedClosure(Blackhole bh) {
+        Closure add = { a, b -> a + b }
+        Closure addFive = add.curry(5)
+
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += addFive(i)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Right curried closure
+     */
+    @Benchmark
+    void benchmarkRightCurriedClosure(Blackhole bh) {
+        Closure subtract = { a, b -> a - b }
+        Closure subtractFive = subtract.rcurry(5)
+
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += subtractFive(i)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Closure composition (rightShift >>)
+     */
+    @Benchmark
+    void benchmarkClosureComposition(Blackhole bh) {
+        Closure double_ = { it * 2 }
+        Closure addOne = { it + 1 }
+        Closure composed = double_ >> addOne
+
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += composed(i)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Closure as method parameter
+     */
+    @Benchmark
+    void benchmarkClosureAsParameter(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += applyOperation(i) { it * 2 }
+        }
+        bh.consume(sum)
+    }
+
+    static int applyOperation(int value, Closure<Integer> operation) {
+        operation(value)
+    }
+
+    /**
+     * Benchmark: Closure with spread operator
+     */
+    @Benchmark
+    void benchmarkClosureSpread(Blackhole bh) {
+        Closure sum3 = { a, b, c -> a + b + c }
+        List<Integer> args = [1, 2, 3]
+
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += sum3(*args)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Closure call vs doCall
+     */
+    @Benchmark
+    void benchmarkClosureCallMethod(Blackhole bh) {
+        Closure c = { it * 2 }
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += c.call(i)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Closure with trampoline (for recursion)
+     */
+    @Benchmark
+    void benchmarkClosureTrampoline(Blackhole bh) {
+        Closure factorial
+        factorial = { n, acc = 1G ->
+            n <= 1 ? acc : factorial.trampoline(n - 1, n * acc)
+        }.trampoline()
+
+        // Smaller iteration count due to computation cost
+        for (int i = 0; i < ITERATIONS / 100; i++) {
+            bh.consume(factorial(20))
+        }
+    }
+
+    /**
+     * Benchmark: each with closure (common pattern)
+     */
+    @Benchmark
+    void benchmarkEachWithClosure(Blackhole bh) {
+        List<Integer> list = (1..10).toList()
+        int sum = 0
+        for (int i = 0; i < ITERATIONS / 10; i++) {
+            list.each { sum += it }
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: collect with closure
+     */
+    @Benchmark
+    void benchmarkCollectWithClosure(Blackhole bh) {
+        List<Integer> list = (1..10).toList()
+        for (int i = 0; i < ITERATIONS / 10; i++) {
+            bh.consume(list.collect { it * 2 })
+        }
+    }
+
+    /**
+     * Benchmark: findAll with closure
+     */
+    @Benchmark
+    void benchmarkFindAllWithClosure(Blackhole bh) {
+        List<Integer> list = (1..10).toList()
+        for (int i = 0; i < ITERATIONS / 10; i++) {
+            bh.consume(list.findAll { it > 5 })
+        }
+    }
+
+    /**
+     * Benchmark: inject/reduce with closure
+     */
+    @Benchmark
+    void benchmarkInjectWithClosure(Blackhole bh) {
+        List<Integer> list = (1..10).toList()
+        int sum = 0
+        for (int i = 0; i < ITERATIONS / 10; i++) {
+            sum += list.inject(0) { acc, val -> acc + val }
+        }
+        bh.consume(sum)
+    }
+
+}
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/GStringBench.groovy
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/GStringBench.groovy
new file mode 100644
index 0000000000..ff601ee44f
--- /dev/null
+++ 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/GStringBench.groovy
@@ -0,0 +1,109 @@
+/*
+ *  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 GString creation, interpolation, and
+ * calling methods on GString results — including simple and multi-value
+ * interpolation, comparison against plain String concatenation, use as
+ * Map keys, and repeated toString() evaluation.
+ */
+@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 GStringBench {
+    static final int ITERATIONS = 1_000_000
+
+    /**
+     * Simple GString with one interpolated value and a method call on the 
result.
+     */
+    @Benchmark
+    void simpleInterpolation(Blackhole bh) {
+        String base = "Hello"
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += "${base}${i}".length()
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Multi-value GString with method call — tests cost of multiple
+     * interpolation expressions and a follow-on method dispatch.
+     */
+    @Benchmark
+    void multiValueInterpolation(Blackhole bh) {
+        String a = "A"
+        String b = "B"
+        for (int i = 0; i < ITERATIONS; i++) {
+            bh.consume("${a}-${i}-${b}".toUpperCase())
+        }
+    }
+
+    /**
+     * GString compared to plain String concatenation — baseline to
+     * isolate the GString-specific overhead.
+     */
+    @Benchmark
+    void stringConcatBaseline(Blackhole bh) {
+        String base = "Hello"
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += (base + i).length()
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * GString used as a Map key — triggers toString() and hashCode(),
+     * testing lazy evaluation and method dispatch on the resulting String.
+     */
+    @Benchmark
+    void gstringAsMapKey(Blackhole bh) {
+        Map<String, Integer> map = [:]
+        String prefix = "key"
+        for (int i = 0; i < ITERATIONS; i++) {
+            map["${prefix}${i % 100}"] = i
+        }
+        bh.consume(map)
+    }
+
+    /**
+     * Repeated toString() on the same GString — tests whether the
+     * GString caches its string representation.
+     */
+    @Benchmark
+    void repeatedToString(Blackhole bh) {
+        String name = "World"
+        GString gs = "Hello ${name}!"
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += gs.toString().length()
+        }
+        bh.consume(sum)
+    }
+}
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/GroovyIdiomBench.groovy
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/GroovyIdiomBench.groovy
new file mode 100644
index 0000000000..d934b95568
--- /dev/null
+++ 
b/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")
+            })
+        }
+    }
+
+    // ===== RANGE =====
+
+    /**
+     * Range creation — (1..N) creates an IntRange object.
+     */
+    @Benchmark
+    void rangeCreation(Blackhole bh) {
+        for (int i = 0; i < ITERATIONS; i++) {
+            bh.consume(1..100)
+        }
+    }
+
+    /**
+     * Range iteration with each — (1..N).each { }.
+     */
+    @Benchmark
+    void rangeIteration(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS / 100; i++) {
+            (1..100).each { sum += it }
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Range contains check — (val in range) uses Range.containsWithinBounds.
+     */
+    @Benchmark
+    void rangeContains(Blackhole bh) {
+        def range = 1..1000
+        int count = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            if ((i % 1500) in range) count++
+        }
+        bh.consume(count)
+    }
+
+    // ===== AS TYPE COERCION =====
+
+    /**
+     * 'as' coercion: list as Set.
+     */
+    @Benchmark
+    void asListToSet(Blackhole bh) {
+        List<Integer> list = [1, 2, 3, 4, 5, 1, 2, 3]
+        for (int i = 0; i < ITERATIONS; i++) {
+            bh.consume(list as Set)
+        }
+    }
+
+    /**
+     * 'as' coercion: object as String.
+     */
+    @Benchmark
+    void asToString(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += (i as String).length()
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * 'as' coercion: String to Integer.
+     */
+    @Benchmark
+    void asStringToInteger(Blackhole bh) {
+        String[] values = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += values[i % 10] as Integer
+        }
+        bh.consume(sum)
+    }
+}
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/LoopsBench.groovy
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/LoopsBench.groovy
new file mode 100644
index 0000000000..1a26324574
--- /dev/null
+++ 
b/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) }
+        }
+    }
+
+    /**
+     * Reused closure invoked in a loop via .call() — tests call site caching
+     * when the same closure is called repeatedly (no new closure allocation 
per iteration).
+     */
+    @Benchmark
+    void reusedClosureInLoop(Blackhole bh) {
+        Closure<?> c = { it.toString() }
+        for (int i = 0; i < LOOP_COUNT; i++) {
+            bh.consume(c.call(1))
+        }
+    }
+
+    /**
+     * Direct method call in a loop — baseline comparison against closure 
dispatch.
+     * Shows the overhead of closure invocation vs plain method invocation.
+     */
+    @Benchmark
+    void methodCallInLoop(Blackhole bh) {
+        for (int i = 0; i < LOOP_COUNT; i++) {
+            bh.consume(doSomething(1))
+        }
+    }
+
+    static String doSomething(Object o) {
+        o.toString()
+    }
+
+    /**
+     * Nested loops with closures — tests call site behavior when multiple
+     * closure call sites are active across nested iteration scopes.
+     */
+    @Benchmark
+    void nestedLoopsWithClosure(Blackhole bh) {
+        int count = (int) Math.sqrt(LOOP_COUNT)
+        for (int i = 0; i < count; i++) {
+            for (int j = 0; j < count; j++) {
+                [i, j].each { bh.consume(it.toString()) }
+            }
+        }
+    }
+}
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/MethodInvocationBench.groovy
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/MethodInvocationBench.groovy
new file mode 100644
index 0000000000..155094665f
--- /dev/null
+++ 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/MethodInvocationBench.groovy
@@ -0,0 +1,187 @@
+/*
+ *  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 dynamic method invocation and dispatch in Groovy.
+ * Covers instance and static method calls, parameter passing, overloaded
+ * method resolution, monomorphic vs polymorphic call sites, interface
+ * dispatch, and dynamically-typed dispatch.
+ *
+ * Property access is in {@link PropertyAccessBench}.
+ * GString operations are in {@link GStringBench}.
+ * Method references as closures 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 MethodInvocationBench {
+    static final int ITERATIONS = 1_000_000
+
+    // Simple fields for method tests
+    int instanceField = 42
+    static int staticField = 100
+
+    // Simple instance method
+    int simpleMethod() {
+        instanceField
+    }
+
+    // Method with parameters
+    int methodWithParams(int a, int b) {
+        a + b
+    }
+
+    // Method with object parameter
+    String methodWithObject(Object obj) {
+        obj.toString()
+    }
+
+    // Overloaded methods to test dispatch
+    String overloaded(String s) { "String: $s" }
+    String overloaded(Integer i) { "Integer: $i" }
+    String overloaded(Object o) { "Object: $o" }
+
+    // Static methods
+    static int staticMethod() {
+        staticField
+    }
+
+    static int staticMethodWithParams(int a, int b) {
+        a + b
+    }
+
+    /**
+     * Benchmark: Simple instance method calls
+     */
+    @Benchmark
+    void benchmarkSimpleMethodCalls(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += simpleMethod()
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Method calls with parameters
+     */
+    @Benchmark
+    void benchmarkMethodWithParams(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += methodWithParams(i, 1)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Method calls with object parameter
+     */
+    @Benchmark
+    void benchmarkMethodWithObject(Blackhole bh) {
+        for (int i = 0; i < ITERATIONS; i++) {
+            bh.consume(methodWithObject(i))
+        }
+    }
+
+    /**
+     * Benchmark: Static method calls
+     */
+    @Benchmark
+    void benchmarkStaticMethodCalls(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += staticMethod()
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Static method calls with parameters
+     */
+    @Benchmark
+    void benchmarkStaticMethodWithParams(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += staticMethodWithParams(i, 1)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Monomorphic call site (same type every time)
+     * This should be fast with proper indy optimization
+     */
+    @Benchmark
+    void benchmarkMonomorphicCallSite(Blackhole bh) {
+        String s = "test"
+        for (int i = 0; i < ITERATIONS; i++) {
+            bh.consume(overloaded(s))
+        }
+    }
+
+    /**
+     * Benchmark: Polymorphic call site (different types)
+     * This tests the call site cache effectiveness
+     */
+    @Benchmark
+    void benchmarkPolymorphicCallSite(Blackhole bh) {
+        Object[] args = ["string", 42, new Object(), "another", 100, [1, 2, 3]]
+
+        for (int i = 0; i < ITERATIONS; i++) {
+            bh.consume(overloaded(args[i % args.length]))
+        }
+    }
+
+    /**
+     * Benchmark: Method calls through interface
+     */
+    @Benchmark
+    void benchmarkInterfaceMethodCalls(Blackhole bh) {
+        List<Integer> list = [1, 2, 3, 4, 5]
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += list.size()
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Benchmark: Method calls on dynamically typed variable
+     */
+    @Benchmark
+    void benchmarkDynamicTypedCalls(Blackhole bh) {
+        def instance = this
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += instance.simpleMethod()
+        }
+        bh.consume(sum)
+    }
+
+}
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/OperatorBench.groovy
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/OperatorBench.groovy
new file mode 100644
index 0000000000..1484aa9b46
--- /dev/null
+++ 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/OperatorBench.groovy
@@ -0,0 +1,206 @@
+/*
+ *  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 operator overloading. In Groovy every
+ * operator (+, -, *, /, [], <<, ==, <=>) compiles to a method call
+ * (plus, minus, multiply, div, getAt, leftShift, equals, compareTo)
+ * dispatched through invokedynamic.
+ */
+@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 OperatorBench {
+    static final int ITERATIONS = 1_000_000
+
+    /**
+     * Integer addition — dispatches to Integer.plus(Integer).
+     */
+    @Benchmark
+    void integerPlus(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum = sum + i
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Integer multiplication — dispatches to Integer.multiply(Integer).
+     */
+    @Benchmark
+    void integerMultiply(Blackhole bh) {
+        int product = 1
+        for (int i = 1; i < ITERATIONS; i++) {
+            product = (i % 100) * (i % 50)
+        }
+        bh.consume(product)
+    }
+
+    /**
+     * BigDecimal arithmetic — common in financial/Grails apps,
+     * all operations go through operator method dispatch.
+     */
+    @Benchmark
+    void bigDecimalArithmetic(Blackhole bh) {
+        BigDecimal sum = 0.0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum = sum + 1.5
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * String multiply (repeat) — "abc" * 3 dispatches to 
String.multiply(Integer).
+     */
+    @Benchmark
+    void stringMultiply(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += ("x" * 5).length()
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * List subscript read — list[i] dispatches to List.getAt(int).
+     */
+    @Benchmark
+    void listGetAt(Blackhole bh) {
+        List<Integer> list = (0..99).toList()
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += list[i % 100]
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * List subscript write — list[i] = val dispatches to List.putAt(int, 
Object).
+     */
+    @Benchmark
+    void listPutAt(Blackhole bh) {
+        List<Integer> list = (0..99).toList()
+        for (int i = 0; i < ITERATIONS; i++) {
+            list[i % 100] = i
+        }
+        bh.consume(list)
+    }
+
+    /**
+     * Map subscript read/write — map[key] dispatches to getAt/putAt.
+     */
+    @Benchmark
+    void mapGetAtPutAt(Blackhole bh) {
+        Map<String, Integer> map = [a: 1, b: 2, c: 3, d: 4, e: 5]
+        String[] keys = ['a', 'b', 'c', 'd', 'e']
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            map[keys[i % 5]] = i
+            sum += map[keys[i % 5]]
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Left shift operator — list << item dispatches to List.leftShift(Object).
+     */
+    @Benchmark
+    void listLeftShift(Blackhole bh) {
+        List<Integer> list = []
+        for (int i = 0; i < ITERATIONS; i++) {
+            if (i % 1000 == 0) list = []
+            list << i
+        }
+        bh.consume(list)
+    }
+
+    /**
+     * Equals operator — == dispatches to Object.equals(Object) in Groovy
+     * (not reference equality like Java).
+     */
+    @Benchmark
+    void equalsOperator(Blackhole bh) {
+        String a = "hello"
+        String b = "hello"
+        int count = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            if (a == b) count++
+        }
+        bh.consume(count)
+    }
+
+    /**
+     * Spaceship operator — <=> dispatches to Comparable.compareTo().
+     */
+    @Benchmark
+    void spaceshipOperator(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += (i <=> (i + 1))
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * Comparison operators — <, >, <=, >= dispatch through compareTo().
+     */
+    @Benchmark
+    void comparisonOperators(Blackhole bh) {
+        int count = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            if (i > 0 && i < ITERATIONS && i >= 0 && i <= ITERATIONS) count++
+        }
+        bh.consume(count)
+    }
+
+    /**
+     * Unary minus — dispatches to Number.unaryMinus().
+     */
+    @Benchmark
+    void unaryMinus(Blackhole bh) {
+        int sum = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            sum += (-i)
+        }
+        bh.consume(sum)
+    }
+
+    /**
+     * In operator — (item in collection) dispatches to 
Collection.isCase(Object).
+     */
+    @Benchmark
+    void inOperator(Blackhole bh) {
+        List<Integer> list = (0..99).toList()
+        int count = 0
+        for (int i = 0; i < ITERATIONS; i++) {
+            if ((i % 100) in list) count++
+        }
+        bh.consume(count)
+    }
+}
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/PropertyAccessBench.groovy
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/PropertyAccessBench.groovy
new file mode 100644
index 0000000000..0b45b44384
--- /dev/null
+++ 
b/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()
+        }
+        bh.consume(sum)
+    }
+}


Reply via email to