Skip to content

Commit 58eb68c

Browse files
authored
Speed up BQVectorUtils#packAsBinary (#132820)
Speed up the method process 8 elements at a time and manually pack the bits.
1 parent 67e9be0 commit 58eb68c

File tree

3 files changed

+128
-1
lines changed

3 files changed

+128
-1
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
package org.elasticsearch.benchmark.vector;
10+
11+
import org.elasticsearch.common.logging.LogConfigurator;
12+
import org.elasticsearch.index.codec.vectors.BQVectorUtils;
13+
import org.openjdk.jmh.annotations.Benchmark;
14+
import org.openjdk.jmh.annotations.BenchmarkMode;
15+
import org.openjdk.jmh.annotations.Fork;
16+
import org.openjdk.jmh.annotations.Measurement;
17+
import org.openjdk.jmh.annotations.Mode;
18+
import org.openjdk.jmh.annotations.OutputTimeUnit;
19+
import org.openjdk.jmh.annotations.Param;
20+
import org.openjdk.jmh.annotations.Scope;
21+
import org.openjdk.jmh.annotations.Setup;
22+
import org.openjdk.jmh.annotations.State;
23+
import org.openjdk.jmh.annotations.Warmup;
24+
import org.openjdk.jmh.infra.Blackhole;
25+
26+
import java.io.IOException;
27+
import java.util.Random;
28+
import java.util.concurrent.TimeUnit;
29+
30+
@BenchmarkMode(Mode.Throughput)
31+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
32+
@State(Scope.Benchmark)
33+
// first iteration is complete garbage, so make sure we really warmup
34+
@Warmup(iterations = 4, time = 1)
35+
// real iterations. not useful to spend tons of time here, better to fork more
36+
@Measurement(iterations = 5, time = 1)
37+
// engage some noise reduction
38+
@Fork(value = 1)
39+
public class PackAsBinaryBenchmark {
40+
41+
static {
42+
LogConfigurator.configureESLogging(); // native access requires logging to be initialized
43+
}
44+
45+
@Param({ "384", "782", "1024" })
46+
int dims;
47+
48+
int length;
49+
50+
int numVectors = 1000;
51+
52+
int[][] qVectors;
53+
byte[] packed;
54+
55+
@Setup
56+
public void setup() throws IOException {
57+
Random random = new Random(123);
58+
59+
this.length = BQVectorUtils.discretize(dims, 64) / 8;
60+
this.packed = new byte[length];
61+
62+
qVectors = new int[numVectors][dims];
63+
for (int[] qVector : qVectors) {
64+
for (int i = 0; i < dims; i++) {
65+
qVector[i] = random.nextInt(2);
66+
}
67+
}
68+
}
69+
70+
@Benchmark
71+
public void packAsBinary(Blackhole bh) {
72+
for (int i = 0; i < numVectors; i++) {
73+
BQVectorUtils.packAsBinary(qVectors[i], packed);
74+
bh.consume(packed);
75+
}
76+
}
77+
78+
@Benchmark
79+
public void packAsBinaryLegacy(Blackhole bh) {
80+
for (int i = 0; i < numVectors; i++) {
81+
BQVectorUtils.packAsBinaryLegacy(qVectors[i], packed);
82+
bh.consume(packed);
83+
}
84+
}
85+
}

server/src/main/java/org/elasticsearch/index/codec/vectors/BQVectorUtils.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public static boolean isUnitVector(float[] v) {
4040
return Math.abs(l1norm - 1.0d) <= EPSILON;
4141
}
4242

43-
public static void packAsBinary(int[] vector, byte[] packed) {
43+
public static void packAsBinaryLegacy(int[] vector, byte[] packed) {
4444
for (int i = 0; i < vector.length;) {
4545
byte result = 0;
4646
for (int j = 7; j >= 0 && i < vector.length; j--) {
@@ -54,6 +54,34 @@ public static void packAsBinary(int[] vector, byte[] packed) {
5454
}
5555
}
5656

57+
public static void packAsBinary(int[] vector, byte[] packed) {
58+
int limit = vector.length - 7;
59+
int i = 0;
60+
int index = 0;
61+
for (; i < limit; i += 8, index++) {
62+
assert vector[i] == 0 || vector[i] == 1;
63+
assert vector[i + 1] == 0 || vector[i + 1] == 1;
64+
assert vector[i + 2] == 0 || vector[i + 2] == 1;
65+
assert vector[i + 3] == 0 || vector[i + 3] == 1;
66+
assert vector[i + 4] == 0 || vector[i + 4] == 1;
67+
assert vector[i + 5] == 0 || vector[i + 5] == 1;
68+
assert vector[i + 6] == 0 || vector[i + 6] == 1;
69+
assert vector[i + 7] == 0 || vector[i + 7] == 1;
70+
int result = vector[i] << 7 | (vector[i + 1] << 6) | (vector[i + 2] << 5) | (vector[i + 3] << 4) | (vector[i + 4] << 3)
71+
| (vector[i + 5] << 2) | (vector[i + 6] << 1) | (vector[i + 7]);
72+
packed[index] = (byte) result;
73+
}
74+
if (i == vector.length) {
75+
return;
76+
}
77+
byte result = 0;
78+
for (int j = 7; j >= 0 && i < vector.length; i++, j--) {
79+
assert vector[i] == 0 || vector[i] == 1;
80+
result |= (byte) ((vector[i] & 1) << j);
81+
}
82+
packed[index] = result;
83+
}
84+
5785
public static int discretize(int value, int bucket) {
5886
return ((value + (bucket - 1)) / bucket) * bucket;
5987
}

server/src/test/java/org/elasticsearch/index/codec/vectors/BQVectorUtilsTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ public void testPackAsBinary() {
6464
assertArrayEquals(new byte[] { (byte) 0b11001010, (byte) 0b11100110 }, packed);
6565
}
6666

67+
public void testPackAsBinaryDuel() {
68+
int dims = random().nextInt(16, 2049);
69+
int[] toPack = new int[dims];
70+
for (int i = 0; i < dims; i++) {
71+
toPack[i] = random().nextInt(2);
72+
}
73+
int length = BQVectorUtils.discretize(dims, 64) / 8;
74+
byte[] packed = new byte[length];
75+
byte[] packedLegacy = new byte[length];
76+
BQVectorUtils.packAsBinaryLegacy(toPack, packedLegacy);
77+
BQVectorUtils.packAsBinary(toPack, packed);
78+
assertArrayEquals(packedLegacy, packed);
79+
}
80+
6781
public void testPadFloat() {
6882
assertArrayEquals(new float[] { 1, 2, 3, 4 }, BQVectorUtils.pad(new float[] { 1, 2, 3, 4 }, 4), DELTA);
6983
assertArrayEquals(new float[] { 1, 2, 3, 4 }, BQVectorUtils.pad(new float[] { 1, 2, 3, 4 }, 3), DELTA);

0 commit comments

Comments
 (0)