Skip to content

Commit 42e71d8

Browse files
authored
Use SIMD in fixed-cardinality SortedNumericDocValues rangeIntoBitSet() (#16283)
Dense fixed-cardinality sorted numeric values can evaluate range blocks with the vectorization provider when the flattened value layout is raw and contiguous. Keep the optimization gated to layouts that benchmark well and retain scalar fallback behavior for other encodings.
1 parent 9b68f63 commit 42e71d8

5 files changed

Lines changed: 105 additions & 25 deletions

File tree

lucene/CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ Optimizations
320320

321321
* GITHUB#16160: Improve numeric doc values range query performance for dense fields that use GCD or delta encoding. (Costin Leau)
322322

323+
* GITHUB#16283: Use Panama Vector API to SIMD-evaluate fixed-cardinality sorted numeric range queries in
324+
rangeIntoBitSet. (Costin Leau)
325+
323326
Bug Fixes
324327
---------------------
325328
(No changes)

lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -562,29 +562,6 @@ private static int fixedCardinality(
562562
return (int) cardinality;
563563
}
564564

565-
private static void sortedNumericScalarRangeIntoBitSet(
566-
LongValues values,
567-
int fromDoc,
568-
int toDoc,
569-
int cardinality,
570-
long minValue,
571-
long maxValue,
572-
FixedBitSet bitSet,
573-
int offset) {
574-
for (int doc = fromDoc; doc < toDoc; doc++) {
575-
long valueOffset = (long) doc * cardinality;
576-
for (int i = 0; i < cardinality; i++) {
577-
long value = values.get(valueOffset + i);
578-
if (value >= minValue) {
579-
if (value <= maxValue) {
580-
bitSet.set(doc - offset);
581-
}
582-
break;
583-
}
584-
}
585-
}
586-
}
587-
588565
private static boolean sortedNumericMatchesRange(
589566
LongValues values, long start, long end, long minValue, long maxValue) {
590567
for (long valueOffset = start; valueOffset < end; valueOffset++) {
@@ -2024,7 +2001,7 @@ public void rangeIntoBitSet(
20242001
}
20252002
int cardinality = denseFixedCardinality;
20262003
if (cardinality > 1) {
2027-
sortedNumericScalarRangeIntoBitSet(
2004+
DOC_VALUES_RANGE_SUPPORT.sortedNumericRangeIntoBitSet(
20282005
values, fromDoc, endDoc, cardinality, minValue, maxValue, bitSet, offset);
20292006
return;
20302007
}

lucene/core/src/java/org/apache/lucene/internal/vectorization/DocValuesRangeSupport.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,36 @@ void rangeIntoBitSet(
5050
long maxValue,
5151
FixedBitSet bitSet,
5252
int offset);
53+
54+
/**
55+
* Fills {@code bitSet} with docs in {@code [fromDoc, toDoc)} whose sorted numeric values contain
56+
* at least one value in {@code [minValue, maxValue]}.
57+
*
58+
* <p>This method only supports <b>fixed-cardinality</b> fields where every document has exactly
59+
* {@code cardinality} values stored contiguously starting at index {@code doc * cardinality}.
60+
*
61+
* @param cardinality number of values per document (must be &gt; 0)
62+
*/
63+
default void sortedNumericRangeIntoBitSet(
64+
LongValues values,
65+
int fromDoc,
66+
int toDoc,
67+
int cardinality,
68+
long minValue,
69+
long maxValue,
70+
FixedBitSet bitSet,
71+
int offset) {
72+
for (int doc = fromDoc; doc < toDoc; doc++) {
73+
long valueOffset = (long) doc * cardinality;
74+
for (int i = 0; i < cardinality; i++) {
75+
long value = values.get(valueOffset + i);
76+
if (value >= minValue) {
77+
if (value <= maxValue) {
78+
bitSet.set(doc - offset);
79+
}
80+
break;
81+
}
82+
}
83+
}
84+
}
5385
}

lucene/core/src/java25/org/apache/lucene/internal/vectorization/PanamaDocValuesRangeSupport.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,61 @@ public void rangeIntoBitSet(
6969
}
7070
}
7171
}
72+
73+
@Override
74+
public void sortedNumericRangeIntoBitSet(
75+
LongValues values,
76+
int fromDoc,
77+
int toDoc,
78+
int cardinality,
79+
long minValue,
80+
long maxValue,
81+
FixedBitSet bitSet,
82+
int offset) {
83+
assert cardinality > 0 : "cardinality must be positive: " + cardinality;
84+
final int vectorLen = LONG_SPECIES.length();
85+
final int docsPerVector = vectorLen / cardinality;
86+
if (docsPerVector == 0 || vectorLen % cardinality != 0) {
87+
DocValuesRangeSupport.super.sortedNumericRangeIntoBitSet(
88+
values, fromDoc, toDoc, cardinality, minValue, maxValue, bitSet, offset);
89+
return;
90+
}
91+
92+
final long[] scratch = new long[vectorLen];
93+
final int vectorDocEnd = fromDoc + (toDoc - fromDoc) / docsPerVector * docsPerVector;
94+
int doc = fromDoc;
95+
for (; doc < vectorDocEnd; doc += docsPerVector) {
96+
long valueOffset = (long) doc * cardinality;
97+
for (int lane = 0; lane < vectorLen; lane++) {
98+
scratch[lane] = values.get(valueOffset + lane);
99+
}
100+
LongVector vector = LongVector.fromArray(LONG_SPECIES, scratch, 0);
101+
// Flat range check on all lanes. Equivalent to the scalar early-break on sorted values:
102+
// a value outside [min,max] that is >= min must be > max, so its lane stays unset, and
103+
// collapseToDocMask OR-reduces lanes per doc to match "any value in range" semantics.
104+
VectorMask<Long> inRange =
105+
vector
106+
.compare(VectorOperators.GE, minValue)
107+
.and(vector.compare(VectorOperators.LE, maxValue));
108+
long docMask = collapseToDocMask(inRange.toLong(), cardinality, docsPerVector);
109+
if (docMask != 0) {
110+
bitSet.orMask(doc - offset, docMask, docsPerVector);
111+
}
112+
}
113+
114+
DocValuesRangeSupport.super.sortedNumericRangeIntoBitSet(
115+
values, doc, toDoc, cardinality, minValue, maxValue, bitSet, offset);
116+
}
117+
118+
private static long collapseToDocMask(long valueMask, int cardinality, int docsPerVector) {
119+
final long valueMaskPerDoc = (1L << cardinality) - 1L;
120+
long docMask = 0L;
121+
for (int d = 0; d < docsPerVector; d++) {
122+
long perDoc = (valueMask >>> (d * cardinality)) & valueMaskPerDoc;
123+
if (perDoc != 0) {
124+
docMask |= 1L << d;
125+
}
126+
}
127+
return docMask;
128+
}
72129
}

lucene/core/src/test/org/apache/lucene/search/TestSkipBlockRangeIteratorIntoBitSet.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,14 +764,25 @@ public void testSortedNumericRangeIntoBitSetSparseVariableCardinality() throws E
764764

765765
private void doTestSortedNumericRangeIntoBitSet(boolean dense, boolean fixedCardinality)
766766
throws Exception {
767+
doTestSortedNumericRangeIntoBitSet(dense, fixedCardinality, 4);
768+
}
769+
770+
public void testSortedNumericRangeIntoBitSetVaryingCardinality() throws Exception {
771+
for (int cardinality : new int[] {2, 3, 4, 5, 7, 8}) {
772+
doTestSortedNumericRangeIntoBitSet(true, true, cardinality);
773+
}
774+
}
775+
776+
private void doTestSortedNumericRangeIntoBitSet(
777+
boolean dense, boolean fixedCardinality, int fixedCardinalityValue) throws Exception {
767778
int numDocs = 4096 * 2;
768779
try (Directory dir = newDirectory()) {
769780
IndexWriterConfig iwc = new IndexWriterConfig().setCodec(new Lucene104Codec());
770781
try (IndexWriter w = new IndexWriter(dir, iwc)) {
771782
for (int docID = 0; docID < numDocs; docID++) {
772783
Document doc = new Document();
773784
if (dense || docID % 3 != 0) {
774-
int valueCount = fixedCardinality ? 4 : 1 + (docID & 3);
785+
int valueCount = fixedCardinality ? fixedCardinalityValue : 1 + (docID & 3);
775786
long firstValue = (docID * 13L) % 100;
776787
for (int i = 0; i < valueCount; i++) {
777788
doc.add(SortedNumericDocValuesField.indexedField("sn", firstValue + i * 3L));

0 commit comments

Comments
 (0)