You have a value that's in [0,N). Ideally all code lengths would be the same ( log2(N) ) which is fractional for N not a power of 2. With just bit output, we can't write fractional bits, so we will lose some efficiency. But how much exactly?
You can of course trivially write a symbol in [0,N) by using log2ceil(N) bits. That's just going up to the next
integer bit count. But you're wasting values in there, so you can take each wasted value and use it to reduce
the length of a code that you need. eg. for N = 5 , start with log2ceil(N) bits :
0 : 000
1 : 001
2 : 010
3 : 011
4 : 100
x : 101
x : 110
x : 111
The first five codes are used for our values, and the last three are wasted.
Rearrange to interleave the wasted codewords :
0 : 000
x : 001
1 : 010
x : 011
2 : 100
x : 101
3 : 110
4 : 111
now since we have adjacent codes where one is used and one is not used, we can reduce the length of
those codes and still have a prefix code. That is, if we see the two bits "00" we know that it must
always be a value of 0, because "001" is wasted. So simply don't send the third bit in that case :
0 : 00
1 : 01
2 : 10
3 : 110
4 : 111
(this is a general way of constructing shorter prefix codes when you have wasted values). You can see that the number of wasted values we had at the top is the number of codes that can be shortened by one bit.
A flat code is written thusly :
void OutputFlat(int sym, int N)
{
ASSERT( N >= 2 && sym >= 0 && sym < N );
int B = intlog2ceil(N);
int T = (1<
That is, we write (T) values in (B-1) bits, and (N-T) in (B) bits.
The intlog2ceil can be slow, so in practice you would want to precompute that
or pass it in as a parameter.
<
B) - N;
// T is the number of "wasted values"
if ( sym < T )
{
// write in B-1 bits
PutBits(sym, B-1);
}
else
{
// write in B bits
// push value up by T
PutBits(sym+T, B);
}
}
int InputFlat(int sym,int N)
{
ASSERT( N >= 2 && sym >= 0 && sym < N );
int B = intlog2ceil(N);
int T = (1<<
B) - N;
int sym = GetBits(B-1);
if ( sym < T )
{
return sym;
}
else
{
// need one more bit :
int ret = (sym<<1) - T + GetBits(1);
return ret;
}
}
So, what is the loss vs. ideal, and where does it occur? Let's work it out :
H = log2(N) is the ideal (fractional) entropy
N is in (2^(B-1),2^B]
so H is in (B-1,B]
The number of bits written by the flat code is :
L = ( T * (B-1) + (N-T) * B ) / N
with T = 2^B - N
Let's set
N = f * 2^B
with f in (0.5,1] our fractional position in the range.
so T = 2^B * (1 - f)
At f = 0.5 and 1.0 there's no loss, so there must be a maximum in that interval.
Doing some simplifying :
L = (T * (B-1) + (N-T) * B)/N
L = (T * B - T + N*B - T * B)/N
L = ( N*B - T)/N = B - T/N
T/N = (1-f)/f = (1/f) - 1
L = B - (1/f) + 1
The excess bits is :
E = L - H
H = log2(N) = log2( f * 2^B ) = B + log2(f)
E = (B - (1/f) + 1) - (B + log2(f))
E = 1 - (1/f) - log2(f)
so find the maximum of E by taking a derivative :
d/df(E) = 0
d/df(E) = 1/f^2 - (1/f)/ln2
1/f^2 = (1/f)/ln2
1/f = 1/ln(2)
f = ln(2)
f = 0.6931472...
and at that spot the excess is :
E = 1 - (1/ln2) - ln(ln2)/ln2
E = 0.08607133...
The worst case is 8.6% of a bit per symbol excess. The worst case
appears periodically, once for each power of two.
The actual excess bits output for some low N's :
The worst case actually occurs as N->large, because at higher N you can get f closer to that worst case fraction (ln(2)). At lower N, the integer steps mean you miss the worst case and so waste less. This is perhaps a bit surprising, you might think that the worst case would be at something like N = 3.
In fact for N = 3 :
H = l2(3) = 1.584962 ...
L = average length written by OutputFlat
L = (1+2+2)/3 = 1.66666...
E = L - H = 0.08170421...
(obviously if you measure the loss as a percentage of the output length, the worst case
is at N=3, and there it's 5.155% of the entropy).
Taking your example, one could also note that 5 x 5 = 125 ~ 128 => 7 bits, so it becomes possible to use 7 bits to store 2 values.
ReplyDeleteSame for 3 :
3 x 3 x 3 = 27 ~ 32
or
3 x 3 x 3 x 3 x 3 = 243 ~ 256
Obviously it's not generic, and most probably not always practical.
Nonetheless, I think this method is used in some kind of texture compression format.
Yeah, that's a very useful general technique.
ReplyDeleteI used it in one of my LZ4 variants,
LZ4PLO-6*9*5 :
http://cbloomrants.blogspot.com/2012/10/10-22-12-lz-bytewise-conclusions.html