RFR: 7950: better DisjointBuilder performance for overlapping or unordered events
Richard Startin
duke at openjdk.org
Mon Oct 31 19:59:08 UTC 2022
When events are not disjoint and sorted by the end quantity, a lot of time can be spent in `DisjointBuilder.add`. Profiling shows that this is because of the linear search for the first lane ending before the added event starts, and then in the sort afterwards.
This patch uses binary search to improve the handling of non-disjoint or non-sorted cases. It also adds checks whether the modification will disrupt descending order to avoid doing any work reordering the array.
With a parameterised benchmark exercising some pathological cases:
import org.openjdk.jmc.common.item.IMemberAccessor;
import org.openjdk.jmc.common.unit.IQuantity;
import org.openjdk.jmc.common.unit.UnitLookup;
import org.openjdk.jmc.flightrecorder.internal.util.DisjointBuilder;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import java.util.concurrent.ThreadLocalRandom;
@State(Scope.Benchmark)
public class DisjointBuilderBenchmark {
private static class RangeObject {
private final IQuantity start;
private final IQuantity end;
RangeObject(IQuantity start, IQuantity end) {
this.start = start;
this.end = end;
}
}
private final static IMemberAccessor<IQuantity, RangeObject> START = inObject -> inObject.start;
private final static IMemberAccessor<IQuantity, RangeObject> END = inObject -> inObject.end;
public enum Scenario {
ASCENDING {
@Override
void fill(RangeObject[] rangeObjects) {
long min = 0;
long max = 1;
for (int i = 0; i < rangeObjects.length; i++) {
rangeObjects[i] = new RangeObject(UnitLookup.NUMBER_UNITY.quantity(min),
UnitLookup.NUMBER_UNITY.quantity(max));
min++;
max++;
}
}
},
DESCENDING {
@Override
void fill(RangeObject[] rangeObjects) {
long min = 2L * (rangeObjects.length - 1);
long max = 2L * rangeObjects.length - 1;
for (int i = 0; i < rangeObjects.length; i++) {
rangeObjects[i] = new RangeObject(UnitLookup.NUMBER_UNITY.quantity(min),
UnitLookup.NUMBER_UNITY.quantity(max));
min--;
max--;
}
}
},
SCRAMBLED {
@Override
void fill(RangeObject[] rangeObjects) {
ASCENDING.fill(rangeObjects);
shuffle(rangeObjects);
}
private void shuffle(RangeObject[] data) {
for (int i = data.length; i > 1; i--) {
swap(data, i - 1, ThreadLocalRandom.current().nextInt(i));
}
}
private void swap(RangeObject[] arr, int i, int j) {
RangeObject tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
},
OVERLAPS {
@Override
void fill(RangeObject[] rangeObjects) {
long min = 0;
long max = 2;
for (int i = 0; i < rangeObjects.length; i++) {
rangeObjects[i] = new RangeObject(UnitLookup.NUMBER_UNITY.quantity(min),
UnitLookup.NUMBER_UNITY.quantity(max));
min++;
max++;
}
}
},
STACKED {
@Override
void fill(RangeObject[] rangeObjects) {
long min = 0;
long max = 2L * rangeObjects.length;
for (int i = 0; i < rangeObjects.length; i++) {
rangeObjects[i] = new RangeObject(UnitLookup.NUMBER_UNITY.quantity(min),
UnitLookup.NUMBER_UNITY.quantity(max));
min++;
max--;
}
}
},
SHALLOW_STACKS {
@Override
void fill(RangeObject[] rangeObjects) {
int stackDepth = 5;
long min = 0;
long max = 2 * stackDepth;
for (int i = 0; i < rangeObjects.length; i++) {
rangeObjects[i] = new RangeObject(UnitLookup.NUMBER_UNITY.quantity(min),
UnitLookup.NUMBER_UNITY.quantity(max));
min++;
max--;
if (min == max) {
max += 2 * stackDepth;
}
}
}
};
abstract void fill(RangeObject[] rangeObjects);
}
@Param({"ASCENDING", "DESCENDING", "SCRAMBLED", "OVERLAPS", "STACKED", "SHALLOW_STACKS"})
Scenario scenario;
@Param({"10000"})
int numEvents;
private RangeObject[] input;
@Setup(Level.Trial)
public void setup() {
input = new RangeObject[numEvents];
scenario.fill(input);
}
@Benchmark
public DisjointBuilder<RangeObject> build() {
DisjointBuilder<RangeObject> builder = new DisjointBuilder<>(START, END);
for (RangeObject rangeObject : input) {
builder.add(rangeObject);
}
return builder;
}
}
Good performance is maintained in the common ascending/disjoint case, but the degenerate cases perform much better:
Before:
Benchmark (numEvents) (scenario) Mode Cnt Score Error Units
DisjointBuilderBenchmark.build 10000 ASCENDING avgt 5 96.050 ± 1.240 us/op
DisjointBuilderBenchmark.build 10000 DESCENDING avgt 5 300246.685 ± 2101.505 us/op
DisjointBuilderBenchmark.build 10000 SCRAMBLED avgt 5 3977.733 ± 29.612 us/op
DisjointBuilderBenchmark.build 10000 OVERLAPS avgt 5 187.701 ± 6.161 us/op
DisjointBuilderBenchmark.build 10000 STACKED avgt 5 268306.481 ± 980.223 us/op
DisjointBuilderBenchmark.build 10000 SHALLOW_STACKS avgt 5 414.298 ± 5.719 us/op
After:
Benchmark (numEvents) (scenario) Mode Cnt Score Error Units
DisjointBuilderBenchmark.build 10000 ASCENDING avgt 5 91.583 ± 1.070 us/op
DisjointBuilderBenchmark.build 10000 DESCENDING avgt 5 608.281 ± 9.338 us/op
DisjointBuilderBenchmark.build 10000 SCRAMBLED avgt 5 596.601 ± 18.243 us/op
DisjointBuilderBenchmark.build 10000 OVERLAPS avgt 5 181.349 ± 1.012 us/op
DisjointBuilderBenchmark.build 10000 STACKED avgt 5 555.167 ± 13.316 us/op
DisjointBuilderBenchmark.build 10000 SHALLOW_STACKS avgt 5 353.700 ± 9.387 us/op
-------------
Commit messages:
- 7950: better DisjointBuilder performance for overlapping or unordered events
Changes: https://git.openjdk.org/jmc/pull/449/files
Webrev: https://webrevs.openjdk.org/?repo=jmc&pr=449&range=00
Issue: https://bugs.openjdk.org/browse/JMC-7950
Stats: 64 lines in 1 file changed: 40 ins; 8 del; 16 mod
Patch: https://git.openjdk.org/jmc/pull/449.diff
Fetch: git fetch https://git.openjdk.org/jmc pull/449/head:pull/449
PR: https://git.openjdk.org/jmc/pull/449
More information about the jmc-dev
mailing list