CNDB-15669: implement mayHaveContent metadata flag#4
Conversation
There was a problem hiding this comment.
This file was CLRF for some reason.. Lmk if I should change it back to CLRF
src/java/org/apache/cassandra/db/tries/DepthAdjustedCursor.java: ASCII text, with CRLF line terminators
| // the head alone would not reflect. | ||
| currentPosition = pos; | ||
| applyToSelectedElementsInHeap((self, cursor, index) -> { | ||
| currentPosition |= cursor.encodedPosition(); |
There was a problem hiding this comment.
Try using self.currentPosition to avoid capturing this in the closure.
| applyToSelectedElementsInHeap((self, cursor, index) -> { | ||
| currentPosition |= cursor.encodedPosition(); | ||
| }, 0); | ||
| currentPosition = Cursor.unionFlags(pos, currentPosition, Cursor.FLAGS_MASK); |
There was a problem hiding this comment.
This shouldn't be necessary as the position bits of all the selected cursors must match. Maybe replace with a comment?
| /// Flag indicating whether this position may have content. | ||
| long MAY_HAVE_CONTENT_BIT = 1L; | ||
|
|
||
| /// reverse direction. |
There was a problem hiding this comment.
The beginning of this doc is missing.
There was a problem hiding this comment.
added it back, removed by mistake when rebasing
| depth(encodedPosition), | ||
| incomingTransition(encodedPosition), | ||
| isOnReturnPath(encodedPosition) ? "↑" : " ", | ||
| (encodedPosition & MAY_HAVE_CONTENT_BIT) != 0 ? "*" : " ", |
| state = c2 != null ? State.AT_BOTH : State.C1_ONLY; | ||
| currentPosition = c1.encodedPosition(); | ||
| if (c2 != null) | ||
| currentPosition = Cursor.unionFlags(currentPosition, c2.encodedPosition(), Cursor.FLAGS_MASK); |
There was a problem hiding this comment.
Maybe add a version of unionFlags to be used when it is known that the position bits match, using a simple or.
Actually I don't think we should call this any other way -- so perhaps do something like
if (DEBUG) assertEquals(0, compare(pos1, pos2));
return pos1 | pos2;
in the method?
If you have the time to experiment, it would also be a good idea to check how costly each version is (with if (DEBUG), with an assertion without the if and assertions enabled (which is how Cassandra is normally run in production), and without the assertion altogether, or the version that masks the flags).
There was a problem hiding this comment.
Wrote a simple benchmark here are the results
[junit-timeout] ================================================================================
[junit-timeout] UnionFlags Performance Benchmark Suite
[junit-timeout] ================================================================================
[junit-timeout] Assertions enabled: true
[junit-timeout] DEBUG flag: false
[junit-timeout] Operations per iteration: 1000000
[junit-timeout] Warmup iterations: 5
[junit-timeout] Measurement iterations: 20
[junit-timeout] ================================================================================
[junit-timeout]
[junit-timeout] Warming up...
[junit-timeout] Warmup complete.
[junit-timeout]
[junit-timeout] Running benchmarks...
[junit-timeout]
[junit-timeout]
[junit-timeout] ================================================================================
[junit-timeout] RESULTS
[junit-timeout] ================================================================================
[junit-timeout] Current Masking Approach : 9,154,872 ns total, 0.46 ns/op, 2,184,629,124 ops/sec
[junit-timeout] Simple OR with DEBUG Assertion : 6,673,580 ns total, 0.33 ns/op, 2,996,892,223 ops/sec
[junit-timeout] Simple OR with Assertion : 9,689,000 ns total, 0.48 ns/op, 2,064,196,512 ops/sec
[junit-timeout] Simple OR (No Assertion) : 6,795,041 ns total, 0.34 ns/op, 2,943,322,932 ops/sec
| if (!Cursor.isExhausted(nextPosition)) | ||
| prepareNextPosition(currentPosition); | ||
| return currentPosition; | ||
| return encodedPosition(); |
There was a problem hiding this comment.
Could we apply the adjustment to currentPosition once here and return it unchanged in encodedPosition?
There was a problem hiding this comment.
Could we use the same adjustNextPosition mechanism as SingletonOrderedCursor, which will also simplify the latter?
| currentPosition = Cursor.positionForDescentWithByte(pos, current); | ||
| nextPosition = Cursor.exhaustedPosition(currentPosition); | ||
| return currentPosition; | ||
| return encodedPosition(); |
There was a problem hiding this comment.
This should always result in reaching the end state, i.e. we can directly add the flag to currentPosition above.
| case ROOT: | ||
| case NONE: | ||
| default: | ||
| if (!Cursor.isExhausted(encodedPosition)) |
There was a problem hiding this comment.
In the NONE case negation will not change the boundary status of the state (negating a boundary still results in a boundary).
In the ROOT case we have to flip the flag (if a boundary existed we drop it, it one didn't we add it).
| class Negated implements TrieSetCursor | ||
| { | ||
| final TrieSetCursor source; | ||
| final Direction direction; |
| break; | ||
| default: | ||
| return checkOverride(source.advance()); | ||
| checkOverride(source.advance()); |
There was a problem hiding this comment.
checkOverride already calls encodedPosition() when needed.
ea33ff1 to
290c584
Compare
6f452e7 to
3df0ae5
Compare
| // the head alone would not reflect. | ||
|
|
||
| // Optimization: if the head already has all flags set, no need to walk the heap | ||
| if ((pos & Cursor.FLAGS_MASK) == Cursor.FLAGS_MASK) |
There was a problem hiding this comment.
For this to be helpful we need to use a mask of only the flags that we can actually set.
|
|
||
| /// Collects and caches the current position by unioning flags from all cursors at the same position. | ||
| /// This is called after advancing to ensure the position is always up-to-date. | ||
| private long collectAndCachePosition() |
There was a problem hiding this comment.
Nit: I would call this collectAndCachePositionFlags().
| { | ||
| currentPosition = nextPosition; | ||
| if (nodeContent != null || isLeaf(fullNode)) | ||
| if (nodeContent != null) |
There was a problem hiding this comment.
AFAIU the only reason to have this now is the constructor. Let's do this there instead of checking for every advance.
(I was tempted to assert that the flag is as expected, but we already do it in DEBUG mode thus even that is not needed.)
| return Cursor.exhaustedPosition(encodedPosition); | ||
| case ROOT: | ||
| // In ROOT case, set flag if negated state is a boundary | ||
| if (!Cursor.isExhausted(encodedPosition) && state().isBoundary()) |
There was a problem hiding this comment.
Just do encodedPosition ^= MAY_HAVE_CONTENT_BIT.
The position cannot be exhausted when overriding is ROOT (run the tries package with an assertion if you want to be certain).
There was a problem hiding this comment.
!isExhausted() check was redundant but we can't do just encodedPosition ^= MAY_HAVE_CONTENT_BIT
It assumes that the content status of the negated cursor is always the exact opposite of the original cursor's
IIUC then this would be a counter example
| Scenario | Source Set | Source isBoundary()? (content bit) | Source succeedingIncluded? | Negated Set Needs Boundary? (correct bit) | Does source_bit ^ 1 work? |
|---|---|---|---|---|---|
| 1 | ("a", "b") | No (0) | No | Yes (1) | Yes (0 ^ 1 = 1) |
| 2 | (null, "b") | No (0) | Yes | No (0) | No (0 ^ 1 = 1) |
There was a problem hiding this comment.
Isn't isBoundary() true for the source root in the second example?
| @Override | ||
| public RangeState state() | ||
| { | ||
| Direction dir = direction(); |
There was a problem hiding this comment.
Almost all calls of this method will go through the NONE path. I'd rather we didn't call direction() for them.
Move this to the ROOT case.
ce51443 to
3d92f1f
Compare
| // In ROOT case, set flag if negated state is a boundary | ||
| if (!Cursor.isExhausted(encodedPosition) && state().isBoundary()) | ||
| encodedPosition |= MAY_HAVE_CONTENT_BIT; | ||
| if (!Cursor.isExhausted(pos) && state().isBoundary()) |
There was a problem hiding this comment.
Is it really possible that we are in the root state and the source is exhausted?
(By contract a cursor cannot start in an exhausted state.)
There was a problem hiding this comment.
Both the source and negated cursors can have boundaries at the ROOT position, then XOR would be incorrect or am I mistaken here? eg.
Negated boundaries: [null, 616263, 616667, 616761, 616a62, null]
Negated set
Forward:
-> START
61 -> CONTAINED
62 -> CONTAINED
63 -> END
66 -> NOT_CONTAINED
67↑ -> START
67 -> CONTAINED
61 -> END
6a -> NOT_CONTAINED
62↑ -> START
↑ -> END
Reverse:
-> END
61 -> CONTAINED
6a -> CONTAINED
62 -> START
67 -> NOT_CONTAINED
61↑ -> END
66 -> CONTAINED
67 -> START
62 -> NOT_CONTAINED
63↑ -> END
↑ -> START
Tail for FORWARD
tail bounds [null, 616263, 616667, 616761, 616a62]
Forward:
java.lang.AssertionError: Non-null content for position without MAY_HAVE_CONTENT_BIT: depth 0 incomingTransition 00 FORWARD
TrieSetCursor$Negated pos depth 0 incomingTransition 00 FORWARD at state START
blambov
left a comment
There was a problem hiding this comment.
I think we also want to do the position check whenever we get mutationCursor.content() in all InMemoryTrie mutators.
|
|
||
| // Always check if we are seeing new content; if we do, that's an easy state update. | ||
| S content = content(); | ||
| S content = (position & MAY_HAVE_CONTENT_BIT) != 0 ? content() : null; |
There was a problem hiding this comment.
I don't think we need to do this as content() is just this.content -- maybe inline it here instead?
970213d to
66c45f1
Compare
af326a8 to
4df869e
Compare
a4cd17f to
f2e5ca8
Compare
ac98486 to
0734395
Compare
952e794 to
cfce0f0
Compare
…lag in trie cursors. This metadata flag allows skipping content lookups for structural nodes and is correctly propagated and unioned across all cursor implementations.
0734395 to
2c64c6a
Compare
| } | ||
|
|
||
| // MAY_HAVE_CONTENT_BIT optimization: only call content() if flag indicates potential content | ||
| if ((currentPosition & MAY_HAVE_CONTENT_BIT) != 0) |
There was a problem hiding this comment.
I don't think it's a good idea for the deletion branch to be presented before content that covers it. Dumps, for one, look weird with this order.
| assertFresh(); | ||
| T content = content(); // handle content on the root node | ||
| long currentPosition = encodedPosition(); | ||
| T content = (currentPosition & MAY_HAVE_CONTENT_BIT) != 0 ? content() : null; |
There was a problem hiding this comment.
We should be able to put this in a static Cursor.content(cursor, cursorPosition) method without changing the performance.
We can also add a line in the content() javadoc to say it's preferable to get this via the static. If you prefer, call it Cursor.checkFlagAndGetContent or something similar.
| { | ||
| assertFresh(); | ||
| T content = content(); // handle content on the root node | ||
| long currentPosition = encodedPosition(); |
There was a problem hiding this comment.
How about making assertFresh return the position (which it has to fetch anyway) and rename it to something like getPositionAndAssertFresh?
| { | ||
| if (!Cursor.isExhausted(position)) | ||
| { | ||
| currentPosition = position; |
There was a problem hiding this comment.
AFAICS callers already update currentPosition, this should not be necessary.
| if (rootAscentContent != null) | ||
| addBacktrack(NONE, 0, -1); | ||
| updateActiveAndReturn(encodedPosition()); | ||
| setNodeState(currentPosition, rootDescentContent, currentFullNode, currentNode); |
There was a problem hiding this comment.
How about
assert currentFullNode == root && currentNode == root;
long rootPos = encodedPosition();
if (rootDescentContent != null)
setNodeState(rootPos | MAY_HAVE_CONTENT_BIT, rootDescentContent, root, root);
updateActiveAndReturn(rootPos);
so that we don't need to change the visibility of currentPosition?
I have messed up this construction a bit, making it do unnecessary work and call updateActiveAndReturn twice; if you prefer, add a separate constructor that doesn't do any of the usual preparation so that we can set the values we need directly.
| } | ||
|
|
||
| @Override | ||
| public long encodedPosition() |
There was a problem hiding this comment.
Doesn't the base class already deal with the content flag? What does the deletion branch have to do with it?
| { | ||
| return Cursor.isRootPosition(encodedPosition()) && deletionBranch != null | ||
| long pos = encodedPosition(); | ||
| return (Cursor.isRootPosition(pos) || Cursor.compare(pos, matchingPositionAtRoot) == 0) && deletionBranch != null |
There was a problem hiding this comment.
Why are we presenting the deletion branch again at after the prefix?
| { | ||
| if (Cursor.isRootPosition(encodedPosition())) | ||
| long pos = encodedPosition(); | ||
| if (Cursor.isRootPosition(pos) || Cursor.compare(pos, matchingPositionAtRoot) == 0) |
There was a problem hiding this comment.
I don't understand this change either. If we are at the root, we should present the deletion branch. If not, we should present the live path only.
There is a problem with the existing code, though, when the input cursor could return deletion branches (e.g. for prefixedBySeparately(prefix, false)) which I'm fixing in the base PR.
| if (!Cursor.isExhausted(nextPosition)) | ||
| prepareNextPosition(currentPosition); | ||
| return currentPosition; | ||
| return encodedPosition(); |
There was a problem hiding this comment.
Could we use the same adjustNextPosition mechanism as SingletonOrderedCursor, which will also simplify the latter?
| } | ||
| else | ||
| return checkOverride(source.skipTo(encodedSkipPosition)); | ||
| checkOverride(source.skipTo(encodedSkipPosition)); |
There was a problem hiding this comment.
This should return because checkOverride already calls updateCurrentPosition().
Introduces a metadata flag in encoded cursor positions to indicate whether a node potentially carries content. This allows the trie iteration infrastructure to skip expensive content resolution calls for nodes that are guaranteed to be structural.
Those are the results of 50 benchmarks averaged:
Benchmark average before:
Benchmark average after:
Flamegraphs for TrieMemtable seem to backup those results eg.


Before:
After: