-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
LUCENE-9536: Optimize OrdinalMap when one segment contains all distinct values. #1948
Conversation
…ct values. For doc values that are not too high cardinality, it is common for some large segments to contain all distinct values. In this case, we can check if the first segment ords map perfectly to global ords, and if so store the global ord deltas and first segment indices as `LongValues.ZEROES` to save some space.
I used
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Julie, this looks good to me, I only left some minor comments. Do you know if we already have tests that exercise this optimization?
if (ordDeltaBits.length > 0 && ordDeltaBits[0] == 0L && ordDeltas[0].size() == this.valueCount) { | ||
this.firstSegments = LongValues.ZEROES; | ||
this.globalOrdDeltas = LongValues.ZEROES; | ||
ramBytesUsed += RamUsageEstimator.shallowSizeOf(LongValues.ZEROES); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could ignore it completely from ramBytesUsed
, since this singleton is allocated anyway, regardless of whether the optimization uses it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this to address a failure in TestOrdinalMap
. But now I see it makes more sense to modify the test !
resources.add(Accountables.namedAccountable("segment map", segmentMap)); | ||
// TODO: would be nice to return actual child segment deltas too, but the optimizations are confusing | ||
// TODO: would be nice to return the ordinal and segment maps too, but it's not straightforward | ||
// because of optimizations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could be do something like if (firstSegments != LongValues.ZEROES) { resources.add(Accountables.namedAccountable("first segments", firstSegments)); }
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we'd need a cast here, since LongValues
doesn't implement Accountable
. Alternatively, we could consider a bigger change to have LongValues
implement Accountable
.
Update: I just saw LUCENE-9387, it probably doesn't make sense to increase usage of Accountable
.
|
||
// If the first segment contains all of the global ords, then we can apply a small optimization | ||
// and hardcode the first segments and global ord deltas as all zeroes. | ||
if (ordDeltaBits.length > 0 && ordDeltaBits[0] == 0L && ordDeltas[0].size() == this.valueCount) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm why only the first segment? Couldn't it be the 3rd segment, in addition, that matches the global ords?
Edit: ahh OK I understand now -- this opto is indeed specific to the first segment, so we can store this.firstSegments
as all 0s. Good!
Do we (somewhere, couldn't find it here) pre-sort all segments by the cardinality descending? Then we could know all segments that meet this optimization are at the start of the segments list, and possibly building the ordinal map is faster (not sure). But then we would need to un-sort in the end to return the final OrdinalMap
. But it might enable this opto to apply more often, except, I think we would then need an additional dereference on lookup, hmm.
Does our PackedLongValues.monotonicBuilder
already optimize for the case where it is all 0s, for the case where another segment (not the first) has all the global values as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we (somewhere, couldn't find it here) pre-sort all segments by the cardinality descending?
We do in fact -- the segments are sorted by 'weight', which in all call sites corresponds to the number of unique terms. This was added in LUCENE-5782.
Does our PackedLongValues.monotonicBuilder already optimize for the case where it is all 0s, for the case where another segment (not the first) has all the global values as well?
When constructing the individual PackedInts.Reader
instances, we do identify the all 0s case and use the lightweight PackedInts.NullReader
. It's great we optimize that case, but it does mean this PR doesn't make an enormous space difference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we (somewhere, couldn't find it here) pre-sort all segments by the cardinality descending?
We do in fact -- the segments are sorted by 'weight', which in all call sites corresponds to the number of unique terms. This was added in LUCENE-5782.
Ahh, so then we know the first segment will indeed have the most unique terms, and therefore the highest chance of having "all 0s" ord deltas.
I think 2nd and 3rd segments also might have all 0s ord deltas? But we can try to optimize that in a followon issue ... progress not perfection!
Does our PackedLongValues.monotonicBuilder already optimize for the case where it is all 0s, for the case where another segment (not the first) has all the global values as well?
When constructing the individual
PackedInts.Reader
instances, we do identify the all 0s case and use the lightweightPackedInts.NullReader
. It's great we optimize that case, but it does mean this PR doesn't make an enormous space difference.
Got it. Well, it's great that all these layers optimize :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think 2nd and 3rd segments also might have all 0s ord deltas? But we can try to optimize that in a followon issue ... progress not perfection!
I wonder if you are confused here, the proposed changes optimize the mapping from global ordinals to the ordinals of one arbitrary segment. When a segment has all value, we can simplify by always picking this segment, but there is no need to optimize this for the 2nd or 3rd segments, since we only need to be able to translate global ordinals to the ordinal of a single segment. Or maybe I'm the one confused by what you were suggesting. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha! Sorry, I was indeed confused ;)
This is to enable "retrieve BytesRef
for this global ordinal" use-case, right? For that, we first pick a segment to use (the first one also containing that BytesRef
), then map to its segment-local ordinal, then retrieve the BytesRef
for that using the existing doc values API for that segment.
We do not (need to, nor) expose an API today to "retrieve segment N's ordinal corresponding to global ordinal M". Only the reverse direction (segment N's ordinal M maps to global ordinal O).
I think I understand now!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to enable "retrieve BytesRef for this global ordinal" use-case, right?
Right!
We do not (need to, nor) expose an API today to "retrieve segment N's ordinal corresponding to global ordinal M"
Correct.
this.valueCount = globalOrd; | ||
|
||
// If the first segment contains all of the global ords, then we can apply a small optimization | ||
// and hardcode the first segments and global ord deltas as all zeroes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Insert possessive quote (first segment's
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the possessive quote gives the right meaning? Perhaps I could say 'first segment indices' here to be more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I thought it was the first segment's deltas as all zeros
and alos the global ord deltas as all zeros
? But I'm OK with just rewording it to make it less controversial, or even just leaving this wording!
I think several randomized tests will hit it, for example |
@jpountz @mikemccand no rush, but this is ready for another look. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @jtibshirani, this opto makes sense to me now and looks great!
|
||
// If the first segment contains all of the global ords, then we can apply a small optimization | ||
// and hardcode the first segments and global ord deltas as all zeroes. | ||
if (ordDeltaBits.length > 0 && ordDeltaBits[0] == 0L && ordDeltas[0].size() == this.valueCount) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha! Sorry, I was indeed confused ;)
This is to enable "retrieve BytesRef
for this global ordinal" use-case, right? For that, we first pick a segment to use (the first one also containing that BytesRef
), then map to its segment-local ordinal, then retrieve the BytesRef
for that using the existing doc values API for that segment.
We do not (need to, nor) expose an API today to "retrieve segment N's ordinal corresponding to global ordinal M". Only the reverse direction (segment N's ordinal M maps to global ordinal O).
I think I understand now!
import java.io.IOException; | ||
import java.lang.reflect.Field; | ||
import java.util.HashMap; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does (would) https://issues.apache.org/jira/browse/LUCENE-9564 enforce import ordering check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would avoid this sort of change (I use IntelliJ which autoformats java
imports at the end).
…ct values. (#1948) For doc values that are not too high cardinality, it is common for some large segments to contain all distinct values. In this case, we can check if the first segment ords map perfectly to global ords, and if so store the global ord deltas and first segment indices as `LongValues.ZEROES` to save some space.
…ct values. (apache#1948) LUCENE-9536: Optimize OrdinalMap when one segment contains all distinct values. For doc values that are not too high cardinality, it is common for some large segments to contain all distinct values. In this case, we can check if the first segment ords map perfectly to global ords, and if so store the global ord deltas and first segment indices as `LongValues.ZEROES` to save some space.
…ct values. (apache#1948) LUCENE-9536: Optimize OrdinalMap when one segment contains all distinct values. For doc values that are not too high cardinality, it is common for some large segments to contain all distinct values. In this case, we can check if the first segment ords map perfectly to global ords, and if so store the global ord deltas and first segment indices as `LongValues.ZEROES` to save some space.
For doc values that are not too high cardinality, it is common for some large
segments to contain all distinct values. In this case, we can check if the first
segment ords map perfectly to global ords, and if so store the global ord deltas
and first segment indices as
LongValues.ZEROES
to save some space.