This blog came about because I was looking into performance issue and profiling showed that a method that was copying objects was very slow. (NB: The fact that this method was called millions of times was an actual bug not the slowness of the copy routine.)
The method to copy object was performing field by field copy using class.getDeclaredFeilds()
method to obtain all fields of the class and then doing copy recursively for all super classes. Code below shows entire thing:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void copyFieldByField(Object src, Object dest) { | |
copyFields(src, dest, src.getClass()); | |
} | |
private static void copyFields(Object src, Object dest, Class<?> klass) { | |
Field[] fields = klass.getDeclaredFields(); | |
for (Field f : fields) { | |
f.setAccessible(true); | |
copyFieldValue(src, dest, f); | |
} | |
klass = klass.getSuperclass(); | |
if (klass != null) { | |
copyFields(src, dest, klass); | |
} | |
} | |
private static void copyFieldValue(Object src, Object dest, Field f) { | |
try { | |
Object value = f.get(src); | |
f.set(dest, value); | |
} catch (ReflectiveOperationException e) { | |
throw new RuntimeException(e); | |
} | |
} |
Profiling showed that this method spend 70% of it’s time in Field#copy()
method. As it turns out that Class#getDeclaredFields()
returns a copy of the Field[]
array on every call, ouch! BTW, the JavaDoc of Class#getDeclaredFields()
does not mention copy behavior at all.
List of approaches
Looking at the profiling results I started wondering what would be a better way to create a shallow copy of the object. Hence I needed to test performance of different approaches. For the simplicity I decided to look only on what is possibly with JDK 7 only (no fancy libraries) and came up with the following list:
- Clone object – implement
Cloneable
interface and publishclone()
method - Copy with copy constructor – copy fields from the source object directly in the constructor
- Copy via reflection (field by field)
getDeclaredFieds()
as in original codegetDeclaredFields()
cached, i.e. call it once and remember list based on the Class
-
Serialization
- Default serialization – just implementing
Serializable
- Custom serialization – implement Serialization and implement
readObject()
andwriteObject()
methods for reading and writing fields directly - Implementing
Externalizable
interface
- Default serialization – just implementing
- MethodHandles
- Use
MethodHandle#invoke(Object... args)
method - Use
MethodHandle#invokeWithArguments(Object... arguments)
method - Use
MethodHandle#invokeExact(Object... args)
method
- Use
Notes on implementation
Entire project with source code and test results is available on github (copy-object-benchmark.git).
Benchmarks are written using JMH framework from OpenJDK. For each class tested there is a method in the benchmark class that simply invokes copy()
method on the constant object representing the class. For example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@GenerateMicroBenchmark | |
public Object copyFieldByFieldGetFieldsEveryTime() { | |
return FieldsCopy.INSTANCE.copy(); | |
} | |
@GenerateMicroBenchmark | |
public Object copyFieldByFieldUseCacheFields() { | |
return CachedFieldsCopy.INSTANCE.copy(); | |
} | |
@GenerateMicroBenchmark | |
public Object copyByClone() { | |
return CloneCopy.INSTANCE.copy(); | |
} |
Every approach from the list above was benchmarked for 2 cases:
- Primitive fields
- Object fields
Because most of the approaches would incur significant overhead via boxing/unboxing.
For every kind of copy method there is a class that extends common super class (i.e. BaseClass
) and implements copy()
method. Since there are 2 use cases tests had to be duplicated in 2 different packages.
One last thing that is worth mentioning before I show the results is the handling of MethodHandle#invokeExact()
case. This method is very special when it gets to invocation. Here is an example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.invoke.MethodHandle; | |
import java.lang.invoke.MethodHandles; | |
class Base { | |
int x = 13; | |
} | |
class Derived extends Base { | |
public long m; | |
} | |
public class TestMH { | |
public static void main(String[] args) throws Throwable { | |
MethodHandle mh = MethodHandles.lookup().findGetter(Base.class, "x", int.class); | |
Derived obj = new Derived(); | |
// mh.invokeExact(obj); => throws WrongMethodTypeException:expected(Base)int but found (Derived)void | |
// mh.invokeExact((Base) obj); => throws WrongMethodTypeException:expected(Base)int but found (Base)void | |
int value = (int) mh.invokeExact((Base) obj); | |
} | |
} |
As you can see in order to call MethodHandle#invokeExact()
it is necessary to match exactly arguments and return type of the method, otherwise such invocation will fail at runtime.
However it is not possible to know upfront all possible classes and return types if you want to write generic copy method. Therefore we need to adjust MethodHandle
so it can be invoked in a generic manner. This is done by adjusting MethodType
signatures of the MethodHandle
as shown below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static MethodHandle prepareGetter(MethodHandle mh) { | |
return mh.asType(mh.type().changeParameterType(0, Object.class) | |
.changeReturnType(Object.class)); | |
} | |
private static MethodHandle prepareSetter(MethodHandle mh) { | |
return mh.asType(mh.type().changeParameterType(0, Object.class) | |
.changeParameterType(1, Object.class)); | |
} |
Basically this erases original information about declaration class and return types and instead uses Object
for both thus allowing calling MethodHandle#invokeExact()
providing instance of any class and expecting result as Object
.
Results
I was running the benchmarks on my MacBook Pro laptop: OS X 10.9, 2.7 GHz Intel Core i7, 16 GB of RAM. Tests were performed on the following JDKs:
- 1.7.0_25 (
1.7.0_25-b15
) - 1.7.0_45 (
1.7.0_45-b18
) - JDK 8 build 112 (
1.8.0-ea-b112
)
Results are reported as throughput (operations/ms) with biggest numbers being the best. All charts are in logarithmic scale.
Results:
- JDK 1.7.0_25:
Raw results are available here results-1.7.0_25.txt.
So in this version of JDK the best 3 methods were:
- Clone
- Copy constructor
- Reflection
Also
MethodHandles
are very slow comparing to other approaches. - JDK 1.7.0_45:
Raw results results-1.7.0_45.txt.
In the 1.7.0_45 release we see that
MethodHandle#invokeExact()
got much faster (~5x times) to the point that it made it to top 3 copy methods (for primitives case). AlsoMethodHandle#invokeWithArguments()
got slower. But otherwise the rest remains the same. - JDK 8 build 112:
Raw results results-1.8.0-ea-b112.txt.
JDK 8 build 112 brings significant performance improvements over JDK 7 results in two areas:
- Reflection
- MethodHandles
Most dramatic change was in the
MethodHandler#invoke()
case which now is more than 120x times faster! The changes were done originally proposed by John Rose on a JDK mailing list and implemented as part of the JDK-8024761: JSR 292 improve performance of generic invocation issue.Along the way
MethodHandle#invokeWithArguments()
got faster as well.
Conclusions
- New JDK releases bring performance improvements
- New JDK versions bring new functionality that can be used in place of old approaches (e.g.
MethodHandles
) - JVM is extremely good at optimizing existing code, i.e. reflection is still faster than invoke dynamic (at least for Object case)
- Read JavaDoc but step through the code in the debugger and not just through your own code but also JDK code
- Write performance tests upfront to know whether selected approach meets performance requirements
- Run performance tests against new JDK versions to avoid/detect performance regressions
Updates
I’ve published second blog post Clone vs copy constructor – a closer look in which I analyze performance difference of clone and copy constructor approaches.