Java字段优化探究
Kale

在看源码的时候,很多时候可以看到方法中会使用一个局部变量接收实例变量,实际上操作的内存是一致的,在看的时候很奇怪为什么要这样写,了解了一下,这是一种字段访问优化的方式,记录一下。

字段优化举例

字段优化就是将原本对对象字段的访问,替换为一个个对局部变量的访问,例如,在HashMap中,在很多方法中,是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {

transient Node<K,V>[] table;

final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// ...
return null;
}
}

可以看到第8行,用一个局部变量tab接收了实例属性table

分析

下面对字段优化进行一个分析

基本分析

  • 如下图所示,未经过字段优化前,需要通过HashMaoRef访问堆中的HashMapInstance实例,然后通过实例中的table属性访问真正的tableInstance,经过了两次转发的过程,而如果使用字段访问优化,使用一个局部变量接收,就可以通过tab直接访问堆中的tableInstance实例,只经过了一次转发,理论上速度会更快。

实验验证

下面写一个demo,使用jmh进行一个性能测试:

实验设计

  • 有一个DemoKlass类,提供两种方法,都是对数组的循环赋值,但是一个采用字段访问优化,一个不采用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class DemoKlass {

private int[] table1;

private int[] table2;

private int capacity;

public DemoKlass (int capacity) {
this.table1 = new int[capacity];
this.table2 = new int[capacity];
this.capacity = capacity;
}

public void put1 (int val) {
for (int i = 0; i < capacity; i++) {
table1[i] = val;
}
}

public void put2 (int val) {
int[] tab = table2;
int currCapacity = capacity;
for (int i = 0; i < currCapacity; i++) {
tab[i] = val;
}
}
}
  • 有一个实验类ExpreJmh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@Fork(1)
@State(value = Scope.Thread)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class ExpreJmh {

private DemoKlass demoKlass = new DemoKlass(100000);

@Benchmark
public void testPut1 () {
for (int i = 0; i < 100000; i++) {
demoKlass.put1(i);
}
}

@Benchmark
public void testPut2 () {
for (int i = 0; i < 100000; i++) {
demoKlass.put2(i);
}
}

public static void main (String[] args) throws RunnerException {
Options options = new OptionsBuilder().include(ExpreJmh.class.getSimpleName()).build();
new Runner(options).run();
}
}

实验结果

  • 从下图可以看到,经过字段访问优化后,访问的平均速度确实提高了很多,可以看出字段访问优化确实会提升性能

深入分析

接下来看一下DemoKlass类的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
$ javap -c src/main/java/com/kalew515/test/DemoKlass.class   

Compiled from "DemoKlass.java"
public class com.kalew515.test.DemoKlass {
public void put1(int);
Code:
0: iconst_0
1: istore_2
2: iload_2
3: aload_0
4: getfield #4 // Field capacity:I
7: if_icmpge 23
10: aload_0
11: getfield #2 // Field table1:[I
14: iload_2
15: iload_1
16: iastore
17: iinc 2, 1
20: goto 2
23: return

public void put2(int);
Code:
0: aload_0
1: getfield #3 // Field table2:[I
4: astore_2
5: aload_0
6: getfield #4 // Field capacity:I
9: istore_3
10: iconst_0
11: istore 4
13: iload 4
15: iload_3
16: if_icmpge 30
19: aload_2
20: iload 4
22: iload_1
23: iastore
24: iinc 4, 1
27: goto 13
30: return
}
  • 主要看put1()方法和put2()方法,可以看到put2()方法相较于put1()方法在循环中(goto)少了两个字节码getField,那么在多次操作时,就会在性能上产生差异,而付出的代价也仅仅是一个指针占用的内存而已。

结论

当然,即时编译器也可能做出相应的优化。

  • 本文标题:Java字段优化探究
  • 本文作者:Kale
  • 创建时间:2023-04-12 09:13:47
  • 本文链接:https://kalew515.com/2023/04/12/Java字段优化探究/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!