Java 枚举的性能?

发布于 2024-12-14 00:05:39 字数 669 浏览 3 评论 0原文

我正在实现一个 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
}

我认为枚举只是整数常量的语法糖,并且对字节码进行了简单的观察, 我是否会受到任何性能影响为测试枚举生成的以及调用它的代码似乎表明使用它们确实与进行静态方法调用相同,但对于首次运行时设置的某些枚举基础结构而言。

我的假设是使用枚举作为静态常量确实是相同的,还是正确的,还是我在这里遗漏了一些东西?

I am implementing a 2-player game that will be run in a tight loop literally hundreds of thousands of times, being then performance paramount.

My code actually looks something like this:

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

    private final int currentPlayer;
    private final int otherPlayer;

    ...
}

I was wondering if I would get any performance hit would I choose to replace

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

to an enum defined as

public enum Players {
    WhitePlayer,
    BlackPlayer
}

I had the idea that enums were just syntactic sugar over integer constants, and taking a glaze look over the bytecode generated for a test enum, as well as the code calling it, seems to indicate that using them is indeed the same as making a static method call but for some enum infrastructure that is set up when it's first run.

Is my assumption that it is indeed the same to use enums as static constants correct or am I missing something here?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

攀登最高峰 2024-12-21 00:05:39

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

然而,在真实的应用程序中,更不用说游戏了,这完全无关紧要。 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%。

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

In a micro-benchmark, yes, checking integer constant equality will be faster than checking enum constant equality.

However, in a real application, let alone a game, this will be totally irrelevant. The things that are happening in the AWT subsystem (or any other GUI toolkit) dwarf these micro-performance considerations by many orders of magnitude.

EDIT

Let me elaborate a little then.

An enum comparison goes like this:

aload_0
getstatic
if_acmpne

An integer comparison for a small integer goes like this:

iload_0
iconst_1
if_icmpne

Obviously, the first is more work than the second, although the difference is quite small.

Run the following test case:

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");
    }
}

and you will find that the enum comparison is slower that the integer comparison, on my machine by around 1.5%.

All I was saying is that this difference will not matter in a real application ("Premature optimization is the root of all evil"). I deal with performance problems on a professional basis (see my profile) and I have never seen a hot spot that could be traced to something like this.

も让我眼熟你 2024-12-21 00:05:39

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

You should care about having nice and readable code before you care about performance. Until your profiling results (no guessing!) show that enums are the bottleneck, forget about performance and use whatever is easier to understand.

青衫负雪 2024-12-21 00:05:39

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

更不用说如果你在代码中犯了错误,枚举会更具可读性并且更加万无一失

JIT will optimize a lot of things making things like this irrelevant after it's been running for a while

not to mention enums are more readable and more foolproof should you make an error in your code

前事休说 2024-12-21 00:05:39

我特别关心在 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() 的性能类似。此方法依赖的原因和原因在中讨论使用枚举序数的好习惯?


在我的应用程序中,反向波兰计算器(这个循环和 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) {
    ...
}

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

I'm particularly concerned with using enums in switch statements. I'll be using a program to count the number of occurrences of a finite set of symbols in a very long array.

First define some constants,

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;

and a corresponding enum.

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;
    }
}

The enum has a field code set by a constructor. We will use this code in our testing
later, which can yield some speed-up.

The set of symbols is stored in an array, and a corresponding int array.

Symbol[] symbolArray;
int[] intArray;

The symbols are counted in a method.

    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;             
            }
        }
    }

and similar method for integers.

    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;              
            }
        }
    }

We can use the code from the Enum to make the switch a little more efficient.

    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;              
            }
        }
    }

Running the three methods 10 time each. Gives the following timings.

Totals enum 2,548,251,200ns code 2,330,238,900ns int 2,043,553,600ns
Percentages enum 100% code 91% int 80%

Giving a noticeable time improvement for using integers. Using the code field gives timing half-way between enums and ints.

These difference in timing can easily disappear by the surrounding code. For instance of using an ArrayList rather than an array makes the timings difference vanish
completely.

There is another option in using the Enum.ordinal() method. This has perfomance similar to using a getCode(). The why and wherfore of this methods depend on are discussed at Is it good practice to use ordinal of enum?.


In my application, a reverse polish calculator, this loop and switch statement, is the heart of the program, run millions of times, and it comes up in performance analysis.

There enums are used for opcodes: PUSH, POP, etc. and each command consist of an opcode with additional arguments.

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;
   }
}

Building the list of commands can use the enum, without needing to know about the actual int values.

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

For non critical parts of the code we can use the enum in a switch. Say in the toString() method of the Command.

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

But critical parts can use the code.

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

for that extra bit of performance.


The byte code of the switch statements are interesting. In the int examples the swicth statment compiles to:

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
  ....

The tableswitch command efficiently jumps forward in the code depending on the value.

The code for the switch using the code (or ordinal) is similar. Just with an extra call to the getCode() method.

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
   ....

Using just the enum the code is more complex.

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
      }

Here there is first a call to a new method $SWITCH_TABLE$toys$EnumTest$Symbol:()
this method creates an array translating the ordinal values to an index used in the switch. Basically its equivalent to

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

The switch table creation method, calculates the table once on its first call, and uses the same table on each subsequent call. So we see two quite trivial function calls and one extra array lookup when compared to the integer case.

梦幻之岛 2024-12-21 00:05:39

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

Your assumption is correct. Java makes sure there is only ever one instance of the enum so that == is as efficient as comparing ints.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文