After reading these excellent cases of Java code, you must be improved!

Posted by daniel-g on Fri, 17 Dec 2021 08:17:51 +0100

preface

Program performance is directly affected by code quality. This time mainly introduces some tips and conventions for coding. Although it seems that some programming skills are insignificant, they may bring multiple improvements to the system performance, so they are still worthy of attention.

Use with caution

In Java development, try catch is often used for error capture, but try catch statement is very bad for system performance. Although the performance loss caused by a try catch can not be detected, once the try catch statement is applied to the loop or traversal body, it will bring great harm to the system performance.

The following is an example code for applying try catch to a loop body:

@Test
    public void test11() {

        long start = System.currentTimeMillis();
        int a = 0;
        for(int i=0;i<1000000000;i++){
            try {
                a++;
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);

    }

The running result of the above code is:

useTime:10

The following is a code to move the try catch to the extracorporeal circulation, and the performance is improved by nearly half. As follows:

@Test
    public void test(){
        long start = System.currentTimeMillis();
        int a = 0;
        try {
            for (int i=0;i<1000000000;i++){
                a++;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println(useTime);
    }

Operation results:

useTime:6

Use local variables

The parameters passed when calling the method and the temporary variables created in the call are saved in the Stack, which is fast. Other variables, such as static variables and instance variables, are created in the Heap, which is slow.

Here is a code that uses local variables for calculation:

@Test
    public void test11() {

        long start = System.currentTimeMillis();
        int a = 0;
        for(int i=0;i<1000000000;i++){
            a++;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);

    }

Operation results:

useTime:5

Replace the local variable with the static variable of the class:

static int aa = 0;
    @Test
    public void test(){
        long start = System.currentTimeMillis();

        for (int i=0;i<1000000000;i++){
            aa++;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

Operation results:

useTime:94

From the above two running results, we can see that the access speed of local variables is much higher than that of class member variables.

Bit operation instead of multiplication and division

Among all operations, bit operation is the most efficient. Therefore, we can try to use bit operation instead of partial arithmetic operation to improve the running speed of the system. The most typical is the optimization of integer multiplication and division.

Here is a code that uses arithmetic operations:

@Test
    public void test11() {

        long start = System.currentTimeMillis();
        int a = 0;
        for(int i=0;i<1000000000;i++){
            a*=2;
            a/=2;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

Operation results:

useTime:1451

Change the multiplication and division operation in the loop body to an equivalent bit operation, and the code is as follows:

@Test
    public void test(){
        long start = System.currentTimeMillis();
        int aa = 0;
        for (int i=0;i<1000000000;i++){
            aa<<=1;
            aa>>=1;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

Operation results:

useTime:10

The last two pieces of code perform exactly the same function. In each loop, the integer is multiplied by 2 and divided by 2. However, the running results vary greatly in time, so the efficiency of bit operation is obvious.

Extract expression

In the process of software development, it is easy for programmers to make code do some "repetitive work" intentionally or unintentionally. In most cases, due to the high-speed operation of the computer, these "repetitive work" will not pose a great threat to the performance, but if they want to give full play to the system performance, it is quite meaningful to extract these "repetitive work".

For example, two arithmetic calculations are performed in the following code:

@Test
    public void testExpression(){
        long start = System.currentTimeMillis();
        double d = Math.random();
        double a = Math.random();
        double b = Math.random();
        double e = Math.random();

        double x,y;
        for(int i=0;i<10000000;i++){
            x = d*a*b/3*4*a;
            y = e*a*b/3*4*a;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);

    }

Operation results:

useTime:21

If you look carefully, you can find that the second half of the two calculation expressions are exactly the same, which also means that the expression of the same part is recalculated in each cycle.

After improvement, it will look like the following:

@Test
    public void testExpression99(){
        long start = System.currentTimeMillis();
        double d = Math.random();
        double a = Math.random();
        double b = Math.random();
        double e = Math.random();

        double p,x,y;
        for(int i=0;i<10000000;i++){
            p = a*b/3*4*a;
            x = d*p;
            y = e*p;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

Operation results:

useTime:11

Through the running results, we can see the specific optimization effect.

Similarly, if a time-consuming operation needs to be performed in a circulation, and the execution result is always unique in the circulation, it should also be extracted out of the circulation.

For example, the following code:

for(int i=0;i<100000;i++){
    x[i] = Math.PI*Math.sin(y)*i;
}

It should be improved to the following code:

//Extract complex, fixed results of business logic processing to extracorporeal circulation
double p = Math.PI*Math.sin(y);
for(int i=0;i<100000;i++){
    x[i] = p*i;
}

Using arrayCopy()

Array replication is a frequently used function. JDK provides an efficient API to implement it.

/**
     * @param      src      the source array.
     * @param      srcPos   starting position in the source array.
     * @param      dest     the destination array.
     * @param      destPos  starting position in the destination data.
     * @param      length   the number of array elements to be copied.
     * @exception  IndexOutOfBoundsException  if copying would cause
     *               access of data outside array bounds.
     * @exception  ArrayStoreException  if an element in the <code>src</code>
     *               array could not be stored into the <code>dest</code> array
     *               because of a type mismatch.
     * @exception  NullPointerException if either <code>src</code> or
     *               <code>dest</code> is <code>null</code>.
     */
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

If you need to copy arrays in your application, you should use this function instead of implementing it yourself.

Here are some examples:

@Test
    public void testArrayCopy(){
        int size = 100000;
        int[] array = new int[size];
        int[] arraydest = new int[size];

        for(int i=0;i<array.length;i++){
            array[i] = i;
        }
        long start = System.currentTimeMillis();
        for (int k=0;k<1000;k++){
            //Copy
            System.arraycopy(array,0,arraydest,0,size);
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

Operation results:

useTime:59

Correspondingly, if you copy the array in the program, the equivalent code is as follows:

@Test
    public void testArrayCopy99(){
        int size = 100000;
        int[] array = new int[size];
        int[] arraydest = new int[size];

        for(int i=0;i<array.length;i++){
            array[i] = i;
        }
        long start = System.currentTimeMillis();
        for (int k=0;k<1000;k++){
            for(int i=0;i<size;i++){
                arraydest[i] = array[i];
            }
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

Operation results:

useTime:102

The effect can be seen through the operation results.

Because system Arraycopy () function is a native function. Generally, the performance of native function is better than that of ordinary function. For performance reasons only, native functions should be called as much as possible during program development.

I/O using Buffer

In addition to NIO, there are two basic ways to use Java for I/O operation;

  • Use the method based on InputStream and OutputStream;
  • Use Writer and Reader;

No matter which method is used for file I/O, if the buffer can be used reasonably, the performance of I/O can be effectively improved.

Buffering components used with InputStream, OutputStream, Writer, and Reader.

As shown below:

Using buffer components to wrap file I/O can effectively improve the performance of file I/O.

The following is a code that directly uses InputStream and OutputStream to read and write files:

@Test
    public void testOutAndInputStream(){

        try {
            DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("/IdeaProjects/client2/src/test/java/com/client2/cnblogtest/teststream.txt"));
            long start = System.currentTimeMillis();
            for(int i=0;i<10000;i++){
                dataOutputStream.writeBytes(Objects.toString(i)+"\r\n");
            }
            dataOutputStream.close();
            long useTime = System.currentTimeMillis()-start;
            System.out.println("Write data--useTime:"+useTime);
            //Start reading data
            long startInput = System.currentTimeMillis();
            DataInputStream dataInputStream = new DataInputStream(new FileInputStream("/IdeaProjects/client2/src/test/java/com/client2/cnblogtest/teststream.txt"));

            while (dataInputStream.readLine() != null){
            }
            dataInputStream.close();
            long useTimeInput = System.currentTimeMillis()-startInput;
            System.out.println("Read data--useTimeInput:"+useTimeInput);
        }catch (Exception e){
            e.printStackTrace();
        }

    }

Operation results:

Write data -- useTime:660 Read data -- useTimeInput:274

The code for using buffer is as follows:

@Test
    public void testBufferedStream(){

        try {
            DataOutputStream dataOutputStream = new DataOutputStream(
                    new BufferedOutputStream(new FileOutputStream("/IdeaProjects/client2/src/test/java/com/client2/cnblogtest/teststream.txt")));
            long start = System.currentTimeMillis();
            for(int i=0;i<10000;i++){
                dataOutputStream.writeBytes(Objects.toString(i)+"\r\n");
            }
            dataOutputStream.close();
            long useTime = System.currentTimeMillis()-start;
            System.out.println("Write data--useTime:"+useTime);
            //Start reading data
            long startInput = System.currentTimeMillis();
            DataInputStream dataInputStream = new DataInputStream(
                    new BufferedInputStream(new FileInputStream("/IdeaProjects/client2/src/test/java/com/client2/cnblogtest/teststream.txt")));

            while (dataInputStream.readLine() != null){
            }
            dataInputStream.close();
            long useTimeInput = System.currentTimeMillis()-startInput;
            System.out.println("Read data--useTimeInput:"+useTimeInput);
        }catch (Exception e){
            e.printStackTrace();
        }

    }

Operation results:

Write data -- useTime:22 Read data -- useTimeInput:12

Through the running results, we can clearly see that the performance of buffered code has been improved by orders of magnitude both in reading and writing files.

Using Wirter and Reader has similar effects.

The following code:

@Test
    public void testWriterAndReader(){

        try {
            long start = System.currentTimeMillis();
            FileWriter fileWriter = new FileWriter("/IdeaProjects/client2/src/test/java/com/client2/cnblogtest/teststream.txt");
            for (int i=0;i<100000;i++){
                fileWriter.write(Objects.toString(i)+"\r\n");
            }
            fileWriter.close();
            long useTime = System.currentTimeMillis()-start;
            System.out.println("Write data--useTime:"+useTime);
            //Start reading data
            long startReader = System.currentTimeMillis();
            FileReader fileReader = new FileReader("/IdeaProjects/client2/src/test/java/com/client2/cnblogtest/teststream.txt");
            while (fileReader.read() != -1){
            }
            fileReader.close();
            long useTimeInput = System.currentTimeMillis()-startReader;
            System.out.println("Read data--useTimeInput:"+useTimeInput);
        }catch (Exception e){
            e.printStackTrace();
        }

    }

Operation results:

Write data -- useTime:221 Read data -- useTimeInput:147

Corresponding code using buffer:

@Test
    public void testBufferedWriterAndReader(){

        try {
            long start = System.currentTimeMillis();
            BufferedWriter fileWriter = new BufferedWriter(
                    new FileWriter("/IdeaProjects/client2/src/test/java/com/client2/cnblogtest/teststream.txt"));
            for (int i=0;i<100000;i++){
                fileWriter.write(Objects.toString(i)+"\r\n");
            }
            fileWriter.close();
            long useTime = System.currentTimeMillis()-start;
            System.out.println("Write data--useTime:"+useTime);
            //Start reading data
            long startReader = System.currentTimeMillis();
            BufferedReader fileReader = new BufferedReader(
                    new FileReader("/IdeaProjects/client2/src/test/java/com/client2/cnblogtest/teststream.txt"));
            while (fileReader.read() != -1){
            }
            fileReader.close();
            long useTimeInput = System.currentTimeMillis()-startReader;
            System.out.println("Read data--useTimeInput:"+useTimeInput);
        }catch (Exception e){
            e.printStackTrace();
        }

    }

Operation results:

Write data -- useTime:157 Read data -- useTimeInput:59

It can be seen from the running results that the performance of both FileReader and FileWriter has been significantly improved after using buffering.

In the above example, because the performance of FileReader and FilerWriter is better than that of directly using FileInputStream and FileOutputStream, the number of cycles is increased by 10 times