【问题标题】:Performance of Java Enums?Java 枚举的性能?
【发布时间】:2011-12-23 03:55:12
【问题描述】:

我正在实现一个 2 人游戏,它将在一个紧密的循环中运行数十万次,因此性能至关重要。

我的代码实际上是这样的:

public class Table {
    private final int WHITE_PLAYER = +1;
    private final int BLACK_PLAYER = -1;

    private final int currentPlayer;
    private final int otherPlayer;

    ...
}

我想知道如果我会选择替换是否会受到任何性能影响

private final int WHITE_PLAYER = +1;
private final int BLACK_PLAYER = -1;

到一个定义为的枚举

public enum Players {
    WhitePlayer,
    BlackPlayer
}

我的想法是枚举只是整数常量的语法糖,并且仔细查看为测试枚举生成的字节码以及调用它的代码,似乎表明使用它们确实与进行静态方法调用,但对于首次运行时设置的一些枚举基础架构。

我的假设是使用枚举作为静态常量确实是一样的,还是我在这里遗漏了什么?

【问题讨论】:

  • 每什么时间单位几十万次?
  • 好问题。我会在几天内(我估计)24/7 全天候运行游戏,所以它们结束得越快,我就越早分析它们的结果。我恳求人们不要把它变成典型的反对性能调整线程的战争..
  • 如果它认为它真的测量了,我会测量它。
  • 如果你不告诉我们你对枚举做了什么,即使我们想要,我们也无法回答这个问题。
  • @devouredelysium 我不认为有一场针对性能调整的战争。大多数人只是建议不要过早优化。请参阅我的(严重否决)答案,了解枚举比较的速度有多慢(~ 1.5%)。

标签: java performance enums


【解决方案1】:

我特别关心在 switch 语句中使用枚举。我将使用一个程序来计算一个非常长的数组中有限符号集的出现次数。

先定义一些常量,

static final int NUMSYMBOLS = Integer.MAX_VALUE/100; // size of array
// Constants for symbols ZERO ... NINE
static final int ZERO_I =0, ONE_I =1, TWO_I =2, THREE_I =3, FOUR_I =4;
static final int FIVE_I =5, SIX_I =6, SEVEN_I =7, EIGHT_I =8, NINE_I =9;

以及相应的枚举。

enum Symbol {
    ZERO (0), ONE (1), TWO (2), THREE (3), FOUR (4),
    FIVE (5), SIX (6), SEVEN (7), EIGHT (8), NINE (9);
    final int code;
    Symbol(int num) {
        code = num;
    }
    public final int getCode() {
        return code;
    }
}

枚举具有由构造函数设置的字段代码。我们将在测试中使用此代码 稍后,这可以产生一些加速。

符号集合存储在一个数组中,以及一个对应的 int 数组。

Symbol[] symbolArray;
int[] intArray;

符号在方法中进行计数。

    void testEnum() {
        for(int i=0;i<NUMSYMBOLS;++i) {
            Symbol sym = symbolArray[i];
            switch(sym) {
            case ZERO:  ++numZero;  break;
            case ONE:   ++numOne;   break;
            case TWO:   ++numTwo;   break;
            case THREE: ++numThree; break;
            case FOUR:  ++numFour;  break;
            case FIVE:  ++numFive;  break;
            case SIX:   ++numSix;   break;
            case SEVEN: ++numSeven; break;
            case EIGHT: ++numEight; break;
            case NINE:  ++numNine;  break;
            default: break;             
            }
        }
    }

和类似的整数方法。

    void testInteger() {
        for(int i=0;i<NUMSYMBOLS;++i) {
            int num = intArray[i];
            switch(num) {
            case ZERO_I:  ++numZero;  break;
            case ONE_I:   ++numOne;   break;
            case TWO_I:   ++numTwo;   break;
            case THREE_I: ++numThree; break;
            case FOUR_I:  ++numFour;  break;
            case FIVE_I:  ++numFive;  break;
            case SIX_I:   ++numSix;   break;
            case SEVEN_I: ++numSeven; break;
            case EIGHT_I: ++numEight; break;
            case NINE_I:  ++numNine;  break;
            default:
                break;              
            }
        }
    }

我们可以使用 Enum 中的代码来提高切换效率。

    void testEnumCode() {
        for(int i=0;i<NUMSYMBOLS;++i) {
            Symbol sym = symbolArray[i];
            switch(sym.getCode()) {             // Uses the code here
            case ZERO_I:  ++numZero;  break;
            case ONE_I:   ++numOne;   break;
            case TWO_I:   ++numTwo;   break;
            case THREE_I: ++numThree; break;
            case FOUR_I:  ++numFour;  break;
            case FIVE_I:  ++numFive;  break;
            case SIX_I:   ++numSix;   break;
            case SEVEN_I: ++numSeven; break;
            case EIGHT_I: ++numEight; break;
            case NINE_I:  ++numNine;  break;
            default:
                break;              
            }
        }
    }

这三种方法各运行 10 次。给出以下时间。

总计 enum 2,548,251,200ns 代码 2,330,238,900ns int 2,043,553,600ns 百分比 enum 100% code 91% int 80%

显着缩短了使用整数的时间。使用代码字段可以在枚举和整数之间提供中间时间。

这些时间上的差异很容易被周围的代码消失。例如,使用 ArrayList 而不是数组会使时间差异消失 完全地。

使用Enum.ordinal() 方法还有另一种选择。这具有类似于使用 getCode() 的性能。 Is it good practice to use ordinal of enum? 讨论了这种方法的原因和位置。


在我的应用程序中,一个反向波兰计算器,这个循环和 switch 语句,是程序的核心,运行了数百万次,它出现在性能分析中。

枚举用于操作码:PUSH、POP 等,每个命令都包含一个带有附加参数的操作码。

enum OpCode {
   PUSH(0), POP(1), ...;
   private final int code;
   OpCode(int n) { code=n; }
   public int getCode() { return code; }
}

class Command {
    OpCode op;
    int code;
    String var;
    Command (OpCode op,String name) {
      this.op = op;
      this.code = op.getCode();
      this.var = name;
   }
}

构建命令列表可以使用枚举,而不需要知道实际的 int 值。

Command com = new Command(OpCode.PUSH,"x");

对于代码的非关键部分,我们可以在开关中使用枚举。在 Command 的 toString() 方法中说。

public String toString() {
    switch(op) {
    case PUSH:
       return "Push "+var;
    ....
    }
}

但关键部分可以使用代码。

public void evaluate(Command com) {
  switch(com.code) {
  case 0: 
     stack.push(com.var);
     break;
  ....
  }
}

为了那额外的性能。


switch 语句的字节码很有趣。在 int 示例中,swicth 语句编译为:

private void testInteger(int);
Code:
   0: iload_1
   1: tableswitch   { // 0 to 9
                 0: 56
                 1: 69
                 2: 82
                 3: 95
                 4: 108
                 5: 121
                 6: 134
                 7: 147
                 8: 160
                 9: 173
           default: 186
      }
  56: aload_0
  57: dup
  58: getfield      #151                // Field numZero:I
  61: iconst_1
  62: iadd
  63: putfield      #151                // Field numZero:I
  66: goto          186
  69: aload_0
  70: dup
  71: getfield      #153                // Field numOne:I
  74: iconst_1
  75: iadd
  76: putfield      #153                // Field numOne:I
  79: goto          186
  ....

tableswitch 命令根据值有效地在代码中向前跳转。

使用代码(或序数)的开关代码类似。只需额外调用 getCode() 方法。

private void testCode(toys.EnumTest$Symbol);
Code:
   0: aload_1
   1: invokevirtual #186                // Method toys/EnumTest$Symbol.getCode:()I
   4: tableswitch   { // 0 to 9
                 0: 60
                 1: 73
                 2: 86
                 3: 99
                 4: 112
                 5: 125
                 6: 138
                 7: 151
                 8: 164
                 9: 177
           default: 190
   ....

仅使用枚举代码更复杂。

private void testEnum(toys.EnumTest$Symbol);
Code:
   0: invokestatic  #176                
                // Method  $SWITCH_TABLE$toys$EnumTest$Symbol:()[I
   3: aload_1
   4: invokevirtual #179                // Method toys/EnumTest$Symbol.ordinal:()I
   7: iaload
   8: tableswitch   { // 1 to 10
                 1: 64
                 2: 77
                 3: 90
                 4: 103
                 5: 116
                 6: 129
                 7: 142
                 8: 155
                 9: 168
                10: 181
           default: 194
      }

这里首先调用了一个新方法 $SWITCH_TABLE$toys$EnumTest$Symbol:() 此方法创建一个数组,将序数值转换为开关中使用的索引。基本上相当于

int[] lookups = get_SWITCH_TABLE();
int pos = array[sys.ordinal()];
switch(pos) {
    ...
}

切换表创建方法,在第一次调用时计算一次表,并在每次后续调用时使用同一个表。因此,与整数情况相比,我们看到了两个非常简单的函数调用和一个额外的数组查找。

【讨论】:

    【解决方案2】:

    在微基准测试中,是的,检查整数常量相等性将比检查枚举常量相等性更快。

    但是,在实际应用中,更不用说游戏了,这完全无关紧要。 AWT 子系统(或任何其他 GUI 工具包)中发生的事情使这些微观性能考虑相形见绌。

    编辑

    那么让我详细说明一下。

    枚举比较是这样的:

    aload_0
    getstatic
    if_acmpne
    

    一个小整数的整数比较是这样的:

    iload_0
    iconst_1
    if_icmpne
    

    显然,第一个比第二个工作量大,虽然差别很小。

    运行以下测试用例:

    class Test {
    
        static final int ONE = 1;
        static final int TWO = 2;
    
        enum TestEnum {ONE, TWO}
    
        public static void main(String[] args) {
            testEnum();
            testInteger();
            time("enum", new Runnable() {
                public void run() {
                    testEnum();
    
                }
            });
            time("integer", new Runnable() {
                public void run() {
                    testInteger();
                }
            });
        }
    
        private static void testEnum() {
            TestEnum value = TestEnum.ONE;
            for (int i = 0; i < 1000000000; i++) {
                if (value == TestEnum.TWO) {
                    System.err.println("impossible");
                }
            }
        }
    
        private static void testInteger() {
            int value = ONE;
            for (int i = 0; i < 1000000000; i++) {
                if (value == TWO) {
                    System.err.println("impossible");
                }
            }
        }
    
        private static void time(String name, Runnable runnable) {
            long startTime = System.currentTimeMillis();
            runnable.run();
            System.err.println(name + ": " + (System.currentTimeMillis() - startTime) + " ms");
        }
    }
    

    你会发现枚举比较比整数比较慢,在我的机器上大约慢了 1.5%。

    我要说的是,这种差异在实际应用中并不重要(“过早的优化是万恶之源”)。我以专业的方式处理性能问题(请参阅我的个人资料),我从未见过可以追溯到这样的热点。

    【讨论】:

    • "然而,在真正的应用程序中,更不用说游戏,这将是完全不相关的。AWT 子系统(或任何其他 GUI 工具包)中发生的事情使这些微观性能考虑相形见绌好几个数量级。”所以你假设确实有这个游戏的GUI?完全错误!
    • 这个基准可能使枚举看起来比实际上更好,因为它的位置。如果枚举访问导致 TLB 未命中,我想看看它的执行速度有多快。我认为这是一个更真实的世界测试。我也很惊讶优化器没有消除这些循环。
    【解决方案3】:

    你的假设是正确的。 Java 确保枚举只有一个实例,因此 == 与比较整数一样有效。

    【讨论】:

    • @IngoKegel 你还没有回答这个问题。我的回答在哪些方面不正确?
    • "== 与比较整数一样有效"
    【解决方案4】:

    JIT 会优化很多东西,让这样的事情在运行一段时间后变得无关紧要

    更不用说枚举更易读也更万无一失,如果您在代码中出错

    【讨论】:

    • 会优化什么?
    【解决方案5】:

    在关心性能之前,您应该关心是否拥有漂亮且可读的代码。在您的分析结果(不要猜测!)表明枚举是瓶颈之前,请忘记性能并使用更容易理解的东西。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-03-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多