Skip to content

Commit

Permalink
Refactor common parts from the Rounding class into a separate 'round'…
Browse files Browse the repository at this point in the history
… package (#11023)

* Refactor common parts from the Rounding class into a separate 'round' package

Signed-off-by: Ketan Verma <[email protected]>

* Move RoundableTests from :server to :libs:opensearch-common module

Signed-off-by: Ketan Verma <[email protected]>

* Address PR comments

Signed-off-by: Ketan Verma <[email protected]>

* Replace assert with IllegalArgumentException for size checks

Signed-off-by: Ketan Verma <[email protected]>

---------

Signed-off-by: Ketan Verma <[email protected]>
  • Loading branch information
ketanv3 authored Nov 15, 2023
1 parent c0c76e6 commit 08db961
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 146 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Add telemetry tracer/metric enable flag and integ test. ([#10395](https://github.com/opensearch-project/OpenSearch/pull/10395))
- Add instrumentation for indexing in transport bulk action and transport shard bulk action. ([#10273](https://github.com/opensearch-project/OpenSearch/pull/10273))
- [BUG] Disable sort optimization for HALF_FLOAT ([#10999](https://github.com/opensearch-project/OpenSearch/pull/10999))
- Refactor common parts from the Rounding class into a separate 'round' package ([#11023](https://github.com/opensearch-project/OpenSearch/issues/11023))
- Performance improvement for MultiTerm Queries on Keyword fields ([#7057](https://github.com/opensearch-project/OpenSearch/issues/7057))
- Disable concurrent aggs for Diversified Sampler and Sampler aggs ([#11087](https://github.com/opensearch-project/OpenSearch/issues/11087))
- Made leader/follower check timeout setting dynamic ([#10528](https://github.com/opensearch-project/OpenSearch/pull/10528))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* compatible open source license.
*/

package org.opensearch.common;
package org.opensearch.common.round;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
Expand All @@ -27,13 +27,13 @@
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 1, time = 1)
@BenchmarkMode(Mode.Throughput)
public class ArrayRoundingBenchmark {
public class RoundableBenchmark {

@Benchmark
public void round(Blackhole bh, Options opts) {
Rounding.Prepared rounding = opts.supplier.get();
public void floor(Blackhole bh, Options opts) {
Roundable roundable = opts.supplier.get();
for (long key : opts.queries) {
bh.consume(rounding.round(key));
bh.consume(roundable.floor(key));
}
}

Expand Down Expand Up @@ -90,7 +90,7 @@ public static class Options {
public String distribution;

public long[] queries;
public Supplier<Rounding.Prepared> supplier;
public Supplier<Roundable> supplier;

@Setup
public void setup() {
Expand Down Expand Up @@ -130,10 +130,10 @@ public void setup() {

switch (type) {
case "binary":
supplier = () -> new Rounding.BinarySearchArrayRounding(values, size, null);
supplier = () -> new BinarySearcher(values, size);
break;
case "linear":
supplier = () -> new Rounding.BidirectionalLinearSearchArrayRounding(values, size, null);
supplier = () -> new BidirectionalLinearSearcher(values, size);
break;
default:
throw new IllegalArgumentException("invalid type: " + type);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.common.round;

import org.opensearch.common.annotation.InternalApi;

/**
* It uses linear search on a sorted array of pre-computed round-down points.
* For small inputs (&le; 64 elements), this can be much faster than binary search as it avoids the penalty of
* branch mispredictions and pipeline stalls, and accesses memory sequentially.
*
* <p>
* It uses "meet in the middle" linear search to avoid the worst case scenario when the desired element is present
* at either side of the array. This is helpful for time-series data where velocity increases over time, so more
* documents are likely to find a greater timestamp which is likely to be present on the right end of the array.
*
* @opensearch.internal
*/
@InternalApi
class BidirectionalLinearSearcher implements Roundable {
private final long[] ascending;
private final long[] descending;

BidirectionalLinearSearcher(long[] values, int size) {
if (size <= 0) {
throw new IllegalArgumentException("at least one value must be present");
}

int len = (size + 1) >>> 1; // rounded-up to handle odd number of values
ascending = new long[len];
descending = new long[len];

for (int i = 0; i < len; i++) {
ascending[i] = values[i];
descending[i] = values[size - i - 1];
}
}

@Override
public long floor(long key) {
int i = 0;
for (; i < ascending.length; i++) {
if (descending[i] <= key) {
return descending[i];
}
if (ascending[i] > key) {
assert i > 0 : "key must be greater than or equal to " + ascending[0];
return ascending[i - 1];
}
}
return ascending[i - 1];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.common.round;

import org.opensearch.common.annotation.InternalApi;

import java.util.Arrays;

/**
* It uses binary search on a sorted array of pre-computed round-down points.
*
* @opensearch.internal
*/
@InternalApi
class BinarySearcher implements Roundable {
private final long[] values;
private final int size;

BinarySearcher(long[] values, int size) {
if (size <= 0) {
throw new IllegalArgumentException("at least one value must be present");
}

this.values = values;
this.size = size;
}

@Override
public long floor(long key) {
int idx = Arrays.binarySearch(values, 0, size, key);
assert idx != -1 : "key must be greater than or equal to " + values[0];
if (idx < 0) {
idx = -2 - idx;
}
return values[idx];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.common.round;

import org.opensearch.common.annotation.InternalApi;

/**
* Interface to round-off values.
*
* @opensearch.internal
*/
@InternalApi
@FunctionalInterface
public interface Roundable {
/**
* Returns the greatest lower bound of the given key.
* In other words, it returns the largest value such that {@code value <= key}.
* @param key to floor
* @return the floored value
*/
long floor(long key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.common.round;

import org.opensearch.common.annotation.InternalApi;

/**
* Factory class to create and return the fastest implementation of {@link Roundable}.
*
* @opensearch.internal
*/
@InternalApi
public final class RoundableFactory {
/**
* The maximum limit up to which linear search is used, otherwise binary search is used.
* This is because linear search is much faster on small arrays.
* Benchmark results: <a href="https://github.com/opensearch-project/OpenSearch/pull/9727">PR #9727</a>
*/
private static final int LINEAR_SEARCH_MAX_SIZE = 64;

private RoundableFactory() {}

/**
* Creates and returns the fastest implementation of {@link Roundable}.
*/
public static Roundable create(long[] values, int size) {
if (size <= LINEAR_SEARCH_MAX_SIZE) {
return new BidirectionalLinearSearcher(values, size);
} else {
return new BinarySearcher(values, size);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

/**
* Contains classes to round-off values.
*/
package org.opensearch.common.round;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.common.round;

import org.opensearch.test.OpenSearchTestCase;

public class RoundableTests extends OpenSearchTestCase {

public void testFloor() {
int size = randomIntBetween(1, 256);
long[] values = new long[size];
for (int i = 1; i < values.length; i++) {
values[i] = values[i - 1] + (randomNonNegativeLong() % 200) + 1;
}

Roundable[] impls = { new BinarySearcher(values, size), new BidirectionalLinearSearcher(values, size) };

for (int i = 0; i < 100000; i++) {
// Index of the expected round-down point.
int idx = randomIntBetween(0, size - 1);

// Value of the expected round-down point.
long expected = values[idx];

// Delta between the expected and the next round-down point.
long delta = (idx < size - 1) ? (values[idx + 1] - values[idx]) : 200;

// Adding a random delta between 0 (inclusive) and delta (exclusive) to the expected
// round-down point, which will still floor to the same value.
long key = expected + (randomNonNegativeLong() % delta);

for (Roundable roundable : impls) {
assertEquals(expected, roundable.floor(key));
}
}
}

public void testFailureCases() {
Throwable throwable;

throwable = assertThrows(IllegalArgumentException.class, () -> new BinarySearcher(new long[0], 0));
assertEquals("at least one value must be present", throwable.getMessage());
throwable = assertThrows(IllegalArgumentException.class, () -> new BidirectionalLinearSearcher(new long[0], 0));
assertEquals("at least one value must be present", throwable.getMessage());

throwable = assertThrows(AssertionError.class, () -> new BinarySearcher(new long[] { 100 }, 1).floor(50));
assertEquals("key must be greater than or equal to 100", throwable.getMessage());
throwable = assertThrows(AssertionError.class, () -> new BidirectionalLinearSearcher(new long[] { 100 }, 1).floor(50));
assertEquals("key must be greater than or equal to 100", throwable.getMessage());
}
}
Loading

0 comments on commit 08db961

Please sign in to comment.