Performance comparison between shallow and deep copies

Keywords: Java Spring Spring Boot Back-end architecture

Preface

In daily development, object conversion is frequently used, which can be roughly divided into shallow copy and deep copy. If subdivided, deep copy can be subdivided into several types. This paper makes a series of tests based on several mainstream deep copy methods, and explores the principles and scenarios.

Readers are welcome to discuss. If there are any errors or omissions, please provide guidance. As new registered users of public numbers do not have message function, students who want to discuss can reply directly on public numbers.
 

1. Shallow and deep copies

Shallow copy: Value transfer for basic data types and reference transfer copy for reference data types. This is a shallow copy

Deep copy: Value transfer to the basic data type, reference data type, create a new object, and copy its contents. This is a deep copy.

There are several main ways to do this:

Shallow copy:

1) Using Java's clone method, requires cloneable implementation and overrides the clone method. The principle is to call the internalClone() native method, which is more intrusive but performs well.

2) Apache's BeanUtil approach, generally considered poor performance, has been discarded and is no longer being tested here.

3) Spring's BeanUtil mode, through reflection.

4) Hutool's BeanUtil style, which is also a shallow copy and has good performance.

5) MapStruct, which actually generates get and set methods with excellent performance, requires deploy ment for each update.

6) Apache's BeanCopier approach, similar to Hutool's BeanCopier, uses CGLIB dynamic proxy with better reflective performance. Select Apache's BeanCopier here.

Shallow copy mainly compares Java clone, Spring BeanUtil, Hutool BeanUtil, MapStruct, and BeanCopier.

Deep copy:

1) Serialization and deserialization with various json tools have very poor performance and are no longer compared here.

2) Kryo method

3) Orika mode

4) Java IO serialization and deserialization

5) Protobuf mode, there are still problems with complex object serialization and deserialization, Hessian mode and Thrift mode also have a variety of problems, there is no comparison here.

(6) Dozer mode

Deep copy mainly compares Kryo, Orika, Java IO, Dozer.

2. Specific scenarios

Shallow copy: Conversion between BO, VO, DTO is often required in the hierarchical structure of a project, which requires a lot of performance because of the large number of scenarios and fields.

Deep copy: In a previous inventory purchase item,

1) A logic needs to update two tables of the database (Table A, Table B, Table A is the specific table of available numbers, Table B is the table of available segments) and record the records of each table operation. The foreground needs to query the history of historical operations.

2) Some complex objects need to be modified for the first time(Because you need to map Table B, there are a large number of fields on both tables, about 30 of which can be mapped, but some complex objects need to be modified, such as the state of the two tables represent different meanings, such as field A of the table is only a subset of table B, such as field C of table A is 1, and record in Table 2 is separated by commas).

3) Logging will not occur until both operations are completed.

The log data recorded at this time is after step 1 and before step 2, but the actual execution of the record takes place only in step 2, so a deep copy scheme is needed to resolve it.

3. Introduction to Test Preparation and Configuration

(1) Test configuration:

System: mac

CPU:i7 hexacore 2.6GHZ

Memory: 16G 2667MHZ DDR4

        JDK:    1.8.0_241

2) Test preparation

The Orika code is as follows, and the other tests are 1,1000, 10w:

public class DeepCopyTest {    /**     * Number of loops     */    private final int LOOP = 1;​    @Test    public void test(){        DeepCopyEntity demo = getInit();        StopWatch stopWatch = new StopWatch("test");        stopWatch.start("Orika");        for (int i = 0; i < LOOP; i++) {            final DeepCopyEntity deepCopyEntity =             MapperUtils.INSTANCE.map(DeepCopyEntity.class,             demo);        }        stopWatch.stop();        System.out.println(stopWatch.prettyPrint());    }  }
public enum MapperUtils {    /**     * Default field instance     */    private static final MapperFacade MAPPER_FACADE =     MAPPER_FACTORY.getMapperFacade();    /**     * Mapping Entity (Default Field)* * @param toClass Mapping Class Object * @param data data (Object)* @return Mapping Class Object     */    public <E, T> E map(Class<E> toClass, T data) {        return MAPPER_FACADE.map(data, toClass);    }}
@Setter@Getter@Accessors(chain = true)@ToString(callSuper = true)public class DeepCopyEntity implements Serializable{​    /**     * Serialized Identity     */    private static final long serialVersionUID =     6172279441386879379L;​    private String id;​    private String field1;        ...        private Blind blind;}

3) Reference Version

<dependency>    <groupId>com.esotericsoftware</groupId>    <artifactId>kryo</artifactId>    <version>5.2.0</version>    </dependency><dependency>    <groupId>ma.glasnost.orika</groupId>    <artifactId>orika-core</artifactId>    <version>1.5.4</version>    </dependency><dependency>    <groupId>org.mapstruct</groupId>    <artifactId>mapstruct-jdk8</artifactId>    <version>1.2.0.Final</version>    </dependency><dependency>    <groupId>org.mapstruct</groupId>    <artifactId>mapstruct-processor</artifactId>    <version>1.2.0.Final</version></dependency><dependency>    <groupId>net.sf.dozer</groupId>    <artifactId>dozer</artifactId>    <version>5.5.1</version></dependency>

IV. Test Results

Shallow copy:

//Number of times: -------------------------------------------------------------------------------------------------------------------------------- Task name, ns =% ----------------------------------------------------------------------------------------------------------------------------------------------------------------------000007177.000% (000272971.000%) 011918713. 001% Hutool 045269308 017% (092633666.029%), 819434606 058% Spring 148264853 055% (150638012 048% (428660357 030%) MapStruct 002084618, 001% (016203277) 001%BeanCopier 073543500, 027% (068328236), 022% (135412730%)

Deep copy:

//Number of times: 1 ----------------------------------------------------------------------------------------------------------------------- Task name, ns =% ---------------------------------------------------------------------------------------- jdk IO010201657. 002% (122777241) 008% (4279557438) 005% Kryo 058354177 009% (049618874 003% (157321253 000% Orika) 344777022 056% (324837077 022%) 562185127 001% Dozer) 201866716 033% (996948636) 067% (75920114538) 094%

summary

1) MapStruct performance in shallow copy is basically the same as jdk's clone. MapStruct is recommended for use in project, and conforms to JSR 269 specification. It can also support custom conversion rules for fields, ignoring fields and so on.

2) Kryo is recommended for deep copy. It has stable performance, but the disadvantage is that each object needs to be registered. For example:

/**     *     * @param source     * @return     */    public DeepCopyEntity copyByKryo(DeepCopyEntity source){        kryo.register(DeepCopyEntity.class);        kryo.register(ArrayList.class);        kryo.register(Blind.class);        return kryo.copy(source);    }

3) The original way MapStruct uses get and set is certainly better than deriving attributes through reflection, but the disadvantage is that it requires deploy, Kryo, every time the mapper is updatedThe way to do deep copy is close to RPC's Protobuf and has relatively good cross-platform capabilities. It should be noted that since Kryo is thread insecure, it means that it needs to be instantiated whenever serialization and deserialization are required, or maintained with ThreadLocal or using the pool provided by Kryo to keep its threads safe, as follows:

private static final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {    protected Kryo initialValue() {        Kryo kryo = new Kryo();        kryo.setInstantiatorStrategy(        new Kryo.DefaultInstantiatorStrategy(                    new StdInstantiatorStrategy()));        return kryo;    };};

public KryoPool newKryoPool() {        return new KryoPool.Builder(() -> {            final Kryo kryo = new Kryo();            kryo.setInstantiatorStrategy(            new Kryo.DefaultInstantiatorStrategy(                    new StdInstantiatorStrategy()));            return kryo;        }).softReferences().build();    }

4) Kryo supports circular references by default, if not required, through kryo.setReferences(false);Off, performance will be better after off.

5) Kryo's source code is too long, and its excellent performance can be attributed to two main points: long, int and other data types, using variable-length byte storage instead of using fixed bytes in java (4,8)Byte mode, because fields with small values are more common, which saves space, much like Protobuf. The second step is to use a caching-like mechanism, in which the same object is serialized only once during the entire recursive serialization, followed by a local int value.

Posted by wilorichie on Wed, 08 Sep 2021 10:36:46 -0700