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) + } +}
