Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fast-path for no-args constructor in BeanUtils.instantiateClass #24104

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

Expand Down Expand Up @@ -182,11 +183,18 @@ public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws
try {
ReflectionUtils.makeAccessible(ctor);
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) {
if (ObjectUtils.isEmpty(args)) {
return KotlinDelegate.instantiateClass(ctor);
Copy link
Contributor

@sdeleuze sdeleuze Dec 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the fact that JetBrains/kotlin#2855 has been closed because in practice it brings no benefit, is that part still relevant from your POV and if yes, could you elaborate on why and provide a JMH benchmark to quantify the gains?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, here's the benchmark:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
open class MyBenchmark {
    private val noArgConstructor = TestClass1::class.java.getDeclaredConstructor()
    private val constructor = TestClass2::class.java.getDeclaredConstructor(Int::class.java, String::class.java)

    @Benchmark
    fun emptyConstructor(): Any {
        return BeanUtils.instantiateClass(noArgConstructor)
    }

    @Benchmark
    fun nonEmptyConstructor(): Any {
        return BeanUtils.instantiateClass(constructor, 1, "str")
    }

    class TestClass1()

    class TestClass2(int: Int, string: String)
}

Results:

before

Benchmark                                                         Mode  Cnt     Score    Error   Units
MyBenchmark.emptyConstructor                                      avgt  100    52,279 ?  1,089   ns/op
MyBenchmark.emptyConstructor:?gc.alloc.rate.norm                  avgt  100   140,815 ?  2,499    B/op
MyBenchmark.nonEmptyConstructor                                   avgt  100   272,787 ?  3,971   ns/op
MyBenchmark.nonEmptyConstructor:?gc.alloc.rate.norm               avgt  100   985,684 ?  4,364    B/op

after

Benchmark                                                         Mode  Cnt     Score    Error   Units
MyBenchmark.emptyConstructor                                      avgt  100    50,635 ?  0,870   ns/op
MyBenchmark.emptyConstructor:?gc.alloc.rate.norm                  avgt  100    91,853 ?  2,329    B/op
MyBenchmark.nonEmptyConstructor                                   avgt  100   265,335 ?  5,555   ns/op
MyBenchmark.nonEmptyConstructor:?gc.alloc.rate.norm               avgt  100   988,885 ?  3,272    B/op

}
return KotlinDelegate.instantiateClass(ctor, args);
}
else {
int constructorParamCount = ctor.getParameterCount();
Assert.isTrue(args.length <= constructorParamCount, "Can't specify more arguments than constructor parameters");
if (constructorParamCount == 0) {
return ctor.newInstance();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please provide a JMH benchmark of the gains?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'll try to. The immediate gain here is that we don't allocate a new array calling ctor.getParameterTypes() for zero-arg constructor case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still interested by concrete data points with JMH benchmarks before and after the proposed change, could you please provide that?

Copy link
Contributor Author

@stsypanov stsypanov Jul 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the benchmark:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
    private Constructor<TestClass1> noArgConstructor;
    private Constructor<TestClass2> constructor;

    @Setup
    public void setUp() throws NoSuchMethodException {
        noArgConstructor = TestClass1.class.getDeclaredConstructor();
        constructor = TestClass2.class.getDeclaredConstructor(int.class, String.class);
    }

    @Benchmark
    public Object emptyConstructor() {
        return BeanUtils.instantiateClass(noArgConstructor);
    }

    @Benchmark
    public Object nonEmptyConstructor() {
        return BeanUtils.instantiateClass(constructor, 1, "str");
    }

    static class TestClass1{
    };

    static class TestClass2 {
        private final int value1;
        private final String value2;

        TestClass2(int value1, String value2) {
            this.value1 = value1;
            this.value2 = value2;
        }
    }
}

And results:

baseline
Benchmark                                                         Mode  Cnt     Score     Error   Units
MyBenchmark.emptyConstructor                                      avgt   40     9,880 ±   0,630   ns/op
MyBenchmark.emptyConstructor:·gc.alloc.rate.norm                  avgt   40    32,003 ±   0,001    B/op
MyBenchmark.nonEmptyConstructor                                   avgt   40    11,937 ±   0,131   ns/op
MyBenchmark.nonEmptyConstructor:·gc.alloc.rate.norm               avgt   40    48,004 ±   0,001    B/op

patched

Benchmark                                                         Mode  Cnt     Score    Error   Units
MyBenchmark.emptyConstructor                                      avgt   40     5,968 ±  0,112   ns/op
MyBenchmark.emptyConstructor:·gc.alloc.rate.norm                  avgt   40    16,002 ±  0,001    B/op
MyBenchmark.nonEmptyConstructor                                   avgt   40    11,922 ±  0,255   ns/op
MyBenchmark.nonEmptyConstructor:·gc.alloc.rate.norm               avgt   40    48,004 ±  0,001    B/op

}
Class<?>[] parameterTypes = ctor.getParameterTypes();
Assert.isTrue(args.length <= parameterTypes.length, "Can't specify more arguments than constructor parameters");
Object[] argsWithDefaultValues = new Object[args.length];
for (int i = 0 ; i < args.length; i++) {
if (args[i] == null) {
Expand Down Expand Up @@ -788,6 +796,24 @@ public static <T> T instantiateClass(Constructor<T> ctor, Object... args)
return kotlinConstructor.callBy(argParameters);
}

/**
* Instantiate a Kotlin class using provided no-arg constructor.
* @param ctor the constructor of the Kotlin class to instantiate
*/
public static <T> T instantiateClass(Constructor<T> ctor)
throws IllegalAccessException, InvocationTargetException, InstantiationException {

KFunction<T> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(ctor);
if (kotlinConstructor == null) {
return ctor.newInstance();
}
List<KParameter> parameters = kotlinConstructor.getParameters();
Assert.isTrue(parameters.isEmpty(), "Default no-args constructor must have no params");
Map<KParameter, Object> argParameters = Collections.emptyMap();
return kotlinConstructor.callBy(argParameters);
}


}

}