Before there was LogLog, SuperLogLog or HyperLogLog there was Probabilistic Counting with Stochastic Averaging (PCSA) from the seminal work “Probabilistic Counting Algorithms for Data Base Applications” (also known as the “FM Sketches” due to its two authors, Flajolet and Martin). The basis of PCSA matches that of the other Flajolet distinct value (DV) counters: hash values from a collection into binary strings, use patterns in those strings as indicators for the number of distinct values in that collection (bit-pattern observables), then use stochastic averaging to combine *m* trials into a better estimate. Our HyperLogLog post has more details on these estimators as well as stochastic averaging.

### Observables

The choice of observable pattern in PCSA comes from the knowledge that in a collection of randomly generated binary strings, the following probabilities occur:

For each value added to the DV counter, a suitable hash is created and the position of the least-significant (right-most) *1* is determined. The corresponding position in a bitmap is updated and stored. I’ve created the simulation below so that you can get a feel for how this plays out.

(All bit representations in this post are numbered from 0 (the least-significant bit) on the right. This is the opposite of the direction in which they’re represented in the paper.)

Run the simulation a few times and notice how the bitmap is filled in. In particular, notice that it doesn’t necessarily fill in from the right side to the left — there are gaps that exist for a time that eventually get filled in. As the cardinality increases there will be a block of *1*s on the right (the high probability slots), a block of *0*s on the left (the low probability slots) and a “fringe” (as Flajolet et al. called it) of *1*s and *0*s in the middle.

I added a small pointer below the bitmap in the simulation to show how the cardinality corresponds to the expected bit position (based on the above probabilities). Notice what Flajolet et al. saw when they ran this same experiment: the least-significant (right-most) *0* is a pretty good estimator for the cardinality! In fact when you run multiple trials you see that this least-significant *0* for a given cardinality has a narrow distribution. When you combine the results with stochastic averaging it leads to a small relative error of and estimates the cardinality of the set quite well. You might have also observed that the most-significant (left-most) *1* can also be used for an estimator for the cardinality but it isn’t as clear-cut. This value is exactly the observable used in LogLog, SuperLogLog and HyperLogLog and does in fact lead to the larger relative error of (in the case of HLL).

### Algorithm

The PCSA algorithm is elegant in its simplicity:

m = 2^b # with b in [4...16] bitmaps = [[0]*32]*m # initialize m 32bit wide bitmaps to 0s ############################################################################################## # Construct the PCSA bitmaps for h in hashed(data): bitmap_index = 1 + get_bitmap_index( h,b ) # binary address of the rightmost b bits run_length = run_of_zeros( h,b ) # length of the run of zeroes starting at bit b+1 bitmaps[ bitmap_index ][ run_length ] = 1 # set the bitmap bit based on the run length observed ############################################################################################## # Determine the cardinality phi = 0.77351 DV = m / phi * 2 ^ (sum( least_sig_bit( bitmap ) ) / m) # the DV estimate

Stochastic averaging is accomplished via the arithmetic mean.

You can see PCSA in action by clicking on the image below.

There is one point to note about PCSA and small cardinalities: Flajolet et al. mention that there are “initial nonlinearities” in the algorithm which result in poor estimation at small cardinalities () which can be dealt with by introducing corrections but they leave it as an exercise for the reader to determine what those corrections are. Scheuermann et al. did the leg work in “Near-Optimal Compression of Probabilistic Counting Sketches for Networking Applications” and came up with a small correction term (see equation 6). Another approach is to simply use the linear (ball-bin) counting introduced in the HLL paper.

### Set Operations

Just like HLL and KMV, unions are trivial to compute and lossless. The PCSA sketch is essentially a “marker” for runs of zeroes, so to perform a union you merely bit-wise OR the two sets of bitmaps together. Folding a PCSA down to a smaller *m* works the same way as HLL but instead of HLL’s *max* you bit-wise OR the bitmaps together. Unfortunately for intersections you have the same issue as HLL, you must perform them using the inclusion/exclusion principle. We haven’t done the plots on intersection errors for PCSA but you can imagine they are similar to HLL (and have the benefit of the better relative error ).

### PCSA vs. HLL

That fact that PCSA has a better relative error than HyperLogLog with the same number of registers () is slightly deceiving in that (the number of stored observations) are different sizes. A better way to look at it is to fix the accuracy of the sketches and see how they compare. If we would like to have the same relative error from both sketches we can see that the relationship between registers is:

Interestingly, PCSA only needs a little more than half the registers of an HLL to reach the same relative error. But this is also deceiving. What we should be asking is what is the size of each sketch if they provide the same relative error? HLL commonly uses a register width of 5 bits to count to billions whereas PCSA requires 32 bits. That means a PCSA sketch with the same accuracy as an HLL would be:

Therefore,

A PCSA sketch with the same accuracy is 3.6 times larger than HLL!

### Optimizations

But what if you could make PCSA smaller by reducing the size of the bitmaps? Near the end of the paper in the *Scrolling* section, Flajolet et al. bring up the point that you can make the bitmaps take up less space. From the simulation you can observe that with a high probability there is a block of consecutive *1*s on the right side of the bitmap and a block of consecutive *0*s on the left side of the bitmap with a fringe in between. If one found the “global fringe” — that is the region defined by the left-most *1* and right-most *0* across all bitmaps — then only those bits need to be stored (along with an offset value). The authors theorized that a fringe width of 8 bits would be sufficient (though they fail to mention if there are any dependencies on the number of distinct values counted). We put this to the test.

In our simulations it appears that a fringe width of 12 bits is necessary to provide an unbiased estimator comparable to full-fringe PCSA (32-bit) for the range of distinct values we analyzed. (Notice the consistent bias of smaller fringe sizes.) There are many interesting reasons that this “fringe” concept can fail. Look at the notes to this post for more. If we take the above math and update 32 to 12 bits per register (and include the 32 bit offset value) we get:

This is getting much closer to HLL! The combination of tighter bounds on the estimate and the fact that the fringe isn’t really that wide in practice result in PCSA being very close to the size of the much lauded HLL. This got us thinking about further compression techniques for PCSA. After all, we only need to get the sketch about 1/3 smaller to be comparable in size to HLL. In a future post we will talk about what happens if you Huffman code the PCSA bitmaps and the tradeoffs you make when you do this.

### Summary

PCSA provides for all of the goodness of HLL: very fast updates making it suitable for real-time use, small footprint compared to the information that it provides, tunable accuracy and unions. The fact that it has a much better relative error per register than HLL indicates that it should get more credit than it does. Unfortunately, each bitmap in PCSA requires more space than HLL and you still get less accuracy per bit. Look for a future post on how it is possible to use compression (e.g. Huffman encoding) to reduce the number of bits per bitmap, thus reducing the error per bit to match that of HLL, resulting in an approach that matches HLL in size but exceeds its precision!

### Notes on the Fringe

While we were putting this post together we discovered many interesting things to look at with respect to fringe optimization. One of the questions we wanted to answer was “How often does the limited size of the fringe muck up a bitmap?” Below is a plot that shows how often any given sketch had a truncation event (*that affected the DV estimate*) in the fringe of any one of its bitmaps for a given fringe width (i.e. some value could not be stored in the space available).

Note that this is an upper bound on the error that could be generated by truncation. If you compare the number of runs that had a truncation event (almost all of the runs) with the error plot in the post it is quite shocking that the errors are as small as they are.

Since we might not get around to all of the interesting research here, we are calling out to the community to help! Some ideas:

- There are likely a few ways to improve the fringe truncation. Since PCSA is so sensitive to the least-significant
*1*in each bitmap, it would be very interesting to see how different approaches affect the algorithm. For example, in our algorithm we “left” truncated meaning that all bitmaps had to have a one in the least-significant position of the bitmap in order to move up the offset. It would be interesting to look at “right” truncation. If one bitmap is causing many of the others to not record incoming values perhaps it should be bumped up. Is there some math to back up this intuition? - It is interesting to us that the fringe width truncation events are DV dependent. We struggled with the math on this for a bit before we just stopped. Essentially we want to know what is the width of the theoretical fringe? It obviously appears to be DV dependent and some sort of coupon collector problem with unequal probabilities. Someone with better math skills than us needs to help here.

### Closing thoughts

We uncovered PCSA again as a way to go back to first principles and see if there are lessons to be learned that could be applied to HLL to make it even better. For instance, can all of this work on the fringe be applied to HLL to reduce the number of bits per register while still maintaining the same precision? HLL effectively records the “strandline” (what we call the left-most *1*s). More research into how this strandline behaves and if it is possible to improve the storage of it through truncation could reduce the standard HLL register width from 5 bits to 4, a huge savings! Obviously, we uncovered a lot of open questions with this research and we feel there are algorithmic improvements to HLL right around the corner. We have done some preliminary tests and the results so far are intriguing. Stay tuned!

A little background on this post: I’m sure like a lot of you that have read the HyperLogLog paper, I just casually glanced at the PCSA paper thinking “well, if Flajolet improved upon it three times already then I don’t need to dig too deeply into it”. When Matt, Timon and I were at the SODA/ALENEX conference (which was an awesome conference BTW) there were a number of presentations that talked about trie encoding and rank/select structures. (I got interested in rank/select before when I read the Google SparseHash implementation (http://sparsehash.googlecode.com/svn/trunk/doc/implementation.html — you have to read it a few times to really get that the “find()” operation is just a rank).)

It seemed like the in vogue thing to find some old problem, look at it like a trie, do some slick rank/select stuff and voilà you’ve got something neato (aka smaller, faster, etc)! The only problem was that I didn’t have anything that looked like a trie that needed fiddling with. Initially I started looking at HyperLogLog (duh!) and didn’t get anywhere interesting. Then I realized that all of the bits that lead up to the “strandline” bit (the value that’s stored in HLL’s registers) can be thought of as a trie. After some futzing around with it, the little librarian in my head finally decided to show up to work and popped up with the obvious answer: you’re exactly thinking about PCSA!

We wanted to make a nice post about PCSA since it’s a nice forcing function for us to get a deep understanding of it. Then the floodgates opened. We got to thinking about the fringe which (as hopefully we’ve conveyed in the post) lead us to get a taste of how close PCSA is to being better than HLL. Then we started working on obvious encoding schemes (e.g. Huffman), v-len encoding, and all sorts of things that we haven’t gotten to write about yet.

All of this leads us to today. We still haven’t gotten back to the trie encodings that originally set us on this path. But we do have a much better understanding and appreciation of PCSA!

Stay tuned for more!

Is source code for the simulation somewhere available ? I would like to replicate the functionality to better understand the algorithm.

Hello Ritesh! Just go to the simulation itself and do a “View Source” in your browser. We tried to put as much of it as possible into the HTML page so that it was easy to grok. (Do note that there are JS includes if you want to download it locally. Just look for the tags.) Let me know if you need more help!