a1188 发表于 2018-4-4 15:49

技术分享-请不要再说 Java 中 final 方法比非 final 性能更好了

本帖最后由 a1188 于 2018-4-4 15:53 编辑

无继承


有 static 修饰

static final

// 生成随机数字和字母,
public static final String getStringRandomFinal(int length) {
    String val = "";
    Random random = new Random();
    // 参数length,表示生成几位随机数
    for (int i = 0; i < length; i++) {
      String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
      // 输出字母还是数字
      if ("char".equalsIgnoreCase(charOrNum)) {
            // 输出是大写字母还是小写字母
            // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
            val += (char) (random.nextInt(26) + 97);
      } else if ("num".equalsIgnoreCase(charOrNum)) {
            val += String.valueOf(random.nextInt(10));
      }
    }
    return val;
}


static 非 final


// 生成随机数字和字母,
public static String getStringRandom(int length) {
    String val = "";
    Random random = new Random();
    // 参数length,表示生成几位随机数
    for (int i = 0; i < length; i++) {
      String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
      // 输出字母还是数字
      if ("char".equalsIgnoreCase(charOrNum)) {
            // 输出是大写字母还是小写字母
            // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
            val += (char) (random.nextInt(26) + 97);
      } else if ("num".equalsIgnoreCase(charOrNum)) {
            val += String.valueOf(random.nextInt(10));
      }
    }
    return val;
}


结果


这里使用了 OpenJDK 的 JMH 基准测试工具来测试的,结果如下:

# JMH 1.4.1 (released 903 days ago, please consider updating!)
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmark
中间忽略了预热及测试过程,这里只显示结果
Result: 206924.113 ±(99.9%) 7746.446 ops/s
Statistics: (min, avg, max) = (132107.466, 206924.113, 267265.397), stdev = 32798.937
Confidence interval (99.9%):
# JMH 1.4.1 (released 903 days ago, please consider updating!)
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmarkFinal
中间忽略了预热及测试过程,这里只显示结果
Result: 210111.568 ±(99.9%) 8486.176 ops/s
Statistics: (min, avg, max) = (133813.368, 210111.568, 267525.228), stdev = 35931.001
Confidence interval (99.9%):

# Run complete. Total time: 00:13:54
Benchmark                     ModeSamples       Score      ErrorUnits
o.a.s.j.Main.benchmark         thrpt      200206924.113 ± 7746.446ops/s
o.a.s.j.Main.benchmarkFinal    thrpt      200210111.568 ± 8486.176ops/s


总结:你说final的性能比非final有没有提升呢?可以说有,但几乎可以忽略不计。如果单纯地追求性能,而将所有的方法修改为 final 的话,我认为这样子是不可取的。
而且这性能的差别,远远也没有网上有些人说的提升 50% 这么恐怖(有可能他们使用的是10年前的JVM来测试的吧^_^,比如 《35+ 个 Java 代码性能优化总结》这篇文章。雷总:不服?咱们来跑个分!)

分析
字节码级别的差别


StringKit.java
StringKitFinal.java


它们在字节码上的差别:

emacsist:target $ diff /tmp/stringkit.log /tmp/stringkit-final.log
1,5c1,5
< Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKit.class
<   Last modified 2017-6-15; size 1098 bytes
<   MD5 checksum fe1ccdde26107e4037afc54c780f2c95
<   Compiled from "StringKit.java"
< public class org.agoncal.sample.jmh.StringKit
---
> Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKitFinal.class
>   Last modified 2017-6-15; size 1118 bytes
>   MD5 checksum 410f8bf0eb723b794e4754c6eb8b9829
>   Compiled from "StringKitFinal.java"
> public class org.agoncal.sample.jmh.StringKitFinal
24c24
<   #15 = Class            #52            // org/agoncal/sample/jmh/StringKit
---
>   #15 = Class            #52            // org/agoncal/sample/jmh/StringKitFinal
32,33c32,33
<   #23 = Utf8               Lorg/agoncal/sample/jmh/StringKit;
<   #24 = Utf8               getStringRandom
---
>   #23 = Utf8               Lorg/agoncal/sample/jmh/StringKitFinal;
>   #24 = Utf8               getStringRandomFinal
47c47
<   #38 = Utf8               StringKit.java
---
>   #38 = Utf8               StringKitFinal.java
61c61
<   #52 = Utf8               org/agoncal/sample/jmh/StringKit
---
>   #52 = Utf8               org/agoncal/sample/jmh/StringKitFinal
75c75
<   public org.agoncal.sample.jmh.StringKit();
---
>   public org.agoncal.sample.jmh.StringKitFinal();
87c87
<             0       5   0this   Lorg/agoncal/sample/jmh/StringKit;
---
>             0       5   0this   Lorg/agoncal/sample/jmh/StringKitFinal;
89c89
<   public static java.lang.String getStringRandom(int);
---
>   public static final java.lang.String getStringRandomFinal(int);
91c91
<   flags: ACC_PUBLIC, ACC_STATIC
---
>   flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
187c187
< SourceFile: "StringKit.java"
---
> SourceFile: "StringKitFinal.java"

可以看到除了方法名和方法修饰符不同之外,其他的没有什么区别了。

在调用者上面的字节码差别

public void benchmark();
descriptor: ()V
flags: ACC_PUBLIC
Code:
    stack=1, locals=1, args_size=1
       0: bipush      32
       2: invokestatic#2                  // Method org/agoncal/sample/jmh/StringKit.getStringRandom:(I)Ljava/lang/String;
       5: pop
       6: return
    LineNumberTable:
      line 21: 0
      line 22: 6
    LocalVariableTable:
      StartLengthSlotName   Signature
          0       7   0this   Lorg/agoncal/sample/jmh/Main;
RuntimeVisibleAnnotations:
    0: #26()
public void benchmarkFinal();
descriptor: ()V
flags: ACC_PUBLIC
Code:
    stack=1, locals=1, args_size=1
       0: bipush      32
       2: invokestatic#3                  // Method org/agoncal/sample/jmh/StringKitFinal.getStringRandomFinal:(I)Ljava/lang/String;
       5: pop
       6: return
    LineNumberTable:
      line 26: 0
      line 27: 6
    LocalVariableTable:
      StartLengthSlotName   Signature
          0       7   0this   Lorg/agoncal/sample/jmh/Main;
RuntimeVisibleAnnotations:
    0: #26()

可以看到,它们在调用者上面的字节码也没有什么区别,只是方法名不一样之外。


对于 JVM 来说,它是只认字节码的,既然字节码除了方法名和修饰符一样,其他都一样,那就可以大概推测它们的性能几乎可以忽略不计了。因为调用 static final 和 static 非 final 的JVM指令是一样。

无 static 修饰
方法体是一样的,只是将它们删除了 static 的修饰。

结果
# JMH version: 1.19
# VM version: JDK 1.8.0_92, VM 25.92-b14
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmark
中间忽略了预热及测试过程,这里只显示结果
Result "org.agoncal.sample.jmh.Main.benchmark":
201306.770 ±(99.9%) 8184.423 ops/s
(min, avg, max) = (131889.934, 201306.770, 259928.172), stdev = 34653.361
CI (99.9%): (assumes normal distribution)
# JMH version: 1.19
# VM version: JDK 1.8.0_92, VM 25.92-b14
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmarkFinal
中间忽略了预热及测试过程,这里只显示结果
Result "org.agoncal.sample.jmh.Main.benchmarkFinal":
196871.022 ±(99.9%) 8595.719 ops/s
(min, avg, max) = (131182.268, 196871.022, 265522.769), stdev = 36394.814
CI (99.9%): (assumes normal distribution)

# Run complete. Total time: 00:13:35
Benchmark             ModeCnt       Score      ErrorUnits
Main.benchmark       thrpt200201306.770 ± 8184.423ops/s
Main.benchmarkFinalthrpt200196871.022 ± 8595.719ops/s

分析


字节码级别的差别

emacsist:target $ diff /tmp/stringkit.log /tmp/stringkit-final.log
1,5c1,5
< Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKit.class
<   Last modified 2017-6-15; size 1110 bytes
<   MD5 checksum f61144e86f7c17dc5d5f2b2d35fac36d
<   Compiled from "StringKit.java"
< public class org.agoncal.sample.jmh.StringKit
---
> Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKitFinal.class
>   Last modified 2017-6-15; size 1130 bytes
>   MD5 checksum 15ce17ee17fdb5f4721f0921977b1e69
>   Compiled from "StringKitFinal.java"
> public class org.agoncal.sample.jmh.StringKitFinal
24c24
<   #15 = Class            #52            // org/agoncal/sample/jmh/StringKit
---
>   #15 = Class            #52            // org/agoncal/sample/jmh/StringKitFinal
32,33c32,33
<   #23 = Utf8               Lorg/agoncal/sample/jmh/StringKit;
<   #24 = Utf8               getStringRandom
---
>   #23 = Utf8               Lorg/agoncal/sample/jmh/StringKitFinal;
>   #24 = Utf8               getStringRandomFinal
47c47
<   #38 = Utf8               StringKit.java
---
>   #38 = Utf8               StringKitFinal.java
61c61
<   #52 = Utf8               org/agoncal/sample/jmh/StringKit
---
>   #52 = Utf8               org/agoncal/sample/jmh/StringKitFinal
75c75
<   public org.agoncal.sample.jmh.StringKit();
---
>   public org.agoncal.sample.jmh.StringKitFinal();
87c87
<             0       5   0this   Lorg/agoncal/sample/jmh/StringKit;
---
>             0       5   0this   Lorg/agoncal/sample/jmh/StringKitFinal;
89c89
<   public java.lang.String getStringRandom(int);
---
>   public final java.lang.String getStringRandomFinal(int);
91c91
<   flags: ACC_PUBLIC
---
>   flags: ACC_PUBLIC, ACC_FINAL
169c169
<             0   125   0this   Lorg/agoncal/sample/jmh/StringKit;
---
>             0   125   0this   Lorg/agoncal/sample/jmh/StringKitFinal;
188c188
< SourceFile: "StringKit.java"
---
> SourceFile: "StringKitFinal.java"

可以看到,字节码上除了名字和 final 修饰符差别外,其余的是一样的。


在调用者上面的字节码差别

public void benchmark();
descriptor: ()V
flags: ACC_PUBLIC
Code:
    stack=2, locals=1, args_size=1
       0: new         #2                  // class org/agoncal/sample/jmh/StringKit
       3: dup
       4: invokespecial #3                  // Method org/agoncal/sample/jmh/StringKit."<init>":()V
       7: bipush      32
       9: invokevirtual #4                  // Method org/agoncal/sample/jmh/StringKit.getStringRandom:(I)Ljava/lang/String;
      12: pop
      13: return
    LineNumberTable:
      line 21: 0
      line 22: 13
    LocalVariableTable:
      StartLengthSlotName   Signature
          0      14   0this   Lorg/agoncal/sample/jmh/Main;
RuntimeVisibleAnnotations:
    0: #30()
public void benchmarkFinal();
descriptor: ()V
flags: ACC_PUBLIC
Code:
    stack=2, locals=1, args_size=1
       0: new         #5                  // class org/agoncal/sample/jmh/StringKitFinal
       3: dup
       4: invokespecial #6                  // Method org/agoncal/sample/jmh/StringKitFinal."<init>":()V
       7: bipush      32
       9: invokevirtual #7                  // Method org/agoncal/sample/jmh/StringKitFinal.getStringRandomFinal:(I)Ljava/lang/String;
      12: pop
      13: return
    LineNumberTable:
      line 26: 0
      line 27: 13
    LocalVariableTable:
      StartLengthSlotName   Signature
          0      14   0this   Lorg/agoncal/sample/jmh/Main;
RuntimeVisibleAnnotations:
    0: #30()

可以看到,它们除了名字不同之外,其他的JVM指令都是一样的。

总结
对于是否有 final 修饰的方法,对性能的影响可以忽略不计。因为它们生成的字节码除了 flags 标志位是否有 final 修饰不同之外,其他所有的JVM指令,都是一样的(对于方法本身,以及调用者本身的字节码都一样)。


对于JVM来说,它执行的就是字节码,如果字节码都一样的话,那对于JVM来说,它就是同一样东西的了。

有继承


无 final 修饰

package org.agoncal.sample.jmh;
import java.util.Random;
/**
* Created by emacsist on 2017/6/15.
*/
public abstract class StringKitAbs {
    // 生成随机数字和字母,
    public String getStringRandom(int length) {
      String val = "";
      Random random = new Random();
      // 参数length,表示生成几位随机数
      for (int i = 0; i < length; i++) {
            String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
            // 输出字母还是数字
            if ("char".equalsIgnoreCase(charOrNum)) {
                // 输出是大写字母还是小写字母
                // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
                val += (char) (random.nextInt(26) + 97);
            } else if ("num".equalsIgnoreCase(charOrNum)) {
                val += String.valueOf(random.nextInt(10));
            }
      }
      return val;
    }
}

有 final 修饰

package org.agoncal.sample.jmh;
import java.util.Random;
/**
* Created by emacsist on 2017/6/15.
*/
public abstract class StringKitAbsFinal {
    // 生成随机数字和字母,
    public final String getStringRandomFinal(int length) {
      String val = "";
      Random random = new Random();
      // 参数length,表示生成几位随机数
      for (int i = 0; i < length; i++) {
            String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
            // 输出字母还是数字
            if ("char".equalsIgnoreCase(charOrNum)) {
                // 输出是大写字母还是小写字母
                // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
                val += (char) (random.nextInt(26) + 97);
            } else if ("num".equalsIgnoreCase(charOrNum)) {
                val += String.valueOf(random.nextInt(10));
            }
      }
      return val;
    }
}

测试代码

写一个类来继承上面的抽象类,以此来测试在继承中 final 有否对多态中的影响

package org.agoncal.sample.jmh;
/**
* Created by emacsist on 2017/6/15.
*/
public class StringKitFinal extends StringKitAbsFinal {
}
package org.agoncal.sample.jmh;
/**
* Created by emacsist on 2017/6/15.
*/
public class StringKit extends StringKitAbs {
}

然后在基准测试中:

@Benchmark
public void benchmark() {
    new StringKit().getStringRandom(32);
}
@Benchmark
public void benchmarkFinal() {
    new StringKitFinal().getStringRandomFinal(32);
}

测试结果

非 final 结果

# JMH version: 1.19
# VM version: JDK 1.8.0_92, VM 25.92-b14
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmark
中间忽略了预热及测试过程
Result "org.agoncal.sample.jmh.Main.benchmark":
213462.677 ±(99.9%) 8670.164 ops/s
(min, avg, max) = (135751.428, 213462.677, 264182.887), stdev = 36710.017
CI (99.9%): (assumes normal distribution)


有 final 结果


# JMH version: 1.19
# VM version: JDK 1.8.0_92, VM 25.92-b14
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmarkFinal
中间忽略了预热及测试过程
Result "org.agoncal.sample.jmh.Main.benchmarkFinal":
213684.585 ±(99.9%) 8571.512 ops/s
(min, avg, max) = (133472.162, 213684.585, 267742.236), stdev = 36292.318
CI (99.9%): (assumes normal distribution)

总对比

# Run complete. Total time: 00:13:35
Benchmark             ModeCnt       Score      ErrorUnits
Main.benchmark       thrpt200213462.677 ± 8670.164ops/s
Main.benchmarkFinalthrpt200213684.585 ± 8571.512ops/s


它们字节码的区别




emacsist:classes $ diff /tmp/StringKit.log /tmp/StringKitFinal.log
1,5c1,5
< Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKit.class
<   Last modified 2017-6-16; size 317 bytes
<   MD5 checksum 7f9b024adc7f39345215e3e8490cafe4
<   Compiled from "StringKit.java"
< public class org.agoncal.sample.jmh.StringKit extends org.agoncal.sample.jmh.StringKitAbs
---
> Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKitFinal.class
>   Last modified 2017-6-16; size 337 bytes
>   MD5 checksum f54eadc79a90675d97e95f766ef88a87
>   Compiled from "StringKitFinal.java"
> public class org.agoncal.sample.jmh.StringKitFinal extends org.agoncal.sample.jmh.StringKitAbsFinal
10,12c10,12
<    #1 = Methodref          #3.#13         // org/agoncal/sample/jmh/StringKitAbs."<init>":()V
<    #2 = Class            #14            // org/agoncal/sample/jmh/StringKit
<    #3 = Class            #15            // org/agoncal/sample/jmh/StringKitAbs
---
>    #1 = Methodref          #3.#13         // org/agoncal/sample/jmh/StringKitAbsFinal."<init>":()V
>    #2 = Class            #14            // org/agoncal/sample/jmh/StringKitFinal
>    #3 = Class            #15            // org/agoncal/sample/jmh/StringKitAbsFinal
19c19
<   #10 = Utf8               Lorg/agoncal/sample/jmh/StringKit;
---
>   #10 = Utf8               Lorg/agoncal/sample/jmh/StringKitFinal;
21c21
<   #12 = Utf8               StringKit.java
---
>   #12 = Utf8               StringKitFinal.java
23,24c23,24
<   #14 = Utf8               org/agoncal/sample/jmh/StringKit
<   #15 = Utf8               org/agoncal/sample/jmh/StringKitAbs
---
>   #14 = Utf8               org/agoncal/sample/jmh/StringKitFinal
>   #15 = Utf8               org/agoncal/sample/jmh/StringKitAbsFinal
26c26
<   public org.agoncal.sample.jmh.StringKit();
---
>   public org.agoncal.sample.jmh.StringKitFinal();
32c32
<          1: invokespecial #1                  // Method org/agoncal/sample/jmh/StringKitAbs."<init>":()V
---
>          1: invokespecial #1                  // Method org/agoncal/sample/jmh/StringKitAbsFinal."<init>":()V
38c38
<             0       5   0this   Lorg/agoncal/sample/jmh/StringKit;
---
>             0       5   0this   Lorg/agoncal/sample/jmh/StringKitFinal;
40c40
< SourceFile: "StringKit.java"
---
> SourceFile: "StringKitFinal.java"


可以看到,除了它们的方法签名和方法名字不同之外其他的都是一样的,包括JVM调用指令也完全是一样的。

总结
可以看到它们几乎是一样的。

总结
基于上面的基准测试结论,我认为滥用或刻意为了所谓的提升性能,而去为每一个方法尽可能添加 final 的关键字是不可取的。使用 final ,更多的应该是根据Java对 final 的语义来定义,而不是只想着为了提升性能(而且这影响可以忽略不计)而刻意用 final.


使用 final 的情况:


final 变量: 表示只读(只初始化一次,但可多次读取)
final 方法:表示子类不可以重写。(网上认为 final 比非 final 快,就是认为它是在编译的时候已经静态绑定了,不需要在运行时再动态绑定。这个可能以前的JVM上是正确的,但在现代的JVM上,这个可以认为没什么影响,至少我在基准测试里是这样子)
final 类: 它们不能被继承,而且final类的方法,默认也是 final 的。


观众老爷 ~ 免费评分送一送多谢~

WqiancangQ 发表于 2018-4-4 16:00

膜拜大神,但是你能把字体颜色修改下吗、这颜色看着真心累,还看不清楚!!!!

5yrant 发表于 2018-6-26 17:47

略微了解了一下JVM,发现以前写的代码,有些很简单的代码,比如说for循环对String 类型的+=操作,虽然自己写得爽,却给JVM造成了不必要的负担{:1_907:}

edm233 发表于 2018-4-4 17:18

大佬说得对

54854 发表于 2018-4-4 18:16

    看不懂,,还是需要学习

PASSER-BY 发表于 2018-4-4 23:23

厉害,新手路过,膜拜一下。

杨大善人 发表于 2018-4-5 00:17

弱弱的问一句,是哪里在讨论这个问题才让你想发帖纠正我咋没看到

bookaccount 发表于 2018-4-5 08:43

这些修饰符本来就不是为性能设计的

憨厚小猪 发表于 2018-4-5 16:31

xoxoixo 发表于 2018-4-8 21:42

新人围观

dopimking 发表于 2018-5-18 18:50

虽然一句代码没看,但是不影响我看lz一本正经的bb,顶起!
页: [1] 2
查看完整版本: 技术分享-请不要再说 Java 中 final 方法比非 final 性能更好了