+2  A: 

Usually I'd do this with bit operations. The second version might eventually get optimized to something close to this by the JVM, but one can't be sure. Now, this is only 24 bits, following your samples, but the question says "29 bit integer". I'm not sure which you really wanted.

public static int readMediumInt(ByteBuffer buf) {
  return ((buf.get() & 0xFF) << 16) 
       | ((buf.get() & 0xFF) <<  8)
       | ((buf.get() & 0xFF);
}
erickson
This surely has to beat any version which involves allocation.
Tom Anderson
On most JVMs, I agree getting rid of allocation is the biggest win. But if I understand correctly, some of the newest JVM versions support escape analysis. If a non-primitive value doesn't "escape" from the stack, they can allocate and de-allocate it very efficiently (its effectively stack allocation).
erickson
This would appear to be the fastest and it is in fact 24 bit that we need here.
Mondain
+1  A: 

If you do actually want to read AMF 29-bit integers, this should do the job (assuming i've understood the format correctly):

private static int readMediumInt(ByteBuffer buf) {
    int b0, b1, b2;
    if ((b0 = buf.get()) >= 0) return b0;
    if ((b1 = buf.get()) >= 0) return ((b0 << 7) & ((~(-1 << 7)) << 7)) | b1;
    if ((b2 = buf.get()) >= 0) return ((b0 << 14) & ((~(-1 << 7)) << 14)) | ((b1 << 7) & ((~(-1 << 7)) << 7)) | b2;
    return ((b0 << 22) & ((~(-1 << 7)) << 22)) | ((b1 << 15) & ((~(-1 << 7)) << 15)) | ((b2 << 8) & ((~(-1 << 7)) << 8)) | (buf.get() & 0xff);
}
Tom Anderson
Might want to stick a few more 7s in there, though. And some brackets.
Tom Anderson
Wow, that is quite a block of code; I'll add it to my unit tests.
Mondain
+1  A: 

The most important change is to avoid allocating objects within the method. By the way your micro benchmark didn't reset "start", so the second result includes the time used for the first method. Also, you need to run micro benchmarks multiple times, otherwise the just in time compiler has no chance to run. I suggest to use a method similar to

public static int readMediumInt3(ByteBuffer buf) {
    return ((buf.get() & 0xff) << 16) + 
            ((buf.get() & 0xff) << 8) + 
            ((buf.get() & 0xff));
}

The complete code is:

import java.nio.ByteBuffer;

public class Main {

    public static int readMediumInt(ByteBuffer in) {
        ByteBuffer buf = ByteBuffer.allocate(4);
        buf.put((byte) 0x00);
        buf.put(in.get());
        buf.put(in.get());
        buf.put(in.get());
        buf.flip();
        return buf.getInt();
    }

    public static int readMediumInt2(ByteBuffer in) {
        byte[] bytes = new byte[3];
        in.get(bytes);
        int val = 0;
        val += bytes[0] * 256 * 256;
        val += bytes[1] * 256;
        val += bytes[2];
        if (val < 0) {
            val += 256;
        }
        return val;
    }

    public static int readMediumInt3(ByteBuffer buf) {
        return ((buf.get() & 0xff) << 16) + 
                ((buf.get() & 0xff) << 8) + 
                ((buf.get() & 0xff));
    }

    public static void main(String[] args) {
        Main m = new Main();
        for (int i = 0; i < 5; i++) {
            // version 1
            ByteBuffer buf = ByteBuffer.allocate(4);
            buf.putInt(424242);
            buf.flip();
            long start;
            start = System.nanoTime();
            for (int j = 0; j < 10000000; j++) {
                buf.position(0);
                readMediumInt(buf);
            }
            start = System.nanoTime() - start;
            System.out.printf("Ver 1: elapsed: %d ms\n", start / 1000000);

            // version 2
            ByteBuffer buf2 = ByteBuffer.allocate(4);
            buf2.putInt(424242);
            buf2.flip();
            start = System.nanoTime();
            for (int j = 0; j < 10000000; j++) {
                buf2.position(0);
                readMediumInt2(buf2);
            }
            start = System.nanoTime() - start;
            System.out.printf("Ver 2: elapsed: %d ms\n", start / 1000000);

            // version 3
            ByteBuffer buf3 = ByteBuffer.allocate(4);
            buf3.putInt(424242);
            buf3.flip();
            start = System.nanoTime();
            for (int j = 0; j < 10000000; j++) {
                buf3.position(0);
                readMediumInt3(buf3);
            }
            start = System.nanoTime() - start;
            System.out.printf("Ver 3: elapsed: %d ms\n", start / 1000000);
        }

    }
}

My results:

  • Ver 1: elapsed: 556 ms
  • Ver 2: elapsed: 187 ms
  • Ver 3: elapsed: 3 ms
Thomas Mueller
Nice catch on my not resetting the "start", I honestly didn't even pay attention to the times. :)
Mondain
I agree with you conclusion, however do you really think you performed 10 million operations each taking 556 ms. (Thats about 2 months) I suggest your timings would be in ns since your nanoTime() are in nano-seconds if you divided by 10 million instead of 1 million (i.e. you performed it 10 million times) Finally, your last loop to an average of 0.3 nano-seconds or about 1 clock cycle. It is highly likely your JVM realised this loop doesn't do anything and optimised it to nothing.
Peter Lawrey
It's not 556 ms "each". Did I say "each"? You maybe right, possibly the operation is optimized away. To ensure the method is fully executed, it's better to add some dummy operation such as sum += readMediumIntX in each case.
Thomas Mueller