Repository navigation
Description
Summary
sequence is the highest-upside remaining codegen-dispatched expression. The dispatcher kernel makes
3 heap allocations per non-null row and passes over every generated element three times, and two
of those allocations scale with the sequence length rather than with the row count. For a 365-element
date spine that is roughly 48 MB of transient long[] garbage per 8192-row batch.
Assessed with the suggest-native-expression skill: compatibility confidence Medium (clean
plan-time guard, see scope), native upside High, and the upside is measured from the emitted
kernel, not inferred from Spark's source.
Recommendation is scoped: implement the integral element type natively, keep the date and timestamp
variants on the dispatcher.
How it runs today
object CometSequence extends CometCodegenDispatch[Sequence]
(spark/src/main/scala/org/apache/comet/serde/arrays.scala:867). A plain CometCodegenDispatch means
the dispatcher is the only path and getSupportLevel is Compatible().
ArrayType(IntegralType) is inside CometBatchKernelCodegen.isSupportedDataType, and I confirmed the
kernel really does compile (CometBatchKernelCodegen.compile with two nullable BigIntVector inputs
returns without throwing), so there is no hidden whole-operator Spark fallback here. The only
fallback trigger is spark.comet.exec.scalaUDF.codegen.enabled=false.
Native upside: High (measured)
Dumped the real kernel via CometBatchKernelCodegen.generateSource for
Sequence(BoundReference(0, LongType), BoundReference(1, LongType), None, Some("UTC")) over two
nullable BigIntVector inputs. The relevant emitted Java, verbatim:
arr_0 = new long[i_1];
while (i_1 > 0) {
i_1--;
arr_0[i_1] = (long) (value_1 + (value_1 <= value_2 ? 1L : -1L) * i_1);
}
value_0 = UnsafeArrayData.fromPrimitiveArray(arr_0);and on the output side:
org.apache.spark.sql.catalyst.util.ArrayData arr_1 = value_5;
int n_0 = arr_1.numElements();
int cidx_0 = output.startNewValue(i);
for (int j_0 = 0; j_0 < n_0; j_0++) {
outListChild_0.setSafe(cidx_0 + j_0, arr_1.getLong(j_0));
}
output.endValue(i, n_0);Per non-null row, where n is the sequence length:
| # | Allocation | Size |
|---|---|---|
| 1 | new long[n] (the element array) |
O(n) |
| 2 | new long[totalSizeInLongs] inside UnsafeArrayData.fromPrimitiveArray (UnsafeArrayData.java:431) |
O(n) |
| 3 | new UnsafeArrayData() (UnsafeArrayData.java:439) |
constant |
Plus three passes over every element: the fill loop, the Platform.copyMemory inside
fromPrimitiveArray, and the output-side getLong / setSafe loop.
Concretely, for a 365-element date spine at the default batch size: 2 arrays × 365 × 8 bytes × 8192
rows = ~48 MB of transient long[] per batch, all on the JVM heap and therefore invisible to
Comet's native memory pool, plus ~3M element writes where one pass would do.
A native kernel writes values straight into the Arrow ListArray child buffer and pushes offsets:
one pass, zero per-row heap allocation, no intermediate ArrayData representation at all.
Honest limits:
- Workload presence is unmeasured.
grepoverbenchmarks/tpc/queries/finds nosequence
usage.explode(sequence(start, stop, interval 1 day))for date spines is a common ETL shape, but
that is a judgment call rather than evidence. - The 48 MB figure is arithmetic on the measured allocation sites, not a heap profile.
Compatibility assessment: Medium (with a clean plan-time guard)
Spark versions
Behavior is stable across 3.4.3, 3.5.8, 4.0.1, and 4.1.1. Diffing the Sequence class body
across the four tags shows only:
- 3.4.3 → 3.5.8: internal refactors (
startType.sameType→DataTypeUtils.sameType,
iType.integral→PhysicalIntegralType.integral). - 3.5.8 → 4.0.1: adds
override lazy val throwable: Boolean = stepOpt.isDefined(an optimizer hint)
andcode.toStringindoGenCode. - 4.0.1 → 4.1.1: byte-identical.
No shims needed. TimeType is not accepted by sequence on 4.1.
Hazard checklist
| Hazard | Applies? |
|---|---|
| JVM formatting APIs | No |
java.util.regex features |
No |
| Collation on Spark 4.0+ | No (no string inputs) |
BigDecimal / MathContext |
No |
| Timezone and calendar | Yes, for the temporal element types only. TemporalSequenceImpl / PeriodSequenceImpl / DurationSequenceImpl carry a zoneId and step through DateTimeUtils.timestampAddInterval, so DST transitions, month-length arithmetic, and the spark.sql.legacy.* calendar flags are all in play. |
| ANSI error class and message parity | Yes, bounded. Two conditions, both enumerable: _LEGACY_ERROR_TEMP_3243 ("Illegal sequence boundaries") when the step direction does not match the bounds, and Sequence.sequenceLength's overflow / MAX_ROUNDED_ARRAY_LENGTH path. step == 0 && start == stop is legal and yields a single element. |
| Float/double text conversion | No |
| Invalid UTF-8 | No |
| Lambda bodies / arbitrary Catalyst trees | No |
| JVM session state beyond a scalar config | No (the timezone arrives as timeZoneId on the node) |
| Nondeterminism / ordering | No |
| Cross-version behavioral differences | No (see above) |
The timezone hazard is what makes this Medium rather than High. It is fully contained in the
temporal element types, and Sequence.impl selects on dataType.elementType, so the split is
knowable at plan time from start.dataType with no runtime probing.
Upstream is not usable
datafusion-spark54.1.0 has nosequence.datafusion-functions-nestedhasrange/generate_series, but the semantics differ (for
example"Cannot generate date range less than 1 day."atrange.rs:379and a different
zero-step message atrange.rs:460), and they are shaped for scalar arguments rather than a
per-row columnar sequence.
So this needs a Comet kernel.
Proposed approach
See the implement-comet-expression skill.
- Add a native
spark_sequenceundernative/spark-expr/, handlingInt8/Int16/Int32/Int64
start, stop, and step. Compute each row's length with Spark's overflow rules, reserve the child
buffer once for the whole batch, write values directly, push offsets. No per-row allocation. - Reproduce the two error conditions with matching classes and messages
(_LEGACY_ERROR_TEMP_3243and the array-size-limit error). - Change
CometSequencefromCometCodegenDispatch[Sequence]to aCometExpressionSerde[Sequence]
withCodegenDispatchFallback, returningUnsupported(Some(...))fromgetSupportLevelwhen
start.dataTypeis not anIntegralTypeso date and timestamp sequences keep running on the
dispatcher exactly as they do today. - Add
native/spark-expr/benches/sequence.rscovering the shapes from
optimizing_expressions.md: short sequences (2 to 5 elements), long sequences (365, 10k),
ascending / descending / zero-step, sparse and dense nulls, and the error path. - Add Comet SQL Tests at
spark/src/test/resources/sql-tests/expressions/array_funcs/sequence.sql.
Non-goals
- Native
DateType/TimestampType/TimestampNTZTypesequences. These stay on the dispatcher
until someone takes on the timezone, DST, and legacy-calendar work as a separate issue. - Any change to
explode, which is wheresequenceoutput usually goes.
Acceptance criteria
- Output bit-identical to the codegen-dispatch path for all integral element types, including null
placement,containsNull = falseon the result array,step == 0 && start == stop, and both error
conditions with matching error class and message. - Date and timestamp sequences demonstrably still route through the dispatcher (assert on the
[COMET-INFO: JVM codegen dispatcher: ...]EXPLAIN segment). - Criterion benchmark committed, no regression on any shape.
- The
sequenceentry indocs/source/contributor-guide/expression-audits/array_funcs.mdupdated
when the work lands.
Filed by the suggest-native-expression skill. Motivation:
Native Coverage for Codegen-Dispatched Expressions.
The assessment is recorded in
docs/source/contributor-guide/expression-audits/array_funcs.md under ## sequence.
Previous run of the same skill produced #5347.
Activity
0lai0 commentedon Aug 22, 2026
take