Skip to content

Commit 9efe184

Browse files
ketanv3shiv0408
authored andcommitted
Refactor common parts from the Rounding class into a separate 'round' package (opensearch-project#11023)
* Refactor common parts from the Rounding class into a separate 'round' package Signed-off-by: Ketan Verma <ketan9495@gmail.com> * Move RoundableTests from :server to :libs:opensearch-common module Signed-off-by: Ketan Verma <ketan9495@gmail.com> * Address PR comments Signed-off-by: Ketan Verma <ketan9495@gmail.com> * Replace assert with IllegalArgumentException for size checks Signed-off-by: Ketan Verma <ketan9495@gmail.com> --------- Signed-off-by: Ketan Verma <ketan9495@gmail.com> Signed-off-by: Shivansh Arora <hishiv@amazon.com>
1 parent c76dbdc commit 9efe184

File tree

10 files changed

+279
-146
lines changed

10 files changed

+279
-146
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
130130
- Add telemetry tracer/metric enable flag and integ test. ([#10395](https://github.com/opensearch-project/OpenSearch/pull/10395))
131131
- Add instrumentation for indexing in transport bulk action and transport shard bulk action. ([#10273](https://github.com/opensearch-project/OpenSearch/pull/10273))
132132
- [BUG] Disable sort optimization for HALF_FLOAT ([#10999](https://github.com/opensearch-project/OpenSearch/pull/10999))
133+
- Refactor common parts from the Rounding class into a separate 'round' package ([#11023](https://github.com/opensearch-project/OpenSearch/issues/11023))
133134
- Performance improvement for MultiTerm Queries on Keyword fields ([#7057](https://github.com/opensearch-project/OpenSearch/issues/7057))
134135
- Disable concurrent aggs for Diversified Sampler and Sampler aggs ([#11087](https://github.com/opensearch-project/OpenSearch/issues/11087))
135136
- Made leader/follower check timeout setting dynamic ([#10528](https://github.com/opensearch-project/OpenSearch/pull/10528))

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* compatible open source license.
77
*/
88

9-
package org.opensearch.common;
9+
package org.opensearch.common.round;
1010

1111
import org.openjdk.jmh.annotations.Benchmark;
1212
import org.openjdk.jmh.annotations.BenchmarkMode;
@@ -27,13 +27,13 @@
2727
@Warmup(iterations = 3, time = 1)
2828
@Measurement(iterations = 1, time = 1)
2929
@BenchmarkMode(Mode.Throughput)
30-
public class ArrayRoundingBenchmark {
30+
public class RoundableBenchmark {
3131

3232
@Benchmark
33-
public void round(Blackhole bh, Options opts) {
34-
Rounding.Prepared rounding = opts.supplier.get();
33+
public void floor(Blackhole bh, Options opts) {
34+
Roundable roundable = opts.supplier.get();
3535
for (long key : opts.queries) {
36-
bh.consume(rounding.round(key));
36+
bh.consume(roundable.floor(key));
3737
}
3838
}
3939

@@ -90,7 +90,7 @@ public static class Options {
9090
public String distribution;
9191

9292
public long[] queries;
93-
public Supplier<Rounding.Prepared> supplier;
93+
public Supplier<Roundable> supplier;
9494

9595
@Setup
9696
public void setup() {
@@ -130,10 +130,10 @@ public void setup() {
130130

131131
switch (type) {
132132
case "binary":
133-
supplier = () -> new Rounding.BinarySearchArrayRounding(values, size, null);
133+
supplier = () -> new BinarySearcher(values, size);
134134
break;
135135
case "linear":
136-
supplier = () -> new Rounding.BidirectionalLinearSearchArrayRounding(values, size, null);
136+
supplier = () -> new BidirectionalLinearSearcher(values, size);
137137
break;
138138
default:
139139
throw new IllegalArgumentException("invalid type: " + type);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
/**
14+
* It uses linear search on a sorted array of pre-computed round-down points.
15+
* For small inputs (&le; 64 elements), this can be much faster than binary search as it avoids the penalty of
16+
* branch mispredictions and pipeline stalls, and accesses memory sequentially.
17+
*
18+
* <p>
19+
* It uses "meet in the middle" linear search to avoid the worst case scenario when the desired element is present
20+
* at either side of the array. This is helpful for time-series data where velocity increases over time, so more
21+
* documents are likely to find a greater timestamp which is likely to be present on the right end of the array.
22+
*
23+
* @opensearch.internal
24+
*/
25+
@InternalApi
26+
class BidirectionalLinearSearcher implements Roundable {
27+
private final long[] ascending;
28+
private final long[] descending;
29+
30+
BidirectionalLinearSearcher(long[] values, int size) {
31+
if (size <= 0) {
32+
throw new IllegalArgumentException("at least one value must be present");
33+
}
34+
35+
int len = (size + 1) >>> 1; // rounded-up to handle odd number of values
36+
ascending = new long[len];
37+
descending = new long[len];
38+
39+
for (int i = 0; i < len; i++) {
40+
ascending[i] = values[i];
41+
descending[i] = values[size - i - 1];
42+
}
43+
}
44+
45+
@Override
46+
public long floor(long key) {
47+
int i = 0;
48+
for (; i < ascending.length; i++) {
49+
if (descending[i] <= key) {
50+
return descending[i];
51+
}
52+
if (ascending[i] > key) {
53+
assert i > 0 : "key must be greater than or equal to " + ascending[0];
54+
return ascending[i - 1];
55+
}
56+
}
57+
return ascending[i - 1];
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 java.util.Arrays;
14+
15+
/**
16+
* It uses binary search on a sorted array of pre-computed round-down points.
17+
*
18+
* @opensearch.internal
19+
*/
20+
@InternalApi
21+
class BinarySearcher implements Roundable {
22+
private final long[] values;
23+
private final int size;
24+
25+
BinarySearcher(long[] values, int size) {
26+
if (size <= 0) {
27+
throw new IllegalArgumentException("at least one value must be present");
28+
}
29+
30+
this.values = values;
31+
this.size = size;
32+
}
33+
34+
@Override
35+
public long floor(long key) {
36+
int idx = Arrays.binarySearch(values, 0, size, key);
37+
assert idx != -1 : "key must be greater than or equal to " + values[0];
38+
if (idx < 0) {
39+
idx = -2 - idx;
40+
}
41+
return values[idx];
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
/**
14+
* Interface to round-off values.
15+
*
16+
* @opensearch.internal
17+
*/
18+
@InternalApi
19+
@FunctionalInterface
20+
public interface Roundable {
21+
/**
22+
* Returns the greatest lower bound of the given key.
23+
* In other words, it returns the largest value such that {@code value <= key}.
24+
* @param key to floor
25+
* @return the floored value
26+
*/
27+
long floor(long key);
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
/**
14+
* Factory class to create and return the fastest implementation of {@link Roundable}.
15+
*
16+
* @opensearch.internal
17+
*/
18+
@InternalApi
19+
public final class RoundableFactory {
20+
/**
21+
* The maximum limit up to which linear search is used, otherwise binary search is used.
22+
* This is because linear search is much faster on small arrays.
23+
* Benchmark results: <a href="https://github.com/opensearch-project/OpenSearch/pull/9727">PR #9727</a>
24+
*/
25+
private static final int LINEAR_SEARCH_MAX_SIZE = 64;
26+
27+
private RoundableFactory() {}
28+
29+
/**
30+
* Creates and returns the fastest implementation of {@link Roundable}.
31+
*/
32+
public static Roundable create(long[] values, int size) {
33+
if (size <= LINEAR_SEARCH_MAX_SIZE) {
34+
return new BidirectionalLinearSearcher(values, size);
35+
} else {
36+
return new BinarySearcher(values, size);
37+
}
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
/**
10+
* Contains classes to round-off values.
11+
*/
12+
package org.opensearch.common.round;
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.test.OpenSearchTestCase;
12+
13+
public class RoundableTests extends OpenSearchTestCase {
14+
15+
public void testFloor() {
16+
int size = randomIntBetween(1, 256);
17+
long[] values = new long[size];
18+
for (int i = 1; i < values.length; i++) {
19+
values[i] = values[i - 1] + (randomNonNegativeLong() % 200) + 1;
20+
}
21+
22+
Roundable[] impls = { new BinarySearcher(values, size), new BidirectionalLinearSearcher(values, size) };
23+
24+
for (int i = 0; i < 100000; i++) {
25+
// Index of the expected round-down point.
26+
int idx = randomIntBetween(0, size - 1);
27+
28+
// Value of the expected round-down point.
29+
long expected = values[idx];
30+
31+
// Delta between the expected and the next round-down point.
32+
long delta = (idx < size - 1) ? (values[idx + 1] - values[idx]) : 200;
33+
34+
// Adding a random delta between 0 (inclusive) and delta (exclusive) to the expected
35+
// round-down point, which will still floor to the same value.
36+
long key = expected + (randomNonNegativeLong() % delta);
37+
38+
for (Roundable roundable : impls) {
39+
assertEquals(expected, roundable.floor(key));
40+
}
41+
}
42+
}
43+
44+
public void testFailureCases() {
45+
Throwable throwable;
46+
47+
throwable = assertThrows(IllegalArgumentException.class, () -> new BinarySearcher(new long[0], 0));
48+
assertEquals("at least one value must be present", throwable.getMessage());
49+
throwable = assertThrows(IllegalArgumentException.class, () -> new BidirectionalLinearSearcher(new long[0], 0));
50+
assertEquals("at least one value must be present", throwable.getMessage());
51+
52+
throwable = assertThrows(AssertionError.class, () -> new BinarySearcher(new long[] { 100 }, 1).floor(50));
53+
assertEquals("key must be greater than or equal to 100", throwable.getMessage());
54+
throwable = assertThrows(AssertionError.class, () -> new BidirectionalLinearSearcher(new long[] { 100 }, 1).floor(50));
55+
assertEquals("key must be greater than or equal to 100", throwable.getMessage());
56+
}
57+
}

0 commit comments

Comments
 (0)