11

I am currently working on some networking code (this is my first server) and had a quick question about optimizing a specific function which writes values as bits and then packs them into a byte. The reason for optimizing this function is because it is used thousands of times each server tick for packing data to be sent to several clients.

An example may serve better in order to explain what the function tries to accomplish: The value 3 can be represented by two bits. In binary, it would look like 00000011. The function would turn this binary value into 11000000. When the function is called again, it would know to start from the 3rd most significant bit (3rd from the right/decimal 32) and write at most up to 6 bits into the current byte. If then there were remaining bits to write, it would start on a new byte.

The purpose of this is to save space if you have multiple values that can be less than byte.

My current function looks like this:

 private ByteBuffer out = ByteBuffer.allocate(1024);
 private int bitIndex = 0;
 /*
  * Value: The value to write
  * Amount: The number of bits to represent the value in.
  */
     public OutputBuffer writeBits(long value, int amount) {
    if (bitIndex != 0) {
        int remainingBits = 8 - bitIndex;
        int bytePos = out.position() - 1;
        byte current = out.get(bytePos);
        int shiftAmount = amount - remainingBits;
        int bitsWritten = amount < remainingBits ? amount : remainingBits;
        int clearShiftAmount = 8 - bitsWritten + 56;

        byte b;
        if (shiftAmount < 0) {
            b = (byte) (current | (value << remainingBits - amount));
        } else {
            //deal with negative values
            long temp = (value >> shiftAmount);
            temp =  (temp << clearShiftAmount);
            temp = (byte) (temp >>> clearShiftAmount);
            b = (byte) (current | temp);
        }
        out.put(bytePos,b);
        bitIndex = (bitIndex + bitsWritten) % 8;
        amount -= bitsWritten;
    }
    if (amount <= 0) {
        return this;
    }
    bitIndex = amount & 7;
    int newAmount = amount - bitIndex;
    //newValue should not equal 2047
    for (int i = 0; i != newAmount; i += 8) {
        writeByte((byte) ((value >> i)), false);
    }
    if (bitIndex > 0)
        writeByte((byte) (value << (8 - bitIndex)), false);
    return this;
}

As I'm new to this I think there may be more efficient ways, maybe using bit-masking or a lookup table of some sort? Any ideas or steering in the right direction would be great. Cheers.

Eladian
  • 958
  • 10
  • 29
  • So on the receiving end you see the bit string `111110011`, how do you know the first two bits are a `3` and it's not really the first 3 bits were sent as a `7`? – Jim Garrison Feb 13 '18 at 08:09
  • Because the receiving end knows how the data was packed. So it would read 2 bits, then 4, ect. – Eladian Feb 13 '18 at 08:12
  • Did you consider using `java.util.BitSet`? It will do everything you want with no complex coding or need to manage the buffer yourself. Read the Javadoc. – Jim Garrison Feb 13 '18 at 08:16
  • 1
    @JimGarrison there's no methods available on BitSet to set specific values (e.g a value of 5 would require biset.set(0),bitset.set(2)) on top of this, it would also have to be reversed ect. I'm thinking it would be more complicated? – Eladian Feb 13 '18 at 08:21
  • Oh well, it was a thought. I went back to the Javadoc and now realize how underpowered that class is. There's quite a bit of functioality it could use such as logical operations between primitives and arbitrary substrings of the set. Never mind. – Jim Garrison Feb 13 '18 at 08:28
  • Yeah, I agree. Quite under powered. Only really useful for flags and other minuscule things. Thanks for the thought though! – Eladian Feb 13 '18 at 08:31
  • Is your byte buffer a huge amount of bits, or are byte limits preserved? Said differently can a single value be splitted over 2 different bytes? For example if the packing for 4, 2, 4, does the third value uses last two bits of previous byte and first two of next one, or directly 4 bits on next byte? – Serge Ballesta Feb 13 '18 at 08:52
  • Yes a single value can be splitted over 2 different bytes. 4,2,4 = 1 byte with a remainder of 2 bits. The 2 bits get packed to the most significant bits of the second byte, so if they were say 0b11, the next byte would be `11000000`. On the next write, we take the last byte and pack the bits into that, based on how many are remaining. – Eladian Feb 13 '18 at 09:18
  • So to answer your question directly: `the third value uses last two bits of previous byte and first two of next one` – Eladian Feb 13 '18 at 09:31
  • this question should be closed and moved to [codereview] – Oleg Estekhin Feb 18 '18 at 15:58

3 Answers3

6

Ok, I tweaked your original algorithm to remove some of the redundant math, and I shaved about 10% off (went from 0.016 ms to about 0.014 ms on my machine). I also altered my test to run each algorithm 1000 times.

There also appears to be some savings to be had in that last for loop because the same bits are being shifted over and over. If you could somehow retain the results of the previous shift that might help. But that would alter the order of bytes going to out, so would require some more thought.

public void writeBits3(long value, int amount) {
    if (bitIndex != 0) {
        int remainingBits = 8 - bitIndex;
        int bytePos = out.position() - 1;
        byte current = out.get(bytePos);
        int shiftAmount = amount - remainingBits;

        int bitsWritten = 0;
        if (shiftAmount < 0) {
            bitsWritten = amount;
            out.put(bytePos, (byte) (current | (value << -shiftAmount)));
        } else {
            bitsWritten = remainingBits;
            out.put(bytePos, (byte) (current | (value >> shiftAmount)));
        }

        bitIndex += bitsWritten;
        amount -= bitsWritten;
        if (bitIndex >= 8) {
            bitIndex = 0;
        }
    }
    if (amount <= 0) {
        return;
    }
    bitIndex = amount & 7;
    int newAmount = amount - bitIndex;
    long newValue = (value >> bitIndex);
    for (; newAmount >= 8; newAmount -= 8) {
        out.put((byte) (newValue >> newAmount));
    }
    out.put((byte) (value << (8 - bitIndex)));
}
Russ Jackson
  • 1,993
  • 1
  • 18
  • 14
  • That should run faster :) I'm wondering also if instead of checking if the bitIndex is >= 8 if modding the bitIndex by 8 and getting rid of the if statement would be faster – Eladian Feb 17 '18 at 22:35
  • By the way, I will wait a little more to see if there are any other contributions but if not ill be accepting this as the answer. – Eladian Feb 18 '18 at 01:37
  • I changed the bitIndex check as you suggested and the results were inconclusive. Most times slightly faster, but sometimes not. I adjusted the test to push 100 longs thru the algorithm and repeated that 10 million times for each algorithm. – Russ Jackson Feb 18 '18 at 05:17
3

Here's a better solution (than my previous one) using recursion, which is perfect for this problem.

private static final long[] mask = { 0, 0x1, 0x3, 0x7, 0xf, 0x1f, 0x3f, 0x7f, 0xff };

private ByteBuffer out = ByteBuffer.allocate(1024);
private int position = 0;
private int dataBits = 0;
private byte remainder = 0;

/**
 * value: The value to write
 * amount: The number of bits to represent the value in.
 */
public void writeBits(long value, int amount) {
    if (amount <= Long.SIZE) {
        if (amount > 0) {
            // left align the bits in value
            writeBitsLeft(value << (Long.SIZE - amount), amount);
        } else {
            // flush what's left
            out.put(position++, remainder);
        }
    } else {
        // the data provided is invalid
        throw new IllegalArgumentException("the amount of bits to write is out of range");
    }
}

/**
 * write amount bits from the given value
 * 
 * @param value represents bits aligned to the left of a long
 * @param amount bits left to be written from value
 */
private void writeBitsLeft(long value, int amount) {
    if (amount > 0) {
        // how many bits are left to be filled in the current byte?
        int room = Byte.SIZE - dataBits;

        // how many bits are we going to add to the current byte?
        int taken = Math.min(amount, room);

        // rotate those number of bits into the rightmost position
        long temp = Long.rotateLeft(value, taken);

        // add count taken to the count of bits in the current byte
        dataBits += taken;

        // add in that number of data bits
        remainder &= temp & mask[taken];

        // have we filled the byte yet?
        if (Byte.SIZE == dataBits) {
            out.put(position++, remainder);

            // reset the current byte
            remainder = 0;
            dataBits = 0;

            // process any bits left over
            writeBitsLeft(temp, amount - taken);
        }
    } 
} // writeBitsLeft()

This solution has fewer math operations, fewer shift operations and fewer if statements and therefore should be more efficient than the original solution, not to mention it may be a little easier to understand.

Russ Jackson
  • 1,993
  • 1
  • 18
  • 14
  • Well, I wrote a unit test that measures your solution (V0) and both of mine (V1 and V2) and yours was the hands down winner: – Russ Jackson Feb 16 '18 at 14:39
  • optimized V2 = 408393 optimized V1 = 342537 optimized V0 = 313885 – Russ Jackson Feb 16 '18 at 14:40
  • Hmm odd, are you saying this unit tested to be slower than the solution I posted? This definitely looks like a more elegant and easy to read version, however maybe it's slower due to the extra method invocations. Thanks for posting your solutions I am however looking for something that will unit test faster than the posted solution :) – Eladian Feb 17 '18 at 03:00
  • I altered my test to run each algorithm 100 times and average the results. V0 (yours) and V1 (mine) were neck and neck - sometimes one was faster and sometimes the other, but not by much. My V2 (recursion) was consistently the slowest by a good margin. – Russ Jackson Feb 17 '18 at 20:00
  • I found this. If you convert the long to base 127 then the sign bit can be used to indicate whether or not there are more bytes for the given number in the stream. https://en.wikipedia.org/wiki/Variable-length_quantity – Russ Jackson Feb 17 '18 at 21:57
1

What about something like this?

private ByteBuffer out = ByteBuffer.allocate(1024);
private int position = 0;
private int dataBits = 0;
private long data = 0;

/**
 * value: The value to write
 * amount: The number of bits to represent the value in.
 */
public void writeBits(long value, int amount) {
    if (amount <= 0) {
        // need to flush what's left
        if (dataBits > 0) {
            dataBits = Byte.SIZE;
        }
    } else {
        int totalBits = dataBits + amount;

        // need to handle overflow?
        if (totalBits > Long.SIZE) {
            // the new data is to big for the room that remains;  by how much?
            int excess = totalBits - Long.SIZE;

            // drop off the excess and write what's left
            writeBits(value >> excess, amount - excess);

            // now we can continue processing just the rightmost excess bits
            amount = excess;
        }

        // push the bits we're interested in all the way to the left of the long
        long temp = value << (Long.SIZE - amount);

        // make room for any existing (leftover) data bits, filling with zeros to the left (important)
        temp = temp >> dataBits;

        // append the new data to the existing
        data |= temp;

        // account for new bits of data
        dataBits += amount;
    }

    while (dataBits >= Byte.SIZE) {
        // shift one byte left, rotating the byte that falls off into the rightmost byte
        data = Long.rotateLeft(data, Byte.SIZE);

        // add the rightmost byte to the buffer
        out.put(position++, (byte)(data & 0xff));

        // drop off the rightmost byte
        data &= 0xffffffffffffff00L;

        // setup for next byte
        dataBits -= Byte.SIZE;
    }
}
Russ Jackson
  • 1,993
  • 1
  • 18
  • 14