Skip to content

Commit 6245611

Browse files
uschindlerjpountz
andauthored
Backport of #15116: Rewrite of the GroupVInt optimization without lambdas, varhandles and no code in subclasses (#15138)
* Backport of: Revision: ebfd863 Author: Uwe Schindler <[email protected]> Date: 25.08.2025 22:58:28 Message: Rewrite of the GroupVInt optimization without lambdas, varhandles and no code in subclasses (#15116) * Rewrite of the GroupVInt optimization without lamdas, varhandles and no code in subclasses * Remove optimized flag and duplicate code * apply suggestions/fixes: fix maximum size; cleanup baseline method and hide internal details * Apply suggestions from code review Co-authored-by: Adrien Grand <[email protected]> * add CHANGES.txt * more docs and a visibility change for previously public method --------- Co-authored-by: Adrien Grand <[email protected]>
1 parent de66961 commit 6245611

File tree

8 files changed

+95
-115
lines changed

8 files changed

+95
-115
lines changed

lucene/CHANGES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ Optimizations
168168

169169
* GITHUB#15045: Use FixedBitSet#cardinality for counting liveDocs in CheckIndex (Zhang Chao)
170170

171+
* GITHUB#15116, GITHUB#15138: Rewrite of the GroupVInt optimization without lambdas, varhandles
172+
and no code in subclasses. The optimization now auto-detects if an IndexInput supports random access
173+
and uses an optimized branchless approach. Any subclasses that have implemented the optimized method
174+
need to remove it as it will disappear in Lucene 11. (Uwe Schindler)
175+
171176
Changes in Runtime Behavior
172177
---------------------
173178
* GITHUB#14823: Decrease TieredMergePolicy's default number of segments per

lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/GroupVIntBenchmark.java

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@
2323
import java.util.concurrent.TimeUnit;
2424
import org.apache.lucene.store.ByteArrayDataInput;
2525
import org.apache.lucene.store.ByteArrayDataOutput;
26-
import org.apache.lucene.store.ByteBuffersDataInput;
2726
import org.apache.lucene.store.ByteBuffersDataOutput;
28-
import org.apache.lucene.store.DataInput;
27+
import org.apache.lucene.store.ByteBuffersDirectory;
2928
import org.apache.lucene.store.Directory;
3029
import org.apache.lucene.store.IOContext;
3130
import org.apache.lucene.store.IndexInput;
@@ -50,8 +49,8 @@
5049
@BenchmarkMode(Mode.Throughput)
5150
@OutputTimeUnit(TimeUnit.MICROSECONDS)
5251
@State(Scope.Benchmark)
53-
@Warmup(iterations = 3, time = 3)
54-
@Measurement(iterations = 5, time = 5)
52+
@Warmup(iterations = 4, time = 8)
53+
@Measurement(iterations = 5, time = 20)
5554
@Fork(
5655
value = 1,
5756
jvmArgsPrepend = {"--add-modules=jdk.unsupported"})
@@ -92,7 +91,7 @@ public class GroupVIntBenchmark {
9291
IndexInput mmapGVIntIn;
9392
IndexInput nioGVIntIn;
9493
IndexInput mmapVIntIn;
95-
ByteBuffersDataInput byteBuffersGVIntIn;
94+
IndexInput byteBuffersGVIntIn;
9695

9796
ByteArrayDataInput byteArrayVIntIn;
9897
ByteArrayDataInput byteArrayGVIntIn;
@@ -125,9 +124,11 @@ void initNioInput(int[] docs) throws Exception {
125124
}
126125

127126
void initByteBuffersInput(int[] docs) throws Exception {
128-
ByteBuffersDataOutput buffer = new ByteBuffersDataOutput();
129-
buffer.writeGroupVInts(docs, docs.length);
130-
byteBuffersGVIntIn = buffer.toDataInput();
127+
Directory dir = new ByteBuffersDirectory();
128+
IndexOutput out = dir.createOutput("gvint", IOContext.DEFAULT);
129+
out.writeGroupVInts(docs, docs.length);
130+
out.close();
131+
byteBuffersGVIntIn = dir.openInput("gvint", IOContext.DEFAULT);
131132
}
132133

133134
void initMMapInput(int[] docs) throws Exception {
@@ -145,16 +146,6 @@ void initMMapInput(int[] docs) throws Exception {
145146
mmapVIntIn = dir.openInput("vint", IOContext.DEFAULT);
146147
}
147148

148-
private void readGroupVIntsBaseline(DataInput in, int[] dst, int limit) throws IOException {
149-
int i;
150-
for (i = 0; i <= limit - 4; i += 4) {
151-
GroupVIntUtil.readGroupVInt(in, dst, i);
152-
}
153-
for (; i < limit; ++i) {
154-
dst[i] = in.readVInt();
155-
}
156-
}
157-
158149
@Setup(Level.Trial)
159150
public void init() throws Exception {
160151
Random r = new Random(0);
@@ -193,7 +184,7 @@ public void benchMMapDirectoryInputs_readGroupVInt(Blackhole bh) throws IOExcept
193184
@Benchmark
194185
public void benchMMapDirectoryInputs_readGroupVIntBaseline(Blackhole bh) throws IOException {
195186
mmapGVIntIn.seek(0);
196-
this.readGroupVIntsBaseline(mmapGVIntIn, values, size);
187+
GroupVIntUtil.readGroupVInts$Baseline(mmapGVIntIn, values, size);
197188
bh.consume(values);
198189
}
199190

@@ -223,7 +214,7 @@ public void benchNIOFSDirectoryInputs_readGroupVInt(Blackhole bh) throws IOExcep
223214
@Benchmark
224215
public void benchNIOFSDirectoryInputs_readGroupVIntBaseline(Blackhole bh) throws IOException {
225216
nioGVIntIn.seek(0);
226-
this.readGroupVIntsBaseline(nioGVIntIn, values, size);
217+
GroupVIntUtil.readGroupVInts$Baseline(nioGVIntIn, values, size);
227218
bh.consume(values);
228219
}
229220

@@ -237,7 +228,7 @@ public void benchByteBuffersIndexInput_readGroupVInt(Blackhole bh) throws IOExce
237228
@Benchmark
238229
public void benchByteBuffersIndexInput_readGroupVIntBaseline(Blackhole bh) throws IOException {
239230
byteBuffersGVIntIn.seek(0);
240-
this.readGroupVIntsBaseline(byteBuffersGVIntIn, values, size);
231+
GroupVIntUtil.readGroupVInts$Baseline(byteBuffersGVIntIn, values, size);
241232
bh.consume(values);
242233
}
243234

lucene/core/src/java/org/apache/lucene/store/BufferedIndexInput.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.io.IOException;
2121
import java.nio.ByteBuffer;
2222
import java.nio.ByteOrder;
23-
import org.apache.lucene.util.GroupVIntUtil;
2423

2524
/** Base implementation class for buffered {@link IndexInput}. */
2625
public abstract class BufferedIndexInput extends IndexInput implements RandomAccessInput {
@@ -150,16 +149,6 @@ public final int readInt() throws IOException {
150149
}
151150
}
152151

153-
@Override
154-
public void readGroupVInt(int[] dst, int offset) throws IOException {
155-
final int len =
156-
GroupVIntUtil.readGroupVInt(
157-
this, buffer.remaining(), p -> buffer.getInt((int) p), buffer.position(), dst, offset);
158-
if (len > 0) {
159-
buffer.position(buffer.position() + len);
160-
}
161-
}
162-
163152
@Override
164153
public final long readLong() throws IOException {
165154
if (Long.BYTES <= buffer.remaining()) {

lucene/core/src/java/org/apache/lucene/store/ByteBuffersDataInput.java

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import java.util.Locale;
3030
import java.util.stream.Collectors;
3131
import org.apache.lucene.util.Accountable;
32-
import org.apache.lucene.util.GroupVIntUtil;
3332
import org.apache.lucene.util.RamUsageEstimator;
3433

3534
/**
@@ -203,25 +202,6 @@ public long readLong() throws IOException {
203202
}
204203
}
205204

206-
@Override
207-
public void readGroupVInt(int[] dst, int offset) throws IOException {
208-
final ByteBuffer block = blocks[blockIndex(pos)];
209-
final int blockOffset = blockOffset(pos);
210-
// We MUST save the return value to local variable, could not use pos += readGroupVInt(...).
211-
// because `pos +=` in java will move current value(not address) of pos to register first,
212-
// then call the function, but we will update pos value in function via readByte(), then
213-
// `pos +=` will use an old pos value plus return value, thereby missing 1 byte.
214-
final int len =
215-
GroupVIntUtil.readGroupVInt(
216-
this,
217-
block.limit() - blockOffset,
218-
p -> block.getInt((int) p),
219-
blockOffset,
220-
dst,
221-
offset);
222-
pos += len;
223-
}
224-
225205
@Override
226206
public long length() {
227207
return length;

lucene/core/src/java/org/apache/lucene/store/ByteBuffersIndexInput.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,6 @@ public void readLongs(long[] dst, int offset, int length) throws IOException {
205205
in.readLongs(dst, offset, length);
206206
}
207207

208-
@Override
209-
public void readGroupVInt(int[] dst, int offset) throws IOException {
210-
ensureOpen();
211-
in.readGroupVInt(dst, offset);
212-
}
213-
214208
@Override
215209
public IndexInput clone() {
216210
ensureOpen();

lucene/core/src/java/org/apache/lucene/store/DataInput.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import java.util.TreeMap;
2828
import java.util.TreeSet;
2929
import org.apache.lucene.util.BitUtil;
30-
import org.apache.lucene.util.GroupVIntUtil;
3130

3231
/**
3332
* Abstract base class for performing read operations of Lucene's low-level data types.
@@ -100,13 +99,19 @@ public int readInt() throws IOException {
10099
}
101100

102101
/**
103-
* Override if you have an efficient implementation. In general this is when the input supports
104-
* random access.
102+
* Legacy: This method allowed to implement GroupVInt encoding in a more efficient way when this
103+
* implementation supports random access. It is no longer called by Lucene's code.
105104
*
105+
* <p>If you have implemented this method, simply remove it!
106+
*
107+
* @throws UnsupportedOperationException (always)
106108
* @lucene.experimental
109+
* @deprecated This method is no longer called. It is only kept for backwards compatibility
107110
*/
111+
@Deprecated
108112
public void readGroupVInt(int[] dst, int offset) throws IOException {
109-
GroupVIntUtil.readGroupVInt(this, dst, offset);
113+
throw new UnsupportedOperationException(
114+
"This method is no longer called by Lucene's code and custom code should also not call it.");
110115
}
111116

112117
/**

lucene/core/src/java/org/apache/lucene/util/GroupVIntUtil.java

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@
1919
import java.io.IOException;
2020
import org.apache.lucene.store.DataInput;
2121
import org.apache.lucene.store.DataOutput;
22+
import org.apache.lucene.store.IndexInput;
23+
import org.apache.lucene.store.RandomAccessInput;
2224

2325
/**
2426
* This class contains utility methods and constants for group varint
2527
*
2628
* @lucene.internal
2729
*/
2830
public final class GroupVIntUtil {
29-
// the maximum length of a single group-varint is 4 integers + 1 byte flag.
30-
public static final int MAX_LENGTH_PER_GROUP = 17;
31+
// the maximum length of a single group-varint is 1 byte flag and 4 integers.
32+
public static final int MAX_LENGTH_PER_GROUP = Byte.BYTES + 4 * Integer.BYTES;
3133

3234
private static final int[] INT_MASKS = new int[] {0xFF, 0xFFFF, 0xFFFFFF, ~0};
3335

@@ -41,21 +43,25 @@ public final class GroupVIntUtil {
4143
public static void readGroupVInts(DataInput in, int[] dst, int limit) throws IOException {
4244
int i;
4345
for (i = 0; i <= limit - 4; i += 4) {
44-
in.readGroupVInt(dst, i);
46+
readGroupVInt(in, dst, i);
4547
}
4648
for (; i < limit; ++i) {
4749
dst[i] = in.readVInt();
4850
}
4951
}
5052

5153
/**
52-
* Default implementation of read single group, for optimal performance, you should use {@link
53-
* GroupVIntUtil#readGroupVInts(DataInput, int[], int)} instead.
54+
* Read single group, for optimal performance, you should use {@link
55+
* GroupVIntUtil#readGroupVInts(DataInput, int[], int)} to decode a full group including tails
56+
* instead.
5457
*
5558
* @param in the input to use to read data.
5659
* @param dst the array to read ints into.
5760
* @param offset the offset in the array to start storing ints.
61+
* @deprecated Use {@link #readGroupVInts(DataInput, int[], int)} to decode a full group including
62+
* tails instead
5863
*/
64+
@Deprecated
5965
public static void readGroupVInt(DataInput in, int[] dst, int offset) throws IOException {
6066
final int flag = in.readByte() & 0xFF;
6167

@@ -64,6 +70,53 @@ public static void readGroupVInt(DataInput in, int[] dst, int offset) throws IOE
6470
final int n3Minus1 = (flag >> 2) & 0x03;
6571
final int n4Minus1 = flag & 0x03;
6672

73+
// if our DataInput implements RandomAccessInput for absolute access and IndexInput for seeking,
74+
// we use a branch-less implementation:
75+
if (in instanceof RandomAccessInput rin && in instanceof IndexInput iin) {
76+
long pos = iin.getFilePointer();
77+
if (iin.length() - pos >= 4 * Integer.BYTES) {
78+
dst[offset] = rin.readInt(pos) & INT_MASKS[n1Minus1];
79+
pos += 1 + n1Minus1;
80+
dst[offset + 1] = rin.readInt(pos) & INT_MASKS[n2Minus1];
81+
pos += 1 + n2Minus1;
82+
dst[offset + 2] = rin.readInt(pos) & INT_MASKS[n3Minus1];
83+
pos += 1 + n3Minus1;
84+
dst[offset + 3] = rin.readInt(pos) & INT_MASKS[n4Minus1];
85+
pos += 1 + n4Minus1;
86+
87+
iin.seek(pos);
88+
return;
89+
}
90+
}
91+
92+
// fall-through: default impl
93+
dst[offset] = readIntInGroup(in, n1Minus1);
94+
dst[offset + 1] = readIntInGroup(in, n2Minus1);
95+
dst[offset + 2] = readIntInGroup(in, n3Minus1);
96+
dst[offset + 3] = readIntInGroup(in, n4Minus1);
97+
}
98+
99+
/** DO not use! Only visible for benchmarking purposes! */
100+
public static void readGroupVInts$Baseline(DataInput in, int[] dst, int limit)
101+
throws IOException {
102+
int i;
103+
for (i = 0; i <= limit - 4; i += 4) {
104+
readGroupVInt$Baseline(in, dst, i);
105+
}
106+
for (; i < limit; ++i) {
107+
dst[i] = in.readVInt();
108+
}
109+
}
110+
111+
private static void readGroupVInt$Baseline(DataInput in, int[] dst, int offset)
112+
throws IOException {
113+
final int flag = in.readByte() & 0xFF;
114+
115+
final int n1Minus1 = flag >> 6;
116+
final int n2Minus1 = (flag >> 4) & 0x03;
117+
final int n3Minus1 = (flag >> 2) & 0x03;
118+
final int n4Minus1 = flag & 0x03;
119+
67120
dst[offset] = readIntInGroup(in, n1Minus1);
68121
dst[offset + 1] = readIntInGroup(in, n2Minus1);
69122
dst[offset + 2] = readIntInGroup(in, n3Minus1);
@@ -86,7 +139,11 @@ private static int readIntInGroup(DataInput in, int numBytesMinus1) throws IOExc
86139
/**
87140
* Provides an abstraction for read int values, so that decoding logic can be reused in different
88141
* DataInput.
142+
*
143+
* @deprecated No longer used.
144+
* @see #readGroupVInt(DataInput, long, IntReader, long, int[], int)
89145
*/
146+
@Deprecated
90147
@FunctionalInterface
91148
public static interface IntReader {
92149
int read(long v);
@@ -96,40 +153,17 @@ public static interface IntReader {
96153
* Faster implementation of read single group, It read values from the buffer that would not cross
97154
* boundaries.
98155
*
99-
* @param in the input to use to read data.
100-
* @param remaining the number of remaining bytes allowed to read for current block/segment.
101-
* @param reader the supplier of read int.
102-
* @param pos the start pos to read from the reader.
103-
* @param dst the array to read ints into.
104-
* @param offset the offset in the array to start storing ints.
105-
* @return the number of bytes read excluding the flag. this indicates the number of positions
106-
* should to be increased for caller, it is 0 or positive number and less than {@link
107-
* #MAX_LENGTH_PER_GROUP}
156+
* @throws UnsupportedOperationException (always)
157+
* @deprecated Don't call this method, it is no longer implemented. It was only provided for
158+
* custom {@link DataInput} subclasses to allow to provide a faster GroupVInt encoding when
159+
* random access is provided by the underlying storage. This is now obsolete, any
160+
* implementation calling this should be removed in the calling class.
108161
*/
162+
@Deprecated
109163
public static int readGroupVInt(
110164
DataInput in, long remaining, IntReader reader, long pos, int[] dst, int offset)
111165
throws IOException {
112-
if (remaining < MAX_LENGTH_PER_GROUP) {
113-
readGroupVInt(in, dst, offset);
114-
return 0;
115-
}
116-
final int flag = in.readByte() & 0xFF;
117-
final long posStart = ++pos; // exclude the flag bytes, the position has updated via readByte().
118-
final int n1Minus1 = flag >> 6;
119-
final int n2Minus1 = (flag >> 4) & 0x03;
120-
final int n3Minus1 = (flag >> 2) & 0x03;
121-
final int n4Minus1 = flag & 0x03;
122-
123-
// This code path has fewer conditionals and tends to be significantly faster in benchmarks
124-
dst[offset] = reader.read(pos) & INT_MASKS[n1Minus1];
125-
pos += 1 + n1Minus1;
126-
dst[offset + 1] = reader.read(pos) & INT_MASKS[n2Minus1];
127-
pos += 1 + n2Minus1;
128-
dst[offset + 2] = reader.read(pos) & INT_MASKS[n3Minus1];
129-
pos += 1 + n3Minus1;
130-
dst[offset + 3] = reader.read(pos) & INT_MASKS[n4Minus1];
131-
pos += 1 + n4Minus1;
132-
return (int) (pos - posStart);
166+
throw new UnsupportedOperationException("No longer implemented.");
133167
}
134168

135169
private static int numBytes(int v) {

lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.apache.lucene.util.ArrayUtil;
3030
import org.apache.lucene.util.BitUtil;
3131
import org.apache.lucene.util.Constants;
32-
import org.apache.lucene.util.GroupVIntUtil;
3332
import org.apache.lucene.util.IOConsumer;
3433

3534
/**
@@ -455,23 +454,6 @@ public byte readByte(long pos) throws IOException {
455454
}
456455
}
457456

458-
@Override
459-
public void readGroupVInt(int[] dst, int offset) throws IOException {
460-
try {
461-
final int len =
462-
GroupVIntUtil.readGroupVInt(
463-
this,
464-
curSegment.byteSize() - curPosition,
465-
p -> curSegment.get(LAYOUT_LE_INT, p),
466-
curPosition,
467-
dst,
468-
offset);
469-
curPosition += len;
470-
} catch (NullPointerException | IllegalStateException e) {
471-
throw alreadyClosed(e);
472-
}
473-
}
474-
475457
@Override
476458
public void readBytes(long pos, byte[] b, int offset, int len) throws IOException {
477459
try {

0 commit comments

Comments
 (0)