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. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
