10 simple Java performance optimizations and java performance optimizations
Recently, the word "full domain (Web Scale)" has become popular, and people are also scaling their application architecture to make their systems more "full domain ". But what exactly is a full domain? Or how to ensure full domains?
Different aspects of Scaling
Scaling load is the most widely used domain. For example, systems that support access by a single user can support access by 10, 100, or even 1 million users. Ideally, our system should be as stateless as possible )". Even if the status must exist, it can be converted and transmitted on different processing terminals of the network. When the load becomes a bottleneck, there may be no latency. Therefore, it takes 50 to 100 milliseconds for a single request. This is the so-called Scaling out ).
The performance of the scale-out in domain optimization is completely different. For example, the algorithm that successfully processes one piece of data can also successfully process 10, 100, or even 1 million pieces of data. Whether the measurement type is feasible or not, the event complexity (large O symbol) is the best description. Latency is the killer of performance expansion. You will try your best to process all the operations on the same machine. This is the so-called Scaling up ).
If Pie falls in the sky (of course this is not possible), we may be able to combine horizontal scaling and vertical scaling. However, today we only want to introduce the following simple methods to improve efficiency.
Large O symbol
Both ForkJoinPool of Java 7 and parallel Stream of Java 8 are helpful for parallel processing. This is especially evident when a Java program is deployed on a multi-core processor, because all the processors can access the same memory.
Therefore, the fundamental advantage of this type of parallel processing over the expansion of different machines across networks is that latency can be almost completely eliminated.
But do not be confused by the effects of Parallel Processing! Remember the following two points:
- Parallel Processing consumes processor resources. Parallel Processing brings great benefits to batch processing, but it is also a nightmare for non-synchronous servers (such as HTTP. There are many reasons to explain why we have been using a single-threaded Servlet model for the past few decades. Parallel processing can only bring practical benefits when it is scaled vertically.
- Parallel processing has no impact on algorithm complexity. If the time complexity of your algorithm is O (nlogn) and the algorithm runs on c processors, the event complexity is still O (nlogn/c ), because c is only an insignificant constant in the algorithm. The only thing you save is the wall-clock time. The actual complexity of the algorithm is not reduced.
Reducing algorithm complexity is undoubtedly the most effective way to improve performance. For example, for the lookup () method of a HashMap instance, event complexity O (1) or space complexity O (1) is the fastest. However, this situation is often impossible, not to mention easy implementation.
If you cannot reduce the complexity of the algorithm, you can also find the key points in the algorithm and improve the method to improve the performance. Suppose we have the following algorithm:
The overall time complexity of this algorithm is O (N3). If it is calculated separately, the complexity is O (N x O x P ). However, when analyzing this code, we may find some strange scenarios:
- In the development environment, test data shows that the time complexity of the left branch (N-> M-> Heavy operation) is greater than that of the O and P on the right, so we only saw the left branch in our analyzer.
- In the production environment, your maintenance team may find out through AppDynamics, DynaTrace, or other gadgets, the main cause of the problem is the right branch (N-> O-> P-> Easy operation or also N. o. p. E .).
Without reference to production data, we may easily come to the conclusion that we want to optimize "high-sales operations. However, the optimization we made has no effect on the delivered products.
The optimized golden rule covers the following content:
- Good design will make optimization easier.
- Too early optimization cannot solve many performance problems, but poor design will lead to an increase in the difficulty of optimization.
Let's talk about this theory first. Suppose we have discovered the problem on the right branch, it is very likely that the response is lost due to the time-consuming simple processing in the product (assuming that the values of N, O, and P are very large ), note that the time complexity of the left branch mentioned in this article is O (N3 ). The effort made here cannot be expanded, but it can save time for users and delay the difficult performance improvement to the end.
Here are 10 small suggestions for improving Java performance:
1. Use StringBuilder
StingBuilder should be used by default in our Java code, and the + operator should be avoided. You may have different opinions on the syntax sugar (syntax sugar) of StringBuilder, such:
String x = "a" + args.length + "b";
Will be compiled:
0 new java.lang.StringBuilder [16] 3 dup 4 ldc <String "a"> [18] 6 invokespecial java.lang.StringBuilder(java.lang.String) [20] 9 aload_0 [args]10 arraylength11 invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23]14 ldc <String "b"> [27]16 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29]19 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32]22 astore_1 [x]
But what exactly happened? Do you need to use the following parts to improve the String?
String x = "a" + args.length + "b"; if (args.length == 1) x = x + args[0];
Now the second StringBuilder is used. This StringBuilder does not consume additional memory in the heap, but it puts pressure on GC.
StringBuilder x = new StringBuilder("a");x.append(args.length);x.append("b"); if (args.length == 1); x.append(args[0]);
Summary
In the above example, if you rely on the Java compiler to generate an instance implicitly, the compilation effect is almost irrelevant to whether the StringBuilder instance is used. Remember: In N. o. p. in Branch E, the cycle time of each CPU to white is spent on GC or allocating the default space for StringBuilder. We are wasting N x O x P time.
In general, the use of StringBuilder is better than the use of the + operator. If possible, select StringBuilder if you need to pass references across multiple methods, because String consumes additional resources. JOOQ uses this method to generate complex SQL statements. Only one StringBuilder is used during the SQL transmission of the Abstract Syntax Tree (AST Abstract Syntax Tree.
Even more tragic, if you are still using StringBuffer, use StringBuilder to replace StringBuffer. After all, there are not many cases for string synchronization.
2. Avoid using regular expressions
Regular Expressions give a quick and easy impression. However, using regular expressions in the N. O. P. E branch is the worst decision. If you have to use regular expressions in computing-intensive code, at least cache Pattern to avoid repeated compilation of Pattern.
static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*");
If only the following simple regular expressions are used:
String[] parts = ipAddress.split("\\.");
It is best to use an ordinary char [] array or an index-based operation. For example, the following code with poor readability actually plays the same role.
int length = ipAddress.length();int offset = 0;int part = 0;for (int i = 0; i < length; i++) { if (i == length - 1 || ipAddress.charAt(i + 1) == '.') { parts[part] = ipAddress.substring(offset, i + 1); part++; offset = i + 2; }}
The code above also shows that premature optimization is meaningless. Although compared with the split () method, this code has poor maintainability.
Challenge: Can smart friends come up with faster algorithms?
Summary
Regular Expressions are very useful, but they also have to pay for them. Especially in the depth of the N. O. P. E branch, do not use regular expressions unless necessary. Be careful with the JDK String methods that use regular expressions, such as String. replaceAll () or String. split (). You can use popular development libraries, such as Apache Commons Lang, to perform string operations.
3. Do not use the iterator () method
This suggestion is not applicable to general scenarios and is only applicable to scenarios deep in the N. O. P. E branch. Even so, you should be aware of it. Java 5 format is so convenient that we can forget the internal loop method, such:
for (String value : strings) { // Do something useful here}
When the code runs in this loop, if the strings variable is an Iterable, the code will automatically create an Iterator instance. If ArrayList is used, the virtual opportunity will automatically allocate three integer-sized memories to the object on the heap.
private class Itr implements Iterator<E> { int cursor; int lastRet = -1; int expectedModCount = modCount; // ...
You can also use the following equivalent loop method to replace the above for loop. It is quite cost-effective to just "waste" the partition on the stack and create an integer.
int size = strings.size();for (int i = 0; i < size; i++) { String value : strings.get(i); // Do something useful here}
If the value of the character string does not change much in a loop, an array can be used for loop.
for (String value : stringArray) { // Do something useful here}
Summary
The iterator, Iterable interface, and foreach loop are both very useful from the perspective of ease of reading and writing or from the perspective of API design. However, when using them, an additional object is created for each loop child on the heap. If the loop is executed many times, be sure to avoid generating meaningless instances. It is best to replace the aforementioned iterator, Iterable interface, and foreach loop with the basic pointer loop method.
Discussion
For some opinions that disagree with the above content (especially replacing the iterator with pointer operations), see the discussion on Reddit.
4. Do not call high-sales methods
Some methods have high overhead. Taking the N. O. P. E branch as an example, we did not mention the leaf-related method, but this can be used. Let us assume that our JDBC driver needs to eliminate all difficulties to calculate the return value of the ResultSet. wasNull () method. The self-implemented SQL framework may look like the following:
if (type == Integer.class) { result = (T) wasNull(rs, Integer.valueOf(rs.getInt(index)));} // And then...static final <T> T wasNull(ResultSet rs, T value)throws SQLException { return rs.wasNull() ? null : value;}
In the above logic, the ResultSet. wasNull () method is called every time the int value is obtained from the result set, but the getInt () method is defined:
Return type: variable value. If the SQL query result is NULL, 0 is returned.
Therefore, a simple and effective improvement method is as follows:
static final <T extends Number> T wasNull( ResultSet rs, T value)throws SQLException { return (value == null || (value.intValue() == 0 && rs.wasNull())) ? null : value;}
This is a breeze.
Summary
Cache the method call to replace the high-end method on the leaf node, or avoid calling the high-end method if the method conventions permit.
5. Use the original type and stack
A large number of generic types are used in the example from jOOQ. The result is that the packaging classes of byte, short, int, and long are used. But at least generics should not be code restrictions until they are specialized in Java 10 or the Valhalla project. Because the following method can be used for replacement:
// The Integer I = 817598 is stored on the stack;
...... If it is written as follows:
// Store int I = 817598 on the stack;
When using arrays, the situation may become worse:
// Three objects Integer [] I = {1337,424 242} are generated on the stack };
...... If it is written as follows:
// Only one object int [] I = {1337,424 242} is generated on the stack };
Summary
When we are in the depth of the N. O. P. E. Branch, we should try our best to avoid using the packaging class. The disadvantage of doing so is that it puts a lot of pressure on GC. GC will be too busy to clear the objects generated by the packaging class.
Therefore, an effective optimization method is to use the basic data type, fixed length array, and a series of segmentation variables to identify the position of the object in the array.
Trove4j, which follows the LGPL protocol, is a Java Collection class library that provides better performance than integer array int.
Exceptions
The following is an exception to this rule: Because the boolean and byte types are insufficient for JDK to provide caching methods. We can write as follows:
Boolean a1 = true; // ... syntax sugar for:Boolean a2 = Boolean.valueOf(true); Byte b1 = (byte) 123; // ... syntax sugar for:Byte b2 = Byte.valueOf((byte) 123);
The basic types of other integers are similar, such as char, short, int, and long.
Do not bind these basic integer types automatically or call the TheType. valueOf () method when calling the constructor.
Do not call the constructor on the packaging class unless you want an instance not created on the stack. The advantage of doing so is to present a big pitfall joke to your colleagues.
Non-Heap Storage
Of course, if you still want to experience off-heap function libraries, although this may involve a lot of strategic decisions, it is not the most optimistic local solution. For an interesting article about non-Heap Storage written by Peter Lawrey and Ben Cotton, click: OpenJDK and HashMap-to give the veteran a safe grasp (non-Heap Storage !) New tips.
6. Avoid Recursion
Currently, functional programming languages like Scala encourage recursion. Because recursion usually means that it can be decomposed into tail-recursing optimized by individual users ). If the programming language you are using can support it, that would be even better. Even so, you should also note that the slight adjustment to the algorithm will make the tail recursion become normal recursion.
We hope the compiler can automatically detect this point, otherwise we will waste a lot of stack frameworks (frames) in order to use just a few local variables ).
Summary
There is nothing to say in this section, except that iteration should be used instead of recursion in the N. O. P. E branch.
7. Use entrySet ()
When we want to traverse a Map saved as a key-value pair, we must find a good reason for the following code:
for (K key : map.keySet()) { V value : map.get(key);}
Let alone the following statement:
for (Entry<K, V> entry : map.entrySet()) { K key = entry.getKey(); V value = entry.getValue();}
Map should be used with caution when we use N. O. P. E. Branch. Because many access operations that seem to be time-complex as O (1) are actually composed of a series of operations. In addition, access is not free. At least, if you have to use map, use the entrySet () method to iterate! In this way, we only need to access the Map. Entry instance.
Summary
The entrySet () method must be used to iterate the Map in the form of key-value pairs.
8. Use EnumSet or EnumMap
In some cases, for example, when configuring map, we may know the key value stored in map in advance. If the key value is very small, we should consider using EnumSet or EnumMap, rather than using our commonly used HashSet or HashMap. The following code provides a clear explanation:
private transient Object[] vals; public V put(K key, V value) { // ... int index = key.ordinal(); vals[index] = maskNull(value); // ...}
The key implementation of the previous Code is that we use arrays instead of hash tables. In particular, when inserting a new value into map, all you need to do is to obtain a constant serial number generated by the compiler for each Enumeration type. If there is a global map configuration (such as only one instance), EnumMap performs better than HashMap when the access speed is increased. The reason is that the heap memory used by EnumMap is one bit less than that of HashMap, and HashMap must call the hashCode () method and equals () method on each key value.
Summary
Enum and EnumMap are close friends. When we use key values similar to the enum-like structure, we should consider declaring these key values as enumeration types and using them as the EnumMap key.
9. Optimize the custom hasCode () method and equals () method
When EnumMap cannot be used, at least the hashCode () and equals () methods must be optimized. A good hashCode () method is necessary because it can prevent unnecessary calls to the high-end equals () method.
Simple objects that are easy to accept in the inheritance structure of each class. Let's take a look at how jOOQ's org. jooq. Table is implemented?
The simplest and fastest Implementation of hashCode () is as follows:
// Basic implementation of a common Table using AbstractTable: @ Overridepublic int hashCode () {// [#1938] This is a more efficient hashCode () than the standard QueryParts () implement return name. hashCode ();}
Name indicates the table name. We do not even need to consider schema or other table attributes, because the table name is usually unique in the database. And the variable name is a string, which has long cached A hashCode () value.
Annotations in this Code are very important because the AbstractTable inherited from AbstractQueryPart is the basic implementation of any abstract syntax tree element. Ordinary abstract syntax tree elements do not have any attributes, so they cannot have any fantasies about optimizing the implementation of the hashCode () method. The overwrite hashCode () method is as follows:
// Define actquerypart is a basic implementation of a general abstract syntax tree: @ Overridepublic int hashCode () {// This is a workable default implementation. // Subclass of the specific implementation should overwrite this method to improve performance. Return create (). renderInlined (this). hashCode ();}
In other words, the entire SQL rendering workflow (rendering workflow) is triggered to calculate the hash code of a common abstract syntax tree element.
The equals () method is more interesting:
// Basic implementation of AbstractTable generic tables: @ Overridepublic boolean equals (Object that) {if (this = that) {return true ;} // [#2144] Call the highly open AbstractQueryPart. before the equals () method, // you can know whether objects are not equal as early as possible. If (that instanceof AbstractTable) {if (StringUtils. equals (name, (AbstractTable <?>) That). name) {return super. equals (that);} return false ;}
First, do not use the equals () method too early (not only in N. O. P. E.) if:
- This = argument
- This "incompatible: Parameter
NOTE: If we use instanceof too early to verify the compatible type, the following conditions actually include argument = null. I have explained this in my previous blog. Please refer to 10 exquisite Java coding best practices.
After comparing the above situations, we should be able to draw some conclusions. For example, the Table. equals () method of jOOQ is used to compare whether the two tables are the same. Regardless of the specific implementation type, they must have the same field name. For example, the following two elements cannot be the same:
- Com. example. generated. Tables. MY_TABLE
- DSL. tableByName ("MY_OTHER_TABLE ")
If we can easily determine whether the input parameter is equal to the instance itself (this), we can discard the operation if the returned result is false. If the returned result is true, we can further judge the implementation of the parent class (super. When most of the objects that have been compared are different, we can end the method as soon as possible to save the CPU execution time.
Some objects have higher similarity than other objects.
In jOOQ, most table instances are generated by the jOOQ code generator, and The equals () methods of these instances are deeply optimized. Dozens of other table types (derived table (derived tables), table-valued functions, array table (array tables), and join table (joined tables) the basic implementation of the equals () method is maintained.
10. Consider using set instead of a single element.
Finally, there is another situation that applies to all languages, not just Java. In addition, the N. O. P. E. Branch we previously studied will also be helpful for understanding from O (N3) to O (n log n.
Unfortunately, many programmers use simple and local algorithms to consider the problem. They are used to solving problems step by step. This is a functional programming style in the "yes/or" form of imperative (imperative. This programming style is easy to model a "larger scenario (bigger picture)" when it is converted from pure imperative programming to object-oriented programming to functional programming, however, these styles are missing only in SQL and R languages:
Declarative Programming.
In SQL, we can declare the effect required by the database without considering the influence of algorithms. Databases can adopt optimal algorithms based on data types, such as constraints, keys, and indexes.
Theoretically, we initially had a basic idea after SQL and relational calculus. In practice, SQL vendors have implemented the overhead-Based efficient Optimisers (CBOs) over the past few decades ). Then, in version 2010, we finally fully explored all the potential of SQL.
However, we do not need to use the set method to implement SQL. All languages and libraries support Sets, collections, bags, and lists. The main benefit of using set is to make our code concise and clear. For example:
SomeSet INTERSECT SomeOtherSet
Instead
// Set result = new HashSet (); for (Object candidate: someSet) if (someOtherSet. contains (candidate) result. add (candidate); // someSet is not very helpful even if Java 8 is used. stream (). filter (someOtherSet: contains ). collect (Collectors. toSet ());
Some may have different opinions on functional programming and Java 8 that can help us write simpler and more concise algorithms. However, this is not necessarily true. We can convert the imperative Java 7 loop into the Stream collection of Java 8, but we still adopt the same algorithm. However, SQL expressions are different:
SomeSet INTERSECT SomeOtherSet
The above code can have 1000 different implementations on different engines. What we have studied today is that, before calling the INTERSECT operation, the two sets are more intelligently converted to EnumSet. Even we can perform parallel INTERSECT operations without calling the underlying Stream. parallel () method. Summary
In this article, we discuss the optimization of N. O. P. E. Branch. For example, go deep into high-complexity algorithms. As a jOOQ developer, we are happy to optimize SQL generation.
- Each query is generated using a unique StringBuilder.
- The template engine actually processes characters rather than regular expressions.
- Select the array as much as possible, especially when performing iteration on the listener.
- The JDBC Method is far away.
- And so on.
JOOQ is at the bottom of the "food chain" because it is the last API called by our computer program when it leaves JVM and enters DBMS. Located at the bottom of the food chain means that it takes N x O x P time for any line to be executed in jOOQ, so I need to optimize it as soon as possible.
Our business logic may not be as complicated as the N. O. P. E. Branch. However, the basic framework may be very complex (local SQL framework, local database, and so on ). Therefore, we need to follow the principles we mentioned today to use Java Mission Control or other tools for review to confirm whether there is any need for optimization.