Skip to content

Commit 1212dee

Browse files
authored
ESQL: Speed up grouping by bytes (#114021) (#114652)
This speeds up grouping by bytes valued fields (keyword, text, ip, and wildcard) when the input is an ordinal block: ``` bytes_refs 22.213 ± 0.322 -> 19.848 ± 0.205 ns/op (*maybe* real, maybe noise. still good) ordinal didn't exist -> 2.988 ± 0.011 ns/op ``` I see this as 20ns -> 3ns, an 85% speed up. We never hard the ordinals branch before so I'm expecting the same performance there - about 20ns per op. This also speeds up grouping by a pair of byte valued fields: ``` two_bytes_refs 83.112 ± 42.348 -> 46.521 ± 0.386 ns/op two_ordinals 83.531 ± 23.473 -> 8.617 ± 0.105 ns/op ``` The speed up is much better when the fields are ordinals because hashing bytes is comparatively slow. I believe the ordinals case is quite common. I've run into it in quite a few profiles.
1 parent 0e2f832 commit 1212dee

File tree

13 files changed

+632
-66
lines changed

13 files changed

+632
-66
lines changed

benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/AggregatorBenchmark.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@
3030
import org.elasticsearch.compute.data.BooleanBlock;
3131
import org.elasticsearch.compute.data.BooleanVector;
3232
import org.elasticsearch.compute.data.BytesRefBlock;
33+
import org.elasticsearch.compute.data.BytesRefVector;
3334
import org.elasticsearch.compute.data.DoubleBlock;
3435
import org.elasticsearch.compute.data.ElementType;
3536
import org.elasticsearch.compute.data.IntBlock;
37+
import org.elasticsearch.compute.data.IntVector;
3638
import org.elasticsearch.compute.data.LongBlock;
39+
import org.elasticsearch.compute.data.OrdinalBytesRefVector;
3740
import org.elasticsearch.compute.data.Page;
3841
import org.elasticsearch.compute.operator.AggregationOperator;
3942
import org.elasticsearch.compute.operator.DriverContext;
@@ -78,7 +81,10 @@ public class AggregatorBenchmark {
7881
private static final String DOUBLES = "doubles";
7982
private static final String BOOLEANS = "booleans";
8083
private static final String BYTES_REFS = "bytes_refs";
84+
private static final String ORDINALS = "ordinals";
8185
private static final String TWO_LONGS = "two_" + LONGS;
86+
private static final String TWO_BYTES_REFS = "two_" + BYTES_REFS;
87+
private static final String TWO_ORDINALS = "two_" + ORDINALS;
8288
private static final String LONGS_AND_BYTES_REFS = LONGS + "_and_" + BYTES_REFS;
8389
private static final String TWO_LONGS_AND_BYTES_REFS = "two_" + LONGS + "_and_" + BYTES_REFS;
8490

@@ -119,7 +125,21 @@ public class AggregatorBenchmark {
119125
}
120126
}
121127

122-
@Param({ NONE, LONGS, INTS, DOUBLES, BOOLEANS, BYTES_REFS, TWO_LONGS, LONGS_AND_BYTES_REFS, TWO_LONGS_AND_BYTES_REFS })
128+
@Param(
129+
{
130+
NONE,
131+
LONGS,
132+
INTS,
133+
DOUBLES,
134+
BOOLEANS,
135+
BYTES_REFS,
136+
ORDINALS,
137+
TWO_LONGS,
138+
TWO_BYTES_REFS,
139+
TWO_ORDINALS,
140+
LONGS_AND_BYTES_REFS,
141+
TWO_LONGS_AND_BYTES_REFS }
142+
)
123143
public String grouping;
124144

125145
@Param({ COUNT, COUNT_DISTINCT, MIN, MAX, SUM })
@@ -144,8 +164,12 @@ private static Operator operator(DriverContext driverContext, String grouping, S
144164
case INTS -> List.of(new BlockHash.GroupSpec(0, ElementType.INT));
145165
case DOUBLES -> List.of(new BlockHash.GroupSpec(0, ElementType.DOUBLE));
146166
case BOOLEANS -> List.of(new BlockHash.GroupSpec(0, ElementType.BOOLEAN));
147-
case BYTES_REFS -> List.of(new BlockHash.GroupSpec(0, ElementType.BYTES_REF));
167+
case BYTES_REFS, ORDINALS -> List.of(new BlockHash.GroupSpec(0, ElementType.BYTES_REF));
148168
case TWO_LONGS -> List.of(new BlockHash.GroupSpec(0, ElementType.LONG), new BlockHash.GroupSpec(1, ElementType.LONG));
169+
case TWO_BYTES_REFS, TWO_ORDINALS -> List.of(
170+
new BlockHash.GroupSpec(0, ElementType.BYTES_REF),
171+
new BlockHash.GroupSpec(1, ElementType.BYTES_REF)
172+
);
149173
case LONGS_AND_BYTES_REFS -> List.of(
150174
new BlockHash.GroupSpec(0, ElementType.LONG),
151175
new BlockHash.GroupSpec(1, ElementType.BYTES_REF)
@@ -218,6 +242,10 @@ private static void checkGrouped(String prefix, String grouping, String op, Stri
218242
checkGroupingBlock(prefix, LONGS, page.getBlock(0));
219243
checkGroupingBlock(prefix, LONGS, page.getBlock(1));
220244
}
245+
case TWO_BYTES_REFS, TWO_ORDINALS -> {
246+
checkGroupingBlock(prefix, BYTES_REFS, page.getBlock(0));
247+
checkGroupingBlock(prefix, BYTES_REFS, page.getBlock(1));
248+
}
221249
case LONGS_AND_BYTES_REFS -> {
222250
checkGroupingBlock(prefix, LONGS, page.getBlock(0));
223251
checkGroupingBlock(prefix, BYTES_REFS, page.getBlock(1));
@@ -379,7 +407,7 @@ private static void checkGroupingBlock(String prefix, String grouping, Block blo
379407
throw new AssertionError(prefix + "bad group expected [true] but was [" + groups.getBoolean(1) + "]");
380408
}
381409
}
382-
case BYTES_REFS -> {
410+
case BYTES_REFS, ORDINALS -> {
383411
BytesRefBlock groups = (BytesRefBlock) block;
384412
for (int g = 0; g < GROUPS; g++) {
385413
if (false == groups.getBytesRef(g, new BytesRef()).equals(bytesGroup(g))) {
@@ -508,6 +536,8 @@ private static Block dataBlock(BlockFactory blockFactory, String blockType) {
508536
private static List<Block> groupingBlocks(String grouping, String blockType) {
509537
return switch (grouping) {
510538
case TWO_LONGS -> List.of(groupingBlock(LONGS, blockType), groupingBlock(LONGS, blockType));
539+
case TWO_BYTES_REFS -> List.of(groupingBlock(BYTES_REFS, blockType), groupingBlock(BYTES_REFS, blockType));
540+
case TWO_ORDINALS -> List.of(groupingBlock(ORDINALS, blockType), groupingBlock(ORDINALS, blockType));
511541
case LONGS_AND_BYTES_REFS -> List.of(groupingBlock(LONGS, blockType), groupingBlock(BYTES_REFS, blockType));
512542
case TWO_LONGS_AND_BYTES_REFS -> List.of(
513543
groupingBlock(LONGS, blockType),
@@ -570,6 +600,19 @@ private static Block groupingBlock(String grouping, String blockType) {
570600
}
571601
yield builder.build();
572602
}
603+
case ORDINALS -> {
604+
IntVector.Builder ordinals = blockFactory.newIntVectorBuilder(BLOCK_LENGTH * valuesPerGroup);
605+
for (int i = 0; i < BLOCK_LENGTH; i++) {
606+
for (int v = 0; v < valuesPerGroup; v++) {
607+
ordinals.appendInt(i % GROUPS);
608+
}
609+
}
610+
BytesRefVector.Builder bytes = blockFactory.newBytesRefVectorBuilder(BLOCK_LENGTH * valuesPerGroup);
611+
for (int i = 0; i < GROUPS; i++) {
612+
bytes.appendBytesRef(bytesGroup(i));
613+
}
614+
yield new OrdinalBytesRefVector(ordinals.build(), bytes.build()).asBlock();
615+
}
573616
default -> throw new UnsupportedOperationException("unsupported grouping [" + grouping + "]");
574617
};
575618
}

docs/changelog/114021.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 114021
2+
summary: "ESQL: Speed up grouping by bytes"
3+
area: ES|QL
4+
type: enhancement
5+
issues: []

x-pack/plugin/esql/compute/src/main/generated-src/org/elasticsearch/compute/aggregation/blockhash/BytesRefBlockHash.java

Lines changed: 32 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/plugin/esql/compute/src/main/generated-src/org/elasticsearch/compute/aggregation/blockhash/DoubleBlockHash.java

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/plugin/esql/compute/src/main/generated-src/org/elasticsearch/compute/aggregation/blockhash/IntBlockHash.java

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/plugin/esql/compute/src/main/generated-src/org/elasticsearch/compute/aggregation/blockhash/LongBlockHash.java

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/BlockHash.java

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.common.util.BigArrays;
1212
import org.elasticsearch.common.util.BitArray;
1313
import org.elasticsearch.common.util.BytesRefHash;
14+
import org.elasticsearch.common.util.Int3Hash;
1415
import org.elasticsearch.common.util.LongHash;
1516
import org.elasticsearch.common.util.LongLongHash;
1617
import org.elasticsearch.compute.aggregation.GroupingAggregatorFunction;
@@ -28,14 +29,37 @@
2829
import java.util.List;
2930

3031
/**
31-
* A specialized hash table implementation maps values of a {@link Block} to ids (in longs).
32-
* This class delegates to {@link LongHash} or {@link BytesRefHash}.
33-
*
34-
* @see LongHash
35-
* @see BytesRefHash
32+
* Specialized hash table implementations that map rows to a <strong>set</strong>
33+
* of bucket IDs to which they belong to implement {@code GROUP BY} expressions.
34+
* <p>
35+
* A row is always in at least one bucket so the results are never {@code null}.
36+
* {@code null} valued key columns will map to some integer bucket id.
37+
* If none of key columns are multivalued then the output is always an
38+
* {@link IntVector}. If any of the key are multivalued then a row is
39+
* in a bucket for each value. If more than one key is multivalued then
40+
* the row is in the combinatorial explosion of all value combinations.
41+
* Luckily for the number of values rows can only be in each bucket once.
42+
* Unluckily, it's the responsibility of {@link BlockHash} to remove those
43+
* duplicates.
44+
* </p>
45+
* <p>
46+
* These classes typically delegate to some combination of {@link BytesRefHash},
47+
* {@link LongHash}, {@link LongLongHash}, {@link Int3Hash}. They don't
48+
* <strong>technically</strong> have to be hash tables, so long as they
49+
* implement the deduplication semantics above and vend integer ids.
50+
* </p>
51+
* <p>
52+
* The integer ids are assigned to offsets into arrays of aggregation states
53+
* so its permissible to have gaps in the ints. But large gaps are a bad
54+
* idea because they'll waste space in the aggregations that use these
55+
* positions. For example, {@link BooleanBlockHash} assigns {@code 0} to
56+
* {@code null}, {@code 1} to {@code false}, and {@code 1} to {@code true}
57+
* and that's <strong>fine</strong> and simple and good because it'll never
58+
* leave a big gap, even if we never see {@code null}.
59+
* </p>
3660
*/
3761
public abstract sealed class BlockHash implements Releasable, SeenGroupIds //
38-
permits BooleanBlockHash, BytesRefBlockHash, DoubleBlockHash, IntBlockHash, LongBlockHash, BytesRef3BlockHash, //
62+
permits BooleanBlockHash, BytesRefBlockHash, DoubleBlockHash, IntBlockHash, LongBlockHash, BytesRef2BlockHash, BytesRef3BlockHash, //
3963
NullBlockHash, PackedValuesBlockHash, BytesRefLongBlockHash, LongLongBlockHash, TimeSeriesBlockHash {
4064

4165
protected final BlockFactory blockFactory;
@@ -98,8 +122,19 @@ public static BlockHash build(List<GroupSpec> groups, BlockFactory blockFactory,
98122
if (groups.size() == 1) {
99123
return newForElementType(groups.get(0).channel(), groups.get(0).elementType(), blockFactory);
100124
}
101-
if (groups.size() == 3 && groups.stream().allMatch(g -> g.elementType == ElementType.BYTES_REF)) {
102-
return new BytesRef3BlockHash(blockFactory, groups.get(0).channel, groups.get(1).channel, groups.get(2).channel, emitBatchSize);
125+
if (groups.stream().allMatch(g -> g.elementType == ElementType.BYTES_REF)) {
126+
switch (groups.size()) {
127+
case 2:
128+
return new BytesRef2BlockHash(blockFactory, groups.get(0).channel, groups.get(1).channel, emitBatchSize);
129+
case 3:
130+
return new BytesRef3BlockHash(
131+
blockFactory,
132+
groups.get(0).channel,
133+
groups.get(1).channel,
134+
groups.get(2).channel,
135+
emitBatchSize
136+
);
137+
}
103138
}
104139
if (allowBrokenOptimizations && groups.size() == 2) {
105140
var g1 = groups.get(0);

x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/BooleanBlockHash.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
import static org.elasticsearch.compute.operator.mvdedupe.MultivalueDedupeBoolean.TRUE_ORD;
2626

2727
/**
28-
* Maps a {@link BooleanBlock} column to group ids. Assigns group
29-
* {@code 0} to {@code false} and group {@code 1} to {@code true}.
28+
* Maps a {@link BooleanBlock} column to group ids. Assigns
29+
* {@code 0} to {@code null}, {@code 1} to {@code false}, and
30+
* {@code 2} to {@code true}.
3031
*/
3132
final class BooleanBlockHash extends BlockHash {
3233
private final int channel;

0 commit comments

Comments
 (0)