The SDK provides several Java. util. List Implementation interfaces for Ordered Sets. Three of them are most well-known: vector, arraylist, and sorted list. The performance difference between these List classes is a frequently asked question. In this article, I want to discuss the performance differences between vector list and vector/arraylist.
To fully analyze the performance differences between these classes, we must know their implementation methods. Therefore, I will first briefly introduce the implementation features of these classes from the perspective of performance.
1. Implementation of vector and arraylist
Both vector and arraylist carry an underlying object [] array, which is used to save elements. When accessing an element through an index, you only need to access the elements of an internal array through an index:
Public object get (INT index) {// First check whether the index is legal... this part of code return is not displayed here Elementdata [Index];} |
The internal array can be larger than the number of elements owned by the vector/arraylist object. The difference between the two can be used as the remaining space to quickly add new elements. With the remaining space, it is very easy to add elements. You only need to save the new elements to an empty position in the internal array, and then add the index value for the new free position:
Public Boolean add (Object O) {Ensurecapacity (size + 1); // elementdata [size ++] = O; return true; // Return value of list. Add (object}
|
Insert an element to any specified position in the Set (rather than the end of the Set). A little more complicated: All the array elements above the insertion point must move one forward before assigning values:
Public void add (INT index, object element ){ // First check whether the index is legal... this part of code is not displayed here Ensurecapacity (size + 1 ); System. arraycopy (elementdata, index, elementdata, index + 1, Size-index ); Elementdata [Index] = element; Size ++; }
|
When the remaining space is used up, if you need to add more elements, the vector/arraylist object must replace its internal object [] array with a larger new array, copy all array elements to the new array. Depending on the SDK version, the new array is 50% or 100% larger than the original one (the code shown below expands the array by 100% ):
Public void ensurecapacity (INT mincapacity ){ Int oldcapacity = elementdata. length; If (mincapacity> oldcapacity ){ Object olddata [] = elementdata; Int newcapacity = math. Max (oldcapacity * 2, mincapacity ); Elementdata = new object [newcapacity]; System. arraycopy (olddata, 0, elementdata, 0, size ); } }
|
The main difference between the Vector class and the arraylist class is synchronization. Except for two methods that are only used for serialization, none of the arraylist methods have the ability to perform synchronization. On the contrary, most methods of Vector have the ability to synchronize, either directly or indirectly. Therefore, vector is thread-safe, but arraylist is not. This makes the arraylist faster than the vector. For some of the latest JVM, the speed difference between the two classes is negligible: strictly speaking, for these JVM, the speed difference between the two classes is less than the time difference shown in the test of comparing the performance of these classes.
When accessing and updating elements through indexes, the implementation of vector and arraylist has excellent performance because there is no overhead other than scope check. Unless the internal array space is exhausted, expansion is required. Otherwise, adding elements to the end of the list or deleting elements from the end of the list also has excellent performance. Insert and delete elements always need to copy the Array (when the array must be expanded first, two copies are required ). The number of copied elements is proportional to [size-Index], that is, the distance between the insertion/deletion point and the last index position in the set. During the insert operation, when an element is inserted to the beginning of the Set (index 0), the performance is the worst. When the element is inserted to the end of the Set (after the last existing element), the performance is the best. As the scale of the Set increases, the overhead of array replication also increases rapidly because the number of elements that must be copied increases each insert operation.
II. Implementation of consumer list
The worker list is implemented through a list of nodes with two-way links. To access elements through indexes, you must search for all nodes until you find the target node:
Public object get (intindex ){ // First check whether the index is legal... this part of code is not displayed here Entry E = header; // Start Node // Search for the forward or backward direction. // Near decision If (index <size/2 ){ For (INT I = 0; I <= index; I ++) E = E. Next; } Else { For (INT I = size; I> index; I --) E = E. Previous; } Return E; }
|
It is easy to insert elements into the list: Find the node with the specified index and insert a new node immediately before the node:
Public void add (INT index, object element ){ // First check whether the index is legal... this part of code is not displayed here Entry E = header; // starting Node // Search for the forward or backward direction. // Near decision If (index <size/2 ){ For (INT I = 0; I <= index; I ++) E = E. Next; } Else { For (INT I = size; I> index; I --) E = E. Previous; } Entry newentry = new entry (element, E, E. Previous ); Newentry. Previous. Next = newentry; Newentry. Next. Previous = newentry; Size ++; }
|
Thread-safe consumer list and other sets
To get a thread-safe consumer list from the Java SDK, you can use a synchronization package to get one from collections. synchronizedlist (list. However, the use of a synchronization package is equivalent to adding an indirect layer, which will bring a high performance cost. When the package passes the call to the encapsulated method, an additional method call is required for each method, the method encapsulated by the synchronous package is two to three times slower than the unencapsulated method. For complex operations such as search, the overhead of indirect calls is not very prominent. However, for simple methods such as access functions or update functions, this overhead may have a serious impact on performance.
This means that, compared with vector, the synchronized encapsulated consumer list has a significant performance disadvantage, because vector does not need to perform any additional indirect calls for thread security. If you want to have a thread-safe consumer list, you can copy the consumer list class and synchronize several necessary methods so that you can get a faster implementation. This is equally effective for all other collection classes: only list and map have efficient thread security implementations (vector and hashtable classes ). Interestingly, these two efficient thread security classes exist only for backward compatibility, not for performance considerations.
For accessing and updating elements through an index, the performance overhead of an indexed list is slightly higher, because accessing any index requires crossing multiple nodes. When an element is inserted, in addition to performance overhead that spans multiple nodes, there is also overhead, that is, the overhead for creating node objects. In terms of advantages, the insert and delete operations implemented by the shortlist have no other overhead. Therefore, the insert-delete overhead is almost entirely dependent on the distance between the insert-delete point and the end of the set.
Iii. Performance Testing
These classes have many different functions that can be tested. The shortlist is frequently used because it is considered to have good performance in random insert and delete operations. Therefore, the following analysis focuses on the performance of the insert operation, that is, constructing a set. I tested and compared the rule list and arraylist, because both are non-synchronous.
The insert operation speed is mainly determined by the size of the Set and the insert position of the element. When the insertion point is located at both ends and the middle of the set, the worst insertion performance and the best insertion performance both have a chance. Therefore, I have selected three insertion locations (beginning, end, and center of the Set), three typical set sizes: medium (100 elements), large (10,000 elements ), super Large (1,000,000 elements ).
In the test in this article, I used the Sun JVM of Java SDK 1.2.0 and 1.3.0 series. In addition, I used hotspot JVM 2.0 for testing. This version can be found in 1.3.0 SDK. In the following table, the measurement time is displayed on the basis of the test time on the SDK 1.2 VM (The unit shown in the table is 100%. During the test, the default JVM configuration is used, that is, JIT compilation is enabled. Therefore, the heap space must be extended for all JVMs to avoid memory overflow errors. The time recorded in the table is the average time of multiple tests. To avoid the impact of garbage collection, I forcibly cleaned up the entire memory (see test source code for details) between tests ). Disk Monitoring ensures that the disk paging does not appear during the test (any test, if it shows serious disk paging operations, is discarded ). All tests that show slow response times for several seconds are repeated until a reasonable time is recorded.
Table 1: Construct a medium-size set (100 elements ). The numbers in parentheses are for a set of predefined sizes. |
|
1.2 JVM |
1.3 JVM |
Hotspot 2.0 JVM |
Always insert to the beginning of arraylist |
100% (48.0%) |
184.9% (152.0%) |
108.0% (66.7%) |
Always insert to the beginning of the consumer list |
135.5% |
109.1% |
85.3% |
Always inserted in the middle of the arraylist |
130.0% (40.6%) |
187.4% (158.0%) |
84.7% (46.0%) |
Always inserted in the middle of the consumer list |
174.0% |
135.0% |
102.3% |
Always insert to the end of arraylist |
63.3% (20.7%) |
65.9% (25.0%) |
60.3% (29.3%) |
Always insert to the end of the consumer list |
106.7% |
86.3% |
80.3% |
For a small set, the performance of arraylist and sorted list is very similar. When an element is inserted to the end of the set, that is, the append element, the arraylist performance changes. However, the append element is an operation especially optimized for arraylist: If you only want a static set of fixed sizes, a Java array (such as object []) it has better performance than any set object. Apart from the append operation, the measured time data is not very different. They reflect the degree of optimization of each JVM, rather than anything else.
For example, for inserting elements into the beginning of the Set (the first two rows of table 1), hotspot 2.0 JVM and consumer list have the best performance (85.3% ), the second place is 1.2 JVM and arraylist (100% ). The two results show that the simple JIT compiler in 1.2 is very efficient in performing simple operations such as iteration and copying arrays. The complex JVM in hotspot, coupled with an optimized compiler, can improve the performance of complex operations, such as object creation (creating a writable list node) and take advantage of Code-inlining. 1.3 JVM results seem to show that its performance is very poor in simple operations, which may be improved in future JVM versions.
Here, I specifically tested another advantage of arraylist over sorted list, that is, the ability to determine the size of a set in advance. Specifically, you can specify a specific size when creating an arraylist (for example, in the test, arraylist can be created as a capacity with 100 elements ), this avoids the overhead of increasing the number of elements. The numbers in the brackets in Table 1 show the increase in the performance of the predefined set size. The resize list (until SDK 1.3) cannot be pre-determined.
In addition, arraylist generates only a small number of objects for garbage collection, that is, the internal array objects used to save the elements, and the additional internal array objects created when each arraylist capacity is insufficient. Vertex list generates a Node object for each insert operation regardless of any possible delete operation. Therefore, the garbage list will bring a lot of work to the garbage collector. With these factors in mind, for any small and medium-sized collection, I will choose to use the arraylist instead of the aggregate list.
Table 2: Construct a large collection (10,000 elements) |
|
1.2 JVM |
1.3 JVM |
Hotspot 2.0 JVM |
Always insert to the beginning of arraylist |
7773% |
7537% |
7500% |
Always insert to the beginning of the consumer list |
100% |
90.34% |
65.6% |
Always inserted in the middle of the arraylist |
3318% |
3412% |
3121% |
Always inserted in the middle of the consumer list |
26264% |
14315% |
14209% |
Always insert to the end of arraylist |
41.4% |
41.2% |
37.5% |
Always insert to the end of the consumer list |
66.4% |
73.9% |
61.7% |
Table 2 shows the test results of a large-scale set. We can see that when a large-scale insert operation occurs, we begin to suffer severe performance penalties. As the result of our previous implementation of the analysis class, the worst case for the sort list is when the element is inserted into the collection. In addition, we can also see that, compared with the worst performance of inserting an element into the beginning of the set when using arraylist, the performance of inserting an element into the middle of the set when using explain list is worse. Compared to the two worst-performing situations, it is much better to insert elements into the arraylist.
In general, arraylist once again shows better performance in most cases, including inserting elements to random positions based on indexes. If you always want to insert elements to the front of the set, the sorted list has better performance. However, you can use a reverse arraylist to get better performance, that is, use a dedicated implementation, or map the position of the flipped index in the set through [size-Index.
Table 3: Construct an ultra-large set (1,000,000 elements) |
|
1.2 JVM |
1.3 JVM |
Hotspot 2.0 JVM |
Always insert to the beginning of arraylist |
Too long |
Too long |
Too long |
Always insert to the beginning of the consumer list |
100% |
179.5% |
144.1% |
Always inserted in the middle of the arraylist |
Too long |
Too long |
Too long |
Always inserted in the middle of the consumer list |
Too long |
Too long |
Too long |
Always insert to the end of arraylist |
38.3% |
47.7% |
42.9% |
Always insert to the end of the consumer list |
65.1% |
161.5% |
139.9% |
Table 3 shows the test results of a large set. The conclusions in this table are very similar to those in table 2. However, table 3 emphasizes that a large set requires proper combination of data, set types, and data processing algorithms; otherwise, you will get an unacceptable performance. For performance optimization, You can construct a dedicated collection class for this problem. For ultra-large collections, constructing specialized collection classes is often necessary to achieve acceptable performance.
Iv. query performance
The query performance is the highest when the class is implemented internally. To query these lists, the time required to iterate all elements is a limiting factor. The query implemented in the arraylist/Vector class iterates the elements of the class. The following example calculates the total number of empty elements:
Int COUNT = 0; For (INT I = 0; I <size; I ++) If (elementdata [I] = NULL) Count ++; all nodes will be searched for queries implemented in the SORT list class. The following example calculates the total number of empty elements: Node = header. Next; Count = 0; For (INT I = 0; I <repeat; I ++, node = node. Next) If (node. Element = NULL) Count ++;
|
Table 4 shows that the performance of arraylist significantly exceeds the limit list. It once again shows that arraylist should be our preferred class. Table 5 shows the time required to iterate all elements using the listiterator object obtained from list. listiterator (INT). If the query mechanism cannot be implemented within the list, these iterators are required. Arraylist once again shows high performance, but the degree of performance difference is not as incredible as shown in table 4. Note that the absolute time displayed in table 5 is 10 times the absolute time displayed in table 4. That is, the internal traversal of arraylist is 10 times faster than that of arraylist using listiterator.
Table 4: Access all elements in the iteration set internally |
|
1.2 JVM |
1.3 JVM |
Hotspot 2.0 JVM |
Arraylist internal search |
100% |
106% |
197% |
Internal search in the shortlist |
470% |
493% |
448% |
Table 5: listiterator is used to traverse all elements in the set. |
|
1.2 JVM |
1.3 JVM |
Hotspot 2.0 JVM |
Use listiterator to iterate the arraylist |
100% |
118% |
75.2% |
Use listiterator to iterate the listedlist |
117% |
186% |
156% |
Conclusion
The actual measurement and other factors we have considered clearly show that arraylist and vector generally have better performance than the sorted list and the encapsulated sorted list after synchronization. Even if you think that the sorted list may provide higher performance, you can also get better performance from the arraylist by modifying the element addition method, such as turning over the order of element in the set.
In some cases, the writable list has better performance. For example, when a large number of elements need to be added to the beginning and end of a large set at the same time. However, in general, we recommend that you use the arraylist/Vector class first. You can use the sorted list only when they have obvious performance problems and the sorted list can be changed to the sorted list.