Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,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))

### Deprecated

Expand All @@ -141,4 +142,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Security

[Unreleased 3.0]: https://github.com/opensearch-project/OpenSearch/compare/2.x...HEAD
[Unreleased 2.x]: https://github.com/opensearch-project/OpenSearch/compare/2.12...2.x
[Unreleased 2.x]: https://github.com/opensearch-project/OpenSearch/compare/2.12...2.x
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,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.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) {
assert size > 0 : "at least one value must be present";
ketanv3 marked this conversation as resolved.
Show resolved Hide resolved

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];
andrross marked this conversation as resolved.
Show resolved Hide resolved
return ascending[i - 1];
}
}
return ascending[i - 1];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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) {
assert size > 0 : "at least one value must be present";
ketanv3 marked this conversation as resolved.
Show resolved Hide resolved

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 testAssertions() {
AssertionError exception;

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

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