Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Add portable Roaring bitmap for row positions #11372

Merged
merged 9 commits into from
Oct 28, 2024

Conversation

aokolnychyi
Copy link
Contributor

@aokolnychyi aokolnychyi commented Oct 21, 2024

This PR adds a Roaring bitmap implementation that follows the official "portable" serialization format from the library for row positions in preparation for V3 position deletes. We are still discussing the blob layout for DVs on the dev list but we agree we should serialize the bitmap according to the portable serialization spec.

We cannot rely on the Roaring64Bitmap from the library as it has undefined serialization behavior:

  /**
   * {@link Roaring64NavigableMap} are serializable. However, contrary to RoaringBitmap, the
   * serialization format is not well-defined: for now, it is strongly coupled with Java standard
   * serialization. Just like the serialization may be incompatible between various Java versions,
   * {@link Roaring64NavigableMap} are subject to incompatibilities. Moreover, even on a given Java
   * versions, the serialization format may change from one RoaringBitmap version to another
   */

Here are benchmarks comparing this implementation to the one from the library we use today.

Benchmark                                                         Mode  Cnt  Score   Error  Units
RoaringPositionBitmapBenchmark.addAndCheckPositionsIcebergBitmap    ss    5  0.535 ± 0.032   s/op
RoaringPositionBitmapBenchmark.addAndCheckPositionsLibraryBitmap    ss    5  1.124 ± 0.013   s/op
RoaringPositionBitmapBenchmark.addOrderedPositionsIcebergBitmap     ss    5  0.023 ± 0.003   s/op
RoaringPositionBitmapBenchmark.addOrderedPositionsLibraryBitmap     ss    5  0.165 ± 0.028   s/op
RoaringPositionBitmapBenchmark.addShuffledPositionsIcebergBitmap    ss    5  0.320 ± 0.006   s/op
RoaringPositionBitmapBenchmark.addShuffledPositionsLibraryBitmap    ss    5  0.338 ± 0.011   s/op

This is not a spec change but part of #11122.

@github-actions github-actions bot added the core label Oct 21, 2024
* @param posStartInclusive the start position of the range (inclusive)
* @param posEndExclusive the end position of the range (exclusive)
*/
public void addRange(long posStartInclusive, long posEndExclusive) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be optimized in the future but we have no use case in the repo beyond tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this package-private then? This has implications for run encoding in the bitmaps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our public PositionDeleteIndex where we will eventually consume this class actually supports marking ranges as removed. Therefore, it is needed, but we shouldn't be concerned optimizing this right away.

import org.roaringbitmap.RoaringBitmap;

/**
* A bitmap that supports positive 64-bit positions, but is optimized for cases where most positions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Credit to @rdblue for most of this description copied from the spec PRs for DVs.

* @see <a href="https://github.com/RoaringBitmap/RoaringFormatSpe">Roaring bitmap spec</a>
*/
public void serialize(ByteBuffer buffer) {
validateByteOrder(buffer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not duplicate the ByteBuffer, set its endianness, and then update the buffer's position to match the duplicate after serialization?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do similar stuff in the caller, it is more like a sanity check at this point, which also follows the Roaring lib API.

}

// extracts high 32 bits from a 64-bit position
private static int highBytes(long pos) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyBytes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to agree here, since you need to read the javadoc to know that these are the key bytes, so might be worth renaming the method to indicate that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just key?

Copy link
Contributor Author

@aokolnychyi aokolnychyi Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that input in many methods is called position and represents 64-bit positions. If we want to rename this method to key (which I agree makes sense), we will have to consider renaming lowBytes too, which I don't have a very good alternative for.

Let me think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed to "key" and "value". Please, take a look, @RussellSpitzer @nastra @amogh-jahagirdar.

}

/**
* Adds a position to the bitmap.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should use "add", because in my mind this is more of a "set"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used add/contains from the Roaring lib but open to any names.
What would be contains equivalent if we go with set?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have super strong opinions on this, but I always kind of think of "add" for Sets and "set" for Bitmaps. I know we are kind of doing the same things but at least mentally in my head it's not as if we are actually "adding" more like we are just flipping a bit.

I think contains would still probably be fine?

*
* @param that the other bitmap
*/
public void or(RoaringPositionBitmap that) {
Copy link
Member

@RussellSpitzer RussellSpitzer Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not return this for chaining? I'm not sure what our standard is for this sort of API

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To highlight it modifies the bitmap in place, not a strong opinion. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way is ok with me, I know in most of our public APIs we use the functional approach even for modify in place.

*
* @return whether the bitmap was compressed
*/
public boolean runOptimize() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we want to expose Roaring bitmap apis like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use this while serializing BitmapPositionDeleteIndex in the main PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to just call this compress or runLengthEncode? I'm OK either way, i see roaring bit map calls it runOptimize, just feels like too broad of a name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like compress, I'll rename.

position(1 /* bitmap */, 2 /* container */, 1L),
position(1 /* bitmap */, 2 /* container */, CONTAINER_OFFSET - 1));

assertThat(bitmap.runOptimize()).as("Bitmap must be RLE encoded").isTrue();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should optimize be run as part of serialization?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I call that in PositionDeleteIndex, not sure about the bitmap itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, we can't do that. The caller may have called serializedSizeInBytes() to estimate the size prior to the serialization.

Copy link
Contributor

@rdblue rdblue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks correct overall to me. I have a few suggestions for minor things, but it looks like this is about ready.


private void allocateBitmapsIfNeeded(int requiredLength) {
if (bitmaps.length < requiredLength) {
if (bitmaps.length == 0 && requiredLength == 1) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a special case for this?

Copy link
Contributor Author

@aokolnychyi aokolnychyi Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply optimizes the most frequent use case.

import org.apache.iceberg.relocated.com.google.common.io.Resources;
import org.junit.jupiter.api.Test;

public class TestRoaringPositionBitmap {
Copy link
Member

@RussellSpitzer RussellSpitzer Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a good place to do some property based testing since we already have a class that matches the behavior we want ("HashSet"). This way we can hit more than a select few positions and verify the behaviors pretty simply I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add some fuzz testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some fuzz tests and included seed information in the test arguments. If anything fails, we will have a way to reproduce the issue reliably.

private static int readKey(ByteBuffer buffer, int lastKey) {
int key = buffer.getInt();
Preconditions.checkArgument(key >= 0, "Invalid unsigned key: %s", key);
Preconditions.checkArgument(key <= Integer.MAX_VALUE - 1, "Key is too large: %s", key);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this and the precondition right below it isn't examined in the tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covering this would be fairly involved as this logic is part of deserialization and we don't produce such files.

Copy link
Contributor

@danielcweeks danielcweeks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are a couple good outstanding ideas for clarifying method names, but I'm ok with whatever is decided.

}

@TestTemplate
public void testRandomSparseBitmap() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RussellSpitzer, here we have 3 tests with random values that are compared against Set<Long>.

return Resources.toByteArray(resource);
}

private static void assertEqual(RoaringPositionBitmap bitmap, Set<Long> positions) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really 3 asserts
assertEqual()
assertEqualAfterSerDE()
assertEqualAfterCompress()

, but it's probably fine to combine them since the debugger will point to which block we are in

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a common method for the checks.

*
* @param pos the position
*/
public void set(long pos) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RussellSpitzer, I switched to set instead of add. It matches BitSet in Java.

@aokolnychyi aokolnychyi merged commit 1e3ee1e into apache:main Oct 28, 2024
49 checks passed
@aokolnychyi
Copy link
Contributor Author

I am going to merge this one as it was thoroughly reviewed. There is no need to include it in 1.7, though (no harm too).

Thanks for reviewing, @rdblue @RussellSpitzer @amogh-jahagirdar @nastra @danielcweeks!

zachdisc pushed a commit to zachdisc/iceberg that referenced this pull request Dec 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants