[
https://issues.apache.org/jira/browse/GROOVY-10307?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18058773#comment-18058773
]
ASF GitHub Bot commented on GROOVY-10307:
-----------------------------------------
Copilot commented on code in PR #2381:
URL: https://github.com/apache/groovy/pull/2381#discussion_r2810120355
##########
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)
+ }
Review Comment:
In `benchmarkClosureMultiParams`, the closure variable is named `c` and one
of the closure parameters is also named `c` (`{ a, b, c -> ... }`). This
parameter name shadows the outer `c` variable and makes the code harder to
read/maintain. Rename the parameter (e.g., `c3`/`z`) or rename the closure
variable to avoid the collision.
##########
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:
`fieldReadWrite` is documented as “Read/write a public field”, but `int
instanceField = 42` in Groovy defines a property (with an underlying private
field + accessors). If you specifically want to benchmark direct field access,
consider using the direct-field operator (`this.@instanceField`) and/or adjust
the comment to reflect Groovy property/field semantics.
##########
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:
The Javadoc for `mapStyleAccess` says it “tests Groovy's map-like property
access on a POGO”, but the code is operating on an actual `Map` and using
bracket notation (`map['a']`). Either update the comment to describe Map
bracket access, or change the benchmark to the intended POGO-style access.
##########
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()) }
Review Comment:
`originalEachToString` allocates a new list (`[1]`) and a new closure on
every loop iteration. That allocation/GC work will dominate the measurement and
makes it difficult to interpret results as “closure dispatch + toString cost”.
If the goal is to measure dispatch, hoist the list and/or closure out of the
inner loop (e.g., create them once per benchmark invocation or in a `@Setup`).
```suggestion
List<Integer> list = [1]
Closure<?> c = { bh.consume(it.toString()) }
for (int i = 0; i < LOOP_COUNT; i++) {
list.each(c)
```
##########
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:
`eachIdentity` also creates a new list (`[1]`) and a new closure for every
loop iteration, so this benchmark will largely measure allocation/GC rather
than the steady-state `each`/closure invocation overhead. Consider reusing the
list/closure outside the loop (similar to the intent described in the comment
for isolating dispatch).
```suggestion
def list = [1]
def identityClosure = { bh.consume(it) }
for (int i = 0; i < LOOP_COUNT; i++) {
list.each(identityClosure)
```
##########
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)
Review Comment:
`integerMultiply` is described as measuring `Integer.multiply(Integer)`, but
the loop body overwrites `product` with `(i % 100) * (i % 50)` each iteration.
This mixes in `%` operations and doesn't use the prior `product`, so it's not
really measuring repeated multiplication cost in a tight loop. Consider
simplifying to a single multiply (and/or accumulating with `product *= ...`) so
the benchmark matches its intent.
```suggestion
product *= i
```
##########
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:
In `tapScope`, the benchmark creates a new `Person` (and `Address`) on every
iteration and then calls `tap { ... }`. The allocation work is likely to
dominate, making it hard to isolate the invokedynamic overhead of `tap` itself.
Consider reusing a preallocated `Person` from state (resetting fields inside
`tap`), or splitting allocation into a separate baseline benchmark for
comparison.
##########
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:
`chainedPropertyAccess` is described as “Chained property access”, but the
expression being benchmarked is `list.first().length()`, which is a chain of
method calls (`first()` and `length()`), not property resolution. Consider
switching to an actual chained property expression (or renaming/updating the
comment) so the benchmark matches its stated purpose.
```suggestion
Map<String, Object> root = [level1: [level2: [value: 0]]]
int sum = 0
for (int i = 0; i < ITERATIONS; i++) {
root.level1.level2.value = i
sum += root.level1.level2.value
```
> 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)