Skip to content

Commit b2e3eed

Browse files
authored
perf: faster cloning (#1650)
- Uses method/field handles instead of reflection. - Special handling for EnumMap and EnumSet. - Reduction of map access. - Better sizing of internal collections for repeated operations. Overall, these changes bring ~30 % throughput improvement in a cloning benchmark.
1 parent 23ff23d commit b2e3eed

File tree

9 files changed

+418
-216
lines changed

9 files changed

+418
-216
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package ai.timefold.solver.core.impl.domain.common.accessor;
2+
3+
import java.lang.invoke.MethodHandle;
4+
import java.lang.invoke.MethodHandles;
5+
import java.lang.reflect.Field;
6+
7+
public record FieldHandle(Field field, MethodHandle getter, MethodHandle setter) {
8+
9+
public static FieldHandle of(Field field) {
10+
try {
11+
field.setAccessible(true);
12+
var getter = MethodHandles.lookup().unreflectGetter(field);
13+
var setter = MethodHandles.lookup().unreflectSetter(field);
14+
return new FieldHandle(field, getter, setter);
15+
} catch (IllegalAccessException e) {
16+
throw new IllegalStateException(
17+
"The field (%s) cannot be accessed to create a planning clone."
18+
.formatted(field),
19+
e);
20+
}
21+
}
22+
23+
public Object get(Object bean) {
24+
try { // Don't null-check the bean; if null, it fails anyway.
25+
return getter.invoke(bean);
26+
} catch (Throwable e) {
27+
var beanClass = bean == null ? "null" : bean.getClass();
28+
throw new IllegalStateException(
29+
"Cannot get the field (%s) on bean of class (%s)."
30+
.formatted(field.getName(), beanClass),
31+
e);
32+
}
33+
}
34+
35+
public void set(Object bean, Object value) {
36+
try { // Don't null-check the bean; if null, it fails anyway.
37+
setter.invoke(bean, value);
38+
} catch (Throwable e) {
39+
var beanClass = bean == null ? "null" : bean.getClass();
40+
throw new IllegalStateException(
41+
"Cannot set the field (%s) on bean of class (%s)."
42+
.formatted(field.getName(), beanClass),
43+
e);
44+
}
45+
}
46+
47+
}

core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionFieldMemberAccessor.java

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,40 @@
99
*/
1010
public final class ReflectionFieldMemberAccessor extends AbstractMemberAccessor {
1111

12-
private final Field field;
12+
private final FieldHandle fieldHandle;
1313

1414
public ReflectionFieldMemberAccessor(Field field) {
15-
this.field = field;
16-
// Performance hack by avoiding security checks
17-
field.setAccessible(true);
15+
this.fieldHandle = FieldHandle.of(field); // Use MethodHandles to access the field.
1816
}
1917

2018
@Override
2119
public Class<?> getDeclaringClass() {
22-
return field.getDeclaringClass();
20+
return fieldHandle.field().getDeclaringClass();
2321
}
2422

2523
@Override
2624
public String getName() {
27-
return field.getName();
25+
return fieldHandle.field().getName();
2826
}
2927

3028
@Override
3129
public Class<?> getType() {
32-
return field.getType();
30+
return fieldHandle.field().getType();
3331
}
3432

3533
@Override
3634
public Type getGenericType() {
37-
return field.getGenericType();
35+
return fieldHandle.field().getGenericType();
3836
}
3937

4038
@Override
4139
public Object executeGetter(Object bean) {
40+
var field = fieldHandle.field();
4241
if (bean == null) {
4342
throw new IllegalArgumentException("Requested field (%s) on a null bean."
4443
.formatted(field));
4544
}
46-
try {
47-
return field.get(bean);
48-
} catch (IllegalAccessException e) {
49-
throw new IllegalStateException("""
50-
Cannot get the field (%s) on bean of class (%s).
51-
%s"""
52-
.formatted(field.getName(), bean.getClass(), MemberAccessorFactory.CLASSLOADER_NUDGE_MESSAGE),
53-
e);
54-
}
45+
return fieldHandle.get(bean);
5546
}
5647

5748
@Override
@@ -61,19 +52,12 @@ public boolean supportSetter() {
6152

6253
@Override
6354
public void executeSetter(Object bean, Object value) {
55+
var field = fieldHandle.field();
6456
if (bean == null) {
6557
throw new IllegalArgumentException("Requested field (%s) on a null bean."
6658
.formatted(field));
6759
}
68-
try {
69-
field.set(bean, value);
70-
} catch (IllegalAccessException e) {
71-
throw new IllegalStateException("""
72-
Cannot set the field (%s) on bean of class (%s).
73-
%s"""
74-
.formatted(field.getName(), bean.getClass(), MemberAccessorFactory.CLASSLOADER_NUDGE_MESSAGE),
75-
e);
76-
}
60+
fieldHandle.set(bean, value);
7761
}
7862

7963
@Override
@@ -83,17 +67,17 @@ public String getSpeedNote() {
8367

8468
@Override
8569
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
86-
return field.getAnnotation(annotationClass);
70+
return fieldHandle.field().getAnnotation(annotationClass);
8771
}
8872

8973
@Override
9074
public <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
91-
return field.getDeclaredAnnotationsByType(annotationClass);
75+
return fieldHandle.field().getDeclaredAnnotationsByType(annotationClass);
9276
}
9377

9478
@Override
9579
public String toString() {
96-
return "field " + field;
80+
return "field " + fieldHandle.field();
9781
}
9882

9983
}

core/src/main/java/ai/timefold/solver/core/impl/domain/solution/cloner/DeepCloningFieldCloner.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.concurrent.atomic.AtomicInteger;
66
import java.util.concurrent.atomic.AtomicReference;
77

8+
import ai.timefold.solver.core.impl.domain.common.accessor.FieldHandle;
89
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
910

1011
/**
@@ -14,14 +15,14 @@ final class DeepCloningFieldCloner {
1415

1516
private final AtomicReference<Metadata> valueDeepCloneDecision = new AtomicReference<>();
1617
private final AtomicInteger fieldDeepCloneDecision = new AtomicInteger(-1);
17-
private final Field field;
18+
private final FieldHandle fieldHandle;
1819

1920
public DeepCloningFieldCloner(Field field) {
20-
this.field = Objects.requireNonNull(field);
21+
this.fieldHandle = FieldHandle.of(Objects.requireNonNull(field));
2122
}
2223

23-
public Field getField() {
24-
return field;
24+
public FieldHandle getFieldHandles() {
25+
return fieldHandle;
2526
}
2627

2728
/**
@@ -33,11 +34,11 @@ public Field getField() {
3334
* @param <C>
3435
*/
3536
public <C> Object clone(SolutionDescriptor<?> solutionDescriptor, C original, C clone) {
36-
Object originalValue = FieldCloningUtils.getObjectFieldValue(original, field);
37+
Object originalValue = FieldCloningUtils.getObjectFieldValue(original, fieldHandle);
3738
if (deepClone(solutionDescriptor, original.getClass(), originalValue)) { // Defer filling in the field.
3839
return originalValue;
3940
} else { // Shallow copy.
40-
FieldCloningUtils.setObjectFieldValue(clone, field, originalValue);
41+
FieldCloningUtils.setObjectFieldValue(clone, fieldHandle, originalValue);
4142
return null;
4243
}
4344
}
@@ -77,7 +78,8 @@ private boolean deepClone(SolutionDescriptor<?> solutionDescriptor, Class<?> fie
7778
* The fieldTypeClass is guaranteed to not change for the particular field.
7879
*/
7980
if (fieldDeepCloneDecision.get() < 0) {
80-
fieldDeepCloneDecision.set(DeepCloningUtils.isFieldDeepCloned(solutionDescriptor, field, fieldTypeClass) ? 1 : 0);
81+
fieldDeepCloneDecision.set(
82+
DeepCloningUtils.isFieldDeepCloned(solutionDescriptor, getFieldHandles().field(), fieldTypeClass) ? 1 : 0);
8183
}
8284
return fieldDeepCloneDecision.get() == 1;
8385
}

0 commit comments

Comments
 (0)