Skip to content

Commit dc4fa26

Browse files
authored
Speed up COALESCE significantly (#120139)
``` before after (operation) Score Error Score Error Units coalesce_2_noop 75.949 ± 3.961 -> 0.010 ± 0.001 ns/op 99.9% coalesce_2_eager 99.299 ± 6.959 -> 4.292 ± 0.227 ns/op 95.7% coalesce_2_lazy 113.118 ± 5.747 -> 26.746 ± 0.954 ns/op 76.4% ``` We tend to advise folks that "COALESCE is faster than CASE", but, as of 8.16.0/#112295 that wasn't the true. I was working with someone a few days ago to port a scripted_metric aggregation to ESQL and we saw COALESCE taking ~60% of the time. That won't do. The trouble is that CASE and COALESCE have to be *lazy*, meaning that operations like: ``` COALESCE(a, 1 / b) ``` should never emit a warning if `a` is not `null`, even if `b` is `0`. In 8.16/#112295 CASE grew an optimization where it could operate non-lazily if it was flagged as "safe". This brings a similar optimization to COALESCE, see it above as "case_2_eager", a 95.7% improvement. It also brings and arguably more important optimization - entire-block execution for COALESCE. The schort version is that, if the first parameter of COALESCE returns no nulls we can return it without doing anything lazily. There are a few more cases, but the upshot is that COALESCE is pretty much *free* in cases where long strings of results are `null` or not `null`. That's the `coalesce_2_noop` line. Finally, when there mixed null and non-null values we were using a single builder with some fairly inefficient paths. This specializes them per type and skips some slow null-checking where possible. That's the `coalesce_2_lazy` result, a more modest 76.4%. NOTE: These %s of improvements on COALESCE itself, or COALESCE with some load-overhead operators like `+`. If COALESCE isn't taking a *ton* time in your query don't get particularly excited about this. It's fun though. Closes #119953
1 parent f27f746 commit dc4fa26

File tree

30 files changed

+1943
-195
lines changed

30 files changed

+1943
-195
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/*.interp li
1111
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseLexer*.java linguist-generated=true
1212
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser*.java linguist-generated=true
1313
x-pack/plugin/esql/src/main/generated/** linguist-generated=true
14+
x-pack/plugin/esql/src/main/generated-src/** linguist-generated=true
1415

1516
# ESQL functions docs are autogenerated. More information at `docs/reference/esql/functions/README.md`
1617
docs/reference/esql/functions/*/** linguist-generated=true

benchmarks/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,12 @@ exit
126126
Grab the async profiler from https://github.com/jvm-profiling-tools/async-profiler
127127
and run `prof async` like so:
128128
```
129-
gradlew -p benchmarks/ run --args 'LongKeyedBucketOrdsBenchmark.multiBucket -prof "async:libPath=/home/nik9000/Downloads/tmp/async-profiler-1.8.3-linux-x64/build/libasyncProfiler.so;dir=/tmp/prof;output=flamegraph"'
129+
gradlew -p benchmarks/ run --args 'LongKeyedBucketOrdsBenchmark.multiBucket -prof "async:libPath=/home/nik9000/Downloads/async-profiler-3.0-29ee888-linux-x64/lib/libasyncProfiler.so;dir=/tmp/prof;output=flamegraph"'
130130
```
131131

132+
Note: As of January 2025 the latest release of async profiler doesn't work
133+
with our JDK but the nightly is fine.
134+
132135
If you are on Mac, this'll warn you that you downloaded the shared library from
133136
the internet. You'll need to go to settings and allow it to run.
134137

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

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc;
3939
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Abs;
4040
import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMin;
41+
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
4142
import org.elasticsearch.xpack.esql.expression.function.scalar.string.RLike;
4243
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add;
4344
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
@@ -96,6 +97,9 @@ public class EvalBenchmark {
9697
"add_double",
9798
"case_1_eager",
9899
"case_1_lazy",
100+
"coalesce_2_noop",
101+
"coalesce_2_eager",
102+
"coalesce_2_lazy",
99103
"date_trunc",
100104
"equal_to_const",
101105
"long_equal_to_long",
@@ -142,8 +146,34 @@ private static EvalOperator.ExpressionEvaluator evaluator(String operation) {
142146
lhs = new Add(Source.EMPTY, lhs, new Literal(Source.EMPTY, 1L, DataType.LONG));
143147
rhs = new Add(Source.EMPTY, rhs, new Literal(Source.EMPTY, 1L, DataType.LONG));
144148
}
145-
yield EvalMapper.toEvaluator(FOLD_CONTEXT, new Case(Source.EMPTY, condition, List.of(lhs, rhs)), layout(f1, f2))
146-
.get(driverContext);
149+
EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
150+
FOLD_CONTEXT,
151+
new Case(Source.EMPTY, condition, List.of(lhs, rhs)),
152+
layout(f1, f2)
153+
).get(driverContext);
154+
String desc = operation.endsWith("lazy") ? "CaseLazyEvaluator" : "CaseEagerEvaluator";
155+
if (evaluator.toString().contains(desc) == false) {
156+
throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
157+
}
158+
yield evaluator;
159+
}
160+
case "coalesce_2_noop", "coalesce_2_eager", "coalesce_2_lazy" -> {
161+
FieldAttribute f1 = longField();
162+
FieldAttribute f2 = longField();
163+
Expression lhs = f1;
164+
if (operation.endsWith("lazy")) {
165+
lhs = new Add(Source.EMPTY, lhs, new Literal(Source.EMPTY, 1L, DataType.LONG));
166+
}
167+
EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
168+
FOLD_CONTEXT,
169+
new Coalesce(Source.EMPTY, lhs, List.of(f2)),
170+
layout(f1, f2)
171+
).get(driverContext);
172+
String desc = operation.endsWith("lazy") ? "CoalesceLazyEvaluator" : "CoalesceEagerEvaluator";
173+
if (evaluator.toString().contains(desc) == false) {
174+
throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
175+
}
176+
yield evaluator;
147177
}
148178
case "date_trunc" -> {
149179
FieldAttribute timestamp = new FieldAttribute(
@@ -260,6 +290,38 @@ private static void checkExpected(String operation, Page actual) {
260290
}
261291
}
262292
}
293+
case "coalesce_2_noop" -> {
294+
LongVector f1 = actual.<LongBlock>getBlock(0).asVector();
295+
LongVector result = actual.<LongBlock>getBlock(2).asVector();
296+
for (int i = 0; i < BLOCK_LENGTH; i++) {
297+
long expected = f1.getLong(i);
298+
if (result.getLong(i) != expected) {
299+
throw new AssertionError("[" + operation + "] expected [" + expected + "] but was [" + result.getLong(i) + "]");
300+
}
301+
}
302+
}
303+
case "coalesce_2_eager" -> {
304+
LongBlock f1 = actual.<LongBlock>getBlock(0);
305+
LongVector f2 = actual.<LongBlock>getBlock(1).asVector();
306+
LongVector result = actual.<LongBlock>getBlock(2).asVector();
307+
for (int i = 0; i < BLOCK_LENGTH; i++) {
308+
long expected = i % 5 == 0 ? f2.getLong(i) : f1.getLong(f1.getFirstValueIndex(i));
309+
if (result.getLong(i) != expected) {
310+
throw new AssertionError("[" + operation + "] expected [" + expected + "] but was [" + result.getLong(i) + "]");
311+
}
312+
}
313+
}
314+
case "coalesce_2_lazy" -> {
315+
LongBlock f1 = actual.<LongBlock>getBlock(0);
316+
LongVector f2 = actual.<LongBlock>getBlock(1).asVector();
317+
LongVector result = actual.<LongBlock>getBlock(2).asVector();
318+
for (int i = 0; i < BLOCK_LENGTH; i++) {
319+
long expected = i % 5 == 0 ? f2.getLong(i) : f1.getLong(f1.getFirstValueIndex(i)) + 1;
320+
if (result.getLong(i) != expected) {
321+
throw new AssertionError("[" + operation + "] expected [" + expected + "] but was [" + result.getLong(i) + "]");
322+
}
323+
}
324+
}
263325
case "date_trunc" -> {
264326
LongVector v = actual.<LongBlock>getBlock(1).asVector();
265327
long oneDay = TimeValue.timeValueHours(24).millis();
@@ -304,7 +366,7 @@ private static void checkExpected(String operation, Page actual) {
304366
}
305367
}
306368
}
307-
default -> throw new UnsupportedOperationException();
369+
default -> throw new UnsupportedOperationException(operation);
308370
}
309371
}
310372

@@ -324,7 +386,7 @@ private static Page page(String operation) {
324386
}
325387
yield new Page(builder.build());
326388
}
327-
case "case_1_eager", "case_1_lazy" -> {
389+
case "case_1_eager", "case_1_lazy", "coalesce_2_noop" -> {
328390
var f1 = blockFactory.newLongBlockBuilder(BLOCK_LENGTH);
329391
var f2 = blockFactory.newLongBlockBuilder(BLOCK_LENGTH);
330392
for (int i = 0; i < BLOCK_LENGTH; i++) {
@@ -333,6 +395,19 @@ private static Page page(String operation) {
333395
}
334396
yield new Page(f1.build(), f2.build());
335397
}
398+
case "coalesce_2_eager", "coalesce_2_lazy" -> {
399+
var f1 = blockFactory.newLongBlockBuilder(BLOCK_LENGTH);
400+
var f2 = blockFactory.newLongBlockBuilder(BLOCK_LENGTH);
401+
for (int i = 0; i < BLOCK_LENGTH; i++) {
402+
if (i % 5 == 0) {
403+
f1.appendNull();
404+
} else {
405+
f1.appendLong(i);
406+
}
407+
f2.appendLong(-i);
408+
}
409+
yield new Page(f1.build(), f2.build());
410+
}
336411
case "long_equal_to_long" -> {
337412
var lhs = blockFactory.newLongBlockBuilder(BLOCK_LENGTH);
338413
var rhs = blockFactory.newLongBlockBuilder(BLOCK_LENGTH);

x-pack/plugin/esql/build.gradle

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,4 +348,31 @@ tasks.named('stringTemplates').configure {
348348
it.inputFile = inInputFile
349349
it.outputFile = "org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/InBytesRefEvaluator.java"
350350
}
351+
352+
File coalesceInputFile = file("src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/nulls/X-CoalesceEvaluator.java.st")
353+
template {
354+
it.properties = booleanProperties
355+
it.inputFile = coalesceInputFile
356+
it.outputFile = "org/elasticsearch/xpack/esql/expression/function/scalar/nulls/CoalesceBooleanEvaluator.java"
357+
}
358+
template {
359+
it.properties = intProperties
360+
it.inputFile = coalesceInputFile
361+
it.outputFile = "org/elasticsearch/xpack/esql/expression/function/scalar/nulls/CoalesceIntEvaluator.java"
362+
}
363+
template {
364+
it.properties = longProperties
365+
it.inputFile = coalesceInputFile
366+
it.outputFile = "org/elasticsearch/xpack/esql/expression/function/scalar/nulls/CoalesceLongEvaluator.java"
367+
}
368+
template {
369+
it.properties = doubleProperties
370+
it.inputFile = coalesceInputFile
371+
it.outputFile = "org/elasticsearch/xpack/esql/expression/function/scalar/nulls/CoalesceDoubleEvaluator.java"
372+
}
373+
template {
374+
it.properties = bytesRefProperties
375+
it.inputFile = coalesceInputFile
376+
it.outputFile = "org/elasticsearch/xpack/esql/expression/function/scalar/nulls/CoalesceBytesRefEvaluator.java"
377+
}
351378
}

x-pack/plugin/esql/compute/src/main/generated-src/org/elasticsearch/compute/data/BooleanBlock.java

Lines changed: 8 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/data/BooleanBlockBuilder.java

Lines changed: 37 additions & 15 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/data/BytesRefBlock.java

Lines changed: 10 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/data/BytesRefBlockBuilder.java

Lines changed: 38 additions & 15 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/data/DoubleBlock.java

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

0 commit comments

Comments
 (0)