0

I need to have enum constants with byte values which can be used in a super performant sorting system. However this presents a problem when I need to get an enum from the corresponding byte value. IE fromValue().

I am wondering if the following approach of using a map of Byte values to constants is considered a bad idea when I want something that is highly optimized, or if I should just stick with static constants. What I am trying to avoid is looping through the enum values to find the correct one at runtime, which I believe would add un-needed overhead when doing millions of these operations.

public enum ReferenceTargetType {
  BINARY((byte)0x1),
  TOPIC((byte)0x2),
  MAP((byte)0x3),
  UNKNOWN((byte)0x4);

  private static Map<Byte,ReferenceTargetType> targetTypeMap = new HashMap<Byte,ReferenceTargetType>();

  static {
    for(ReferenceTargetType type : ReferenceTargetType.values()){
      targetTypeMap.put(type.getValue(), type);
    }
  }

  private byte value;

  ReferenceTargetType(byte value){
    this.value = value;
  }

  byte getValue(){
    return this.value;
  }

  static ReferenceTargetType fromValue(byte value){
    return targetTypeMap.get(value);
  }
}

Thanks

UPDATE

I created some tests to look at performance of various methods. The first method uses a hashmap, the second by looping through values, the third array offsets, and the fourth array offsets with ints instead of bytes (To see if up casting from byte to int has a performance impact), the fifth uses a switch.

Averages are over 100 runs with each run doing a 100 million fromValue() calls each. Times are in ms (I changed this from nanotime because it was blowing up on me for the larger values).

Here are the results:

  • Run 1 average: 385 (HashMap lookup)
  • Run 2 average: 914 (Looping through values)
  • Run 3 average: 0 (Array (byte))
  • Run 4 average: 0 (Array (int))
  • Run 5 average: 314 (Switch)

and the code:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.HashMap;
import java.util.Map;

@RunWith(JUnit4.class)
public class EnumFromValueTest {

  static int masterRuns = 100;
  static int runs = 100000000;
  static long[] r1runs = new long[masterRuns];
  static long[] r2runs = new long[masterRuns];
  static long[] r3runs = new long[masterRuns];
  static long[] r4runs = new long[masterRuns];
  static long[] r5runs = new long[masterRuns];

  static long average(long[] values){

      int total = 0;
      for(int i = 0; i < values.length; i++)
      {
        total += values[i];
      }
      int average = total / values.length;

    return average;

  }

  public enum ReferenceTargetType1 {
    BINARY((byte)0x0),
    TOPIC((byte)0x1),
    MAP((byte)0x2),
    UNKNOWN((byte)0x3);

    private static
    Map<Byte,ReferenceTargetType1>
        targetTypeMap = new HashMap<Byte, ReferenceTargetType1>();

    static {
      for(ReferenceTargetType1 type : ReferenceTargetType1.values()){
        targetTypeMap.put(type.getValue(), type);
      }
    }

    private byte value;

    ReferenceTargetType1(byte value){
      this.value = value;
    }

    byte getValue(){
      return this.value;
    }

    static ReferenceTargetType1 fromValue(byte value){

      return targetTypeMap.get(value);
    }

  }

  public enum ReferenceTargetType2 {
    BINARY((byte)0x0),
    TOPIC((byte)0x1),
    MAP((byte)0x2),
    UNKNOWN((byte)0x3);

    private byte value;

    ReferenceTargetType2(byte value){
      this.value = value;
    }

    byte getValue(){
      return this.value;
    }

    static ReferenceTargetType2 fromValue(byte value){
      for(ReferenceTargetType2 type : ReferenceTargetType2.values()){
        if(type.getValue() == value)
          return type;
      }

      return null;
    }

  }

  public enum ReferenceTargetType3 {
    BINARY((byte)0x0),
    TOPIC((byte)0x1),
    MAP((byte)0x2),
    UNKNOWN((byte)0x3);

    private byte value;

    private static ReferenceTargetType3[] values = new ReferenceTargetType3[ReferenceTargetType3.values().length];
    static {
      int i = 0;
      for(ReferenceTargetType3 type : ReferenceTargetType3.values()){
       values[i]= type;
        i++;
      }
    }
    ReferenceTargetType3(byte value){
      this.value = value;
    }

    byte getValue(){
      return this.value;
    }

    static ReferenceTargetType3 fromValue(byte value){
      return values[value];
    }

  }




  public enum ReferenceTargetType4 {
    BINARY(0),
    TOPIC(1),
    MAP(2),
    UNKNOWN(3);

    private int value;

    private static ReferenceTargetType4[] values = new ReferenceTargetType4[ReferenceTargetType4.values().length];
    static {

        int i = 0;
        for(ReferenceTargetType4 type : ReferenceTargetType4.values()){
          values[i]= type;
          i++;
        }

    }
    ReferenceTargetType4(int value){
      this.value = value;
    }

    int getValue(){
      return this.value;
    }

    static ReferenceTargetType4 fromValue(int value){
      return values[value];
    }

  }

  public enum ReferenceTargetType5 {
    BINARY((byte)0x0),
    TOPIC((byte)0x1),
    MAP((byte)0x2),
    UNKNOWN((byte)0x3);

    private byte value;

    ReferenceTargetType5(byte value){
      this.value = value;
    }

    byte getValue(){
      return this.value;
    }

    static ReferenceTargetType5 fromValue(byte value) {
      switch (value) {
        case 0x0: return BINARY;
        case 0x1: return TOPIC;
        case 0x2: return BINARY;
        case 0x3: return UNKNOWN;
        default:  return UNKNOWN;
      }
    }

  }

  @Test
  public void doPerformanceTest(){

    for(int i = 0; i < masterRuns;i++){
      doRuns(i);
    }

    System.out.println("Run 1 average: " + average(r1runs));
    System.out.println("Run 2 average: " + average(r2runs));
    System.out.println("Run 3 average: " + average(r3runs));
    System.out.println("Run 4 average: " + average(r4runs));
    System.out.println("Run 5 average: " + average(r5runs));
  }

  public void doRuns(int runnum){

    ReferenceTargetType1 type1 = ReferenceTargetType1.UNKNOWN;
    ReferenceTargetType2 type2 = ReferenceTargetType2.UNKNOWN;
    ReferenceTargetType3 type3 = ReferenceTargetType3.UNKNOWN;
    ReferenceTargetType4 type4 = ReferenceTargetType4.UNKNOWN;
    ReferenceTargetType5 type5 = ReferenceTargetType5.UNKNOWN;

    long startTime1 = System.currentTimeMillis();

    for(int i = 0; i < runs;i++){
      ReferenceTargetType1.fromValue(type1.getValue());
    }
    r1runs[runnum] = (System.currentTimeMillis() - startTime1);

    long startTime2 = System.currentTimeMillis();

    for(int i = 0; i < runs;i++){
      ReferenceTargetType2.fromValue(type2.getValue());
    }
    r2runs[runnum] = (System.currentTimeMillis() - startTime2);

    long startTime3 = System.currentTimeMillis();

    for(int i = 0; i < runs;i++){
      ReferenceTargetType3.fromValue(type3.getValue());
    }

    r3runs[runnum] = (System.currentTimeMillis() - startTime3);

    long startTime4 = System.currentTimeMillis();

    for(int i = 0; i < runs;i++){
      ReferenceTargetType4.fromValue(type4.getValue());
    }

    r4runs[runnum] = (System.currentTimeMillis() - startTime4);

    long startTime5 = System.currentTimeMillis();

    for(int i = 0; i < runs;i++){
      ReferenceTargetType5.fromValue(type5.getValue());
    }

    r5runs[runnum] = (System.currentTimeMillis() - startTime5);


  }
}
Casey Jordan
  • 1,204
  • 1
  • 20
  • 39
  • 1
    Yeah, using a `ByteBuffer` as a key is virtually guaranteed to cause problems. Why not just a `Map`--or even, since it's so small, a `ReferenceTargetType[256]`? – chrylis -cautiouslyoptimistic- Dec 12 '14 at 04:07
  • Good suggestion, I'll update the example to use Byte. I am not sure I understand how your second suggestion would work in my case? – Casey Jordan Dec 12 '14 at 04:12
  • 2
    If the key is a `byte`, then you can just use that as the index into an array (with a suitable offset), since the universe of possible keys is very small. – chrylis -cautiouslyoptimistic- Dec 12 '14 at 04:19
  • If optimization is really that important, seriously consider not using enums. In fact, consider not using Java. This is what JNI is for – etherous Dec 12 '14 at 04:57
  • @etherous, well the reason I am using an enum here is that I am going to expose this value in some API's and I would really prefer not to just pass a primitive back to the user, although this would probably be more efficient. That being said, I need this to be reasonably optimized, I think going to JNI would be premature at this point without further testing. It is food for thought though, thanks! – Casey Jordan Dec 12 '14 at 05:01
  • I added performance tests. chrylis your suggestion indeed seems pretty performant. Thanks. – Casey Jordan Dec 12 '14 at 05:12
  • @etherous Please no JNI. The number of cases where it can speed things up is rather small and all of them are pretty complicated computations using things inaccessible from Java (e.g., AES-NI) or some low level hacks. The JNI overhead is orders of magnitude higher than what this is about. – maaartinus Dec 12 '14 at 08:41
  • @CaseyJordan You aren't initializing the arrays in test 3 and 4 – kapex Dec 12 '14 at 13:06
  • Gah, good point. It was late when I wrote those. Fixing now... – Casey Jordan Dec 12 '14 at 15:03
  • Updated, and I added a test for switch. Unless my tests are still wrong, it's pretty clear that array is the best way to go by far. When I timed these in nanoseconds the array versions came in around 300-500ns, both byte and int versions were pretty much comparable. – Casey Jordan Dec 12 '14 at 15:32

2 Answers2

2

Obviously, an array is the fastest solution. Something like

private final static ReferenceTargetType TYPES = ReferenceTargetType.values();

public ReferenceTargetType byteToType(byte b) {
    int index = b - 1;
    if (0<=index && index<TYPES.length) return TYPES[index];
    ... throw SomeException or return null;
}

can't be beaten by anything, except maybe a hardcoded switch or if (though I strongly doubt it).

As this is most probably way faster then other operations (somehow you must get the byte and the result gets somehow used), I'd stop here. No need to optimize beyond this.


If your byte values were different, you'd need to initialize the array differently and possibly also make it longer, but otherwise nothing changes.


Using JNI for something as simple as an array access is about as efficient as using an airplane to get to the bathroom. It's complicated, it has a huge overhead, but maybe a coolness factor, too.

Adam Stelmaszczyk
  • 19,665
  • 4
  • 70
  • 110
maaartinus
  • 44,714
  • 32
  • 161
  • 320
1

I'd expect a switch to be way faster than "obviously" using arrays. In your case the compiler can optimize switch statements (see Why does Java switch on contiguous ints appear to run faster with added cases?).

I doubt that these tests provide any useful numbers anyway, but I've tried a fifth test case using switch and I get following results.

Run 1 average: 57729
Run 2 average: 93424
Run 3 average: 797
Run 4 average: 776
Run 5 average: 237
public enum ReferenceTargetType5 {
    BINARY((byte) 0x0), TOPIC((byte) 0x1), MAP((byte) 0x2), UNKNOWN((byte) 0x3);

    private byte value;

    ReferenceTargetType5(byte value) {
        this.value = value;
    }

    byte getValue() {
        return this.value;
    }

    static ReferenceTargetType5 fromValue(byte value) {
        switch (value) {
        case 0x0: return BINARY;
        case 0x1: return TOPIC;
        case 0x2: return BINARY;
        case 0x3: return UNKNOWN;
        default:  return UNKNOWN;
        }
    }

}
Community
  • 1
  • 1
kapex
  • 28,903
  • 6
  • 107
  • 121
  • Thanks Kapep, I updated my tests to include this one, however I do not get the same results as you. The switch does not appear to be faster than the array method. Maybe I am missing something... – Casey Jordan Dec 12 '14 at 15:39
  • Yeah that's why I said such numbers probably aren't too useful. Different JVM versions can have quite different results. Always using the same enum constants in the tests could trigger compiler or hotspot optimizations, branch prediction etc that can make all comparisons moot. – kapex Dec 12 '14 at 17:42
  • I think this method offers the best combination of performance and readability. However I have to choose the array method as the correct answer simply because that is what my tests show, that being said I agree measuring this type of micro optimization is probably not that accurate. – Casey Jordan Dec 15 '14 at 04:11
  • I'm missing the corresponding number for the arrays. However, you should use Caliper or JMH for such microbenchmarks, otherwise you risk getting pure nonsense out. While I don't know, how the compiler exactly optimized the switch, the choices here are limited: branching and arrays. Branching is usually way slower and with array we are where we started. – maaartinus Dec 15 '14 at 06:45