Skip to content

Commit 9b7dab1

Browse files
ketanv3retaandrross
authored andcommitted
Add experimental SIMD implementation of B-tree to round down dates (opensearch-project#11194)
* Add experimental SIMD implementation of B-tree to round down dates Signed-off-by: Ketan Verma <[email protected]> * Use system properties in favor of feature flags to remove dependency on the server module Signed-off-by: Ketan Verma <[email protected]> * Removed Java 20 test sources to simplify builds Signed-off-by: Ketan Verma <[email protected]> * Remove the use of forbidden APIs in unit-tests Signed-off-by: Ketan Verma <[email protected]> * Migrate to the recommended usage for custom test task classpath Signed-off-by: Ketan Verma <[email protected]> * Switch benchmarks module to multi-release one Signed-off-by: Andriy Redko <[email protected]> * Add JMH annotation processing for JDK-20+ sources Signed-off-by: Andriy Redko <[email protected]> * Make JMH annotations consistent across sources Signed-off-by: Ketan Verma <[email protected]> * Improve execution of Roundable unit-tests Signed-off-by: Ketan Verma <[email protected]> * Revert "Improve execution of Roundable unit-tests" This reverts commit 2e82d0a. Signed-off-by: Ketan Verma <[email protected]> * Add 'forced' as a possible feature flag value to simplify the execution of unit-tests Signed-off-by: Ketan Verma <[email protected]> --------- Signed-off-by: Ketan Verma <[email protected]> Signed-off-by: Andriy Redko <[email protected]> Co-authored-by: Andriy Redko <[email protected]> Co-authored-by: Andrew Ross <[email protected]>
1 parent a81a5de commit 9b7dab1

File tree

9 files changed

+370
-34
lines changed

9 files changed

+370
-34
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
118118
- Request level coordinator slow logs ([#10650](https://github.com/opensearch-project/OpenSearch/pull/10650))
119119
- Add template snippets support for field and target_field in KV ingest processor ([#10040](https://github.com/opensearch-project/OpenSearch/pull/10040))
120120
- Allowing pipeline processors to access index mapping info by passing ingest service ref as part of the processor factory parameters ([#10307](https://github.com/opensearch-project/OpenSearch/pull/10307))
121+
- Add experimental SIMD implementation of B-tree to round down dates ([#11194](https://github.com/opensearch-project/OpenSearch/issues/11194))
121122
- Make number of segment metadata files in remote segment store configurable ([#11329](https://github.com/opensearch-project/OpenSearch/pull/11329))
122123
- Allow changing number of replicas of searchable snapshot index ([#11317](https://github.com/opensearch-project/OpenSearch/pull/11317))
123124
- Adding slf4j license header to LoggerMessageFormat.java ([#11069](https://github.com/opensearch-project/OpenSearch/pull/11069))

benchmarks/build.gradle

+42
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,45 @@ spotless {
8484
targetExclude 'src/main/generated/**/*.java'
8585
}
8686
}
87+
88+
if (BuildParams.runtimeJavaVersion >= JavaVersion.VERSION_20) {
89+
// Add support for incubator modules on supported Java versions.
90+
run.jvmArgs += ['--add-modules=jdk.incubator.vector']
91+
run.classpath += files(jar.archiveFile)
92+
run.classpath -= sourceSets.main.output
93+
evaluationDependsOn(':libs:opensearch-common')
94+
95+
sourceSets {
96+
java20 {
97+
java {
98+
srcDirs = ['src/main/java20']
99+
}
100+
}
101+
}
102+
103+
configurations {
104+
java20Implementation.extendsFrom(implementation)
105+
}
106+
107+
dependencies {
108+
java20Implementation sourceSets.main.output
109+
java20Implementation project(':libs:opensearch-common').sourceSets.java20.output
110+
java20AnnotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:$versions.jmh"
111+
}
112+
113+
compileJava20Java {
114+
targetCompatibility = JavaVersion.VERSION_20
115+
options.compilerArgs.addAll(["-processor", "org.openjdk.jmh.generators.BenchmarkProcessor"])
116+
}
117+
118+
jar {
119+
metaInf {
120+
into 'versions/20'
121+
from sourceSets.java20.output
122+
}
123+
manifest.attributes('Multi-Release': 'true')
124+
}
125+
126+
// classes generated by JMH can use all sorts of forbidden APIs but we have no influence at all and cannot exclude these classes
127+
disableTasks('forbiddenApisJava20')
128+
}

benchmarks/src/main/java/org/opensearch/common/round/RoundableBenchmark.java

+4-14
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import org.openjdk.jmh.infra.Blackhole;
2222

2323
import java.util.Random;
24-
import java.util.function.Supplier;
2524

2625
@Fork(value = 3)
2726
@Warmup(iterations = 3, time = 1)
@@ -83,17 +82,17 @@ public static class Options {
8382
"256" })
8483
public Integer size;
8584

86-
@Param({ "binary", "linear" })
85+
@Param({ "binary", "linear", "btree" })
8786
public String type;
8887

8988
@Param({ "uniform", "skewed_edge", "skewed_center" })
9089
public String distribution;
9190

9291
public long[] queries;
93-
public Supplier<Roundable> supplier;
92+
public RoundableSupplier supplier;
9493

9594
@Setup
96-
public void setup() {
95+
public void setup() throws ClassNotFoundException {
9796
Random random = new Random(size);
9897
long[] values = new long[size];
9998
for (int i = 1; i < values.length; i++) {
@@ -128,16 +127,7 @@ public void setup() {
128127
throw new IllegalArgumentException("invalid distribution: " + distribution);
129128
}
130129

131-
switch (type) {
132-
case "binary":
133-
supplier = () -> new BinarySearcher(values, size);
134-
break;
135-
case "linear":
136-
supplier = () -> new BidirectionalLinearSearcher(values, size);
137-
break;
138-
default:
139-
throw new IllegalArgumentException("invalid type: " + type);
140-
}
130+
supplier = new RoundableSupplier(type, values, size);
141131
}
142132

143133
private static long nextPositiveLong(Random random) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.common.round;
10+
11+
import java.util.function.Supplier;
12+
13+
public class RoundableSupplier implements Supplier<Roundable> {
14+
private final Supplier<Roundable> delegate;
15+
16+
RoundableSupplier(String type, long[] values, int size) throws ClassNotFoundException {
17+
switch (type) {
18+
case "binary":
19+
delegate = () -> new BinarySearcher(values, size);
20+
break;
21+
case "linear":
22+
delegate = () -> new BidirectionalLinearSearcher(values, size);
23+
break;
24+
case "btree":
25+
throw new ClassNotFoundException("BtreeSearcher is not supported below JDK 20");
26+
default:
27+
throw new IllegalArgumentException("invalid type: " + type);
28+
}
29+
}
30+
31+
@Override
32+
public Roundable get() {
33+
return delegate.get();
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.common.round;
10+
11+
import java.util.function.Supplier;
12+
13+
public class RoundableSupplier implements Supplier<Roundable> {
14+
private final Supplier<Roundable> delegate;
15+
16+
RoundableSupplier(String type, long[] values, int size) {
17+
switch (type) {
18+
case "binary":
19+
delegate = () -> new BinarySearcher(values, size);
20+
break;
21+
case "linear":
22+
delegate = () -> new BidirectionalLinearSearcher(values, size);
23+
break;
24+
case "btree":
25+
delegate = () -> new BtreeSearcher(values, size);
26+
break;
27+
default:
28+
throw new IllegalArgumentException("invalid type: " + type);
29+
}
30+
}
31+
32+
@Override
33+
public Roundable get() {
34+
return delegate.get();
35+
}
36+
}

libs/common/build.gradle

+61
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,64 @@ tasks.named('forbiddenApisMain').configure {
4343
// TODO: Need to decide how we want to handle for forbidden signatures with the changes to server
4444
replaceSignatureFiles 'jdk-signatures'
4545
}
46+
47+
// Add support for incubator modules on supported Java versions.
48+
if (BuildParams.runtimeJavaVersion >= JavaVersion.VERSION_20) {
49+
sourceSets {
50+
java20 {
51+
java {
52+
srcDirs = ['src/main/java20']
53+
}
54+
}
55+
}
56+
57+
configurations {
58+
java20Implementation.extendsFrom(implementation)
59+
}
60+
61+
dependencies {
62+
java20Implementation sourceSets.main.output
63+
}
64+
65+
compileJava20Java {
66+
targetCompatibility = JavaVersion.VERSION_20
67+
options.compilerArgs += ['--add-modules', 'jdk.incubator.vector']
68+
options.compilerArgs -= '-Werror' // use of incubator modules is reported as a warning
69+
}
70+
71+
jar {
72+
metaInf {
73+
into 'versions/20'
74+
from sourceSets.java20.output
75+
}
76+
manifest.attributes('Multi-Release': 'true')
77+
}
78+
79+
tasks.withType(Test).configureEach {
80+
// Relying on the convention for Test.classpath in custom Test tasks has been deprecated
81+
// and scheduled to be removed in Gradle 9.0. Below lines are added from the migration guide:
82+
// https://docs.gradle.org/8.5/userguide/upgrading_version_8.html#test_task_default_classpath
83+
testClassesDirs = testing.suites.test.sources.output.classesDirs
84+
classpath = testing.suites.test.sources.runtimeClasspath
85+
86+
// Adds the multi-release JAR to the classpath when executing tests.
87+
// This allows newer sources to be picked up at test runtime (if supported).
88+
classpath += files(jar.archiveFile)
89+
// Removes the "main" sources from the classpath to avoid JarHell problems as
90+
// the multi-release JAR already contains those classes.
91+
classpath -= sourceSets.main.output
92+
}
93+
94+
tasks.register('roundableSimdTest', Test) {
95+
group 'verification'
96+
include '**/RoundableTests.class'
97+
systemProperty 'opensearch.experimental.feature.simd.rounding.enabled', 'forced'
98+
}
99+
100+
check.dependsOn(roundableSimdTest)
101+
102+
forbiddenApisJava20 {
103+
failOnMissingClasses = false
104+
ignoreSignaturesOfMissingClasses = true
105+
}
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.common.round;
10+
11+
import org.opensearch.common.annotation.InternalApi;
12+
13+
import jdk.incubator.vector.LongVector;
14+
import jdk.incubator.vector.Vector;
15+
import jdk.incubator.vector.VectorOperators;
16+
import jdk.incubator.vector.VectorSpecies;
17+
18+
/**
19+
* It uses vectorized B-tree search to find the round-down point.
20+
*
21+
* @opensearch.internal
22+
*/
23+
@InternalApi
24+
class BtreeSearcher implements Roundable {
25+
private static final VectorSpecies<Long> LONG_VECTOR_SPECIES = LongVector.SPECIES_PREFERRED;
26+
private static final int LANES = LONG_VECTOR_SPECIES.length();
27+
private static final int SHIFT = log2(LANES);
28+
29+
private final long[] values;
30+
private final long minValue;
31+
32+
BtreeSearcher(long[] values, int size) {
33+
if (size <= 0) {
34+
throw new IllegalArgumentException("at least one value must be present");
35+
}
36+
37+
int blocks = (size + LANES - 1) / LANES; // number of blocks
38+
int length = 1 + blocks * LANES; // size of the backing array (1-indexed)
39+
40+
this.minValue = values[0];
41+
this.values = new long[length];
42+
build(values, 0, size, this.values, 1);
43+
}
44+
45+
/**
46+
* Builds the B-tree memory layout.
47+
* It builds the tree recursively, following an in-order traversal.
48+
*
49+
* <p>
50+
* Each block stores 'lanes' values at indices {@code i, i + 1, ..., i + lanes - 1} where {@code i} is the
51+
* starting offset. The starting offset of the root block is 1. The branching factor is (1 + lanes) so each
52+
* block can have these many children. Given the starting offset {@code i} of a block, the starting offset
53+
* of its k-th child (ranging from {@code 0, 1, ..., k}) can be computed as {@code i + ((i + k) << shift)}.
54+
*
55+
* @param src is the sorted input array
56+
* @param i is the index in the input array to read the value from
57+
* @param size the number of values in the input array
58+
* @param dst is the output array
59+
* @param j is the index in the output array to write the value to
60+
* @return the next index 'i'
61+
*/
62+
private static int build(long[] src, int i, int size, long[] dst, int j) {
63+
if (j < dst.length) {
64+
for (int k = 0; k < LANES; k++) {
65+
i = build(src, i, size, dst, j + ((j + k) << SHIFT));
66+
67+
// Fills the B-tree as a complete tree, i.e., all levels are completely filled,
68+
// except the last level which is filled from left to right.
69+
// The trick is to fill the destination array between indices 1...size (inclusive / 1-indexed)
70+
// and pad the remaining array with +infinity.
71+
dst[j + k] = (j + k <= size) ? src[i++] : Long.MAX_VALUE;
72+
}
73+
i = build(src, i, size, dst, j + ((j + LANES) << SHIFT));
74+
}
75+
return i;
76+
}
77+
78+
@Override
79+
public long floor(long key) {
80+
Vector<Long> keyVector = LongVector.broadcast(LONG_VECTOR_SPECIES, key);
81+
int i = 1, result = 1;
82+
83+
while (i < values.length) {
84+
Vector<Long> valuesVector = LongVector.fromArray(LONG_VECTOR_SPECIES, values, i);
85+
int j = i + valuesVector.compare(VectorOperators.GT, keyVector).firstTrue();
86+
result = (j > i) ? j : result;
87+
i += (j << SHIFT);
88+
}
89+
90+
assert result > 1 : "key must be greater than or equal to " + minValue;
91+
return values[result - 1];
92+
}
93+
94+
private static int log2(int num) {
95+
if ((num <= 0) || ((num & (num - 1)) != 0)) {
96+
throw new IllegalArgumentException(num + " is not a positive power of 2");
97+
}
98+
return 32 - Integer.numberOfLeadingZeros(num - 1);
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.common.round;
10+
11+
import org.opensearch.common.annotation.InternalApi;
12+
13+
import jdk.incubator.vector.LongVector;
14+
15+
/**
16+
* Factory class to create and return the fastest implementation of {@link Roundable}.
17+
*
18+
* @opensearch.internal
19+
*/
20+
@InternalApi
21+
public final class RoundableFactory {
22+
/**
23+
* The maximum limit up to which linear search is used, otherwise binary or B-tree search is used.
24+
* This is because linear search is much faster on small arrays.
25+
* Benchmark results: <a href="https://github.com/opensearch-project/OpenSearch/pull/9727">PR #9727</a>
26+
*/
27+
private static final int LINEAR_SEARCH_MAX_SIZE = 64;
28+
29+
/**
30+
* Indicates whether the vectorized (SIMD) B-tree search implementation is to be used.
31+
* It is true when either:
32+
* 1. The feature flag is set to "forced", or
33+
* 2. The platform has a minimum of 4 long vector lanes and the feature flag is set to "true".
34+
*/
35+
private static final boolean USE_BTREE_SEARCHER;
36+
37+
static {
38+
String simdRoundingFeatureFlag = System.getProperty("opensearch.experimental.feature.simd.rounding.enabled");
39+
USE_BTREE_SEARCHER = "forced".equalsIgnoreCase(simdRoundingFeatureFlag)
40+
|| (LongVector.SPECIES_PREFERRED.length() >= 4 && "true".equalsIgnoreCase(simdRoundingFeatureFlag));
41+
}
42+
43+
private RoundableFactory() {}
44+
45+
/**
46+
* Creates and returns the fastest implementation of {@link Roundable}.
47+
*/
48+
public static Roundable create(long[] values, int size) {
49+
if (size <= LINEAR_SEARCH_MAX_SIZE) {
50+
return new BidirectionalLinearSearcher(values, size);
51+
} else if (USE_BTREE_SEARCHER) {
52+
return new BtreeSearcher(values, size);
53+
} else {
54+
return new BinarySearcher(values, size);
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)