Reducing Row/Columnar Conversion Overhead#
When a query stage contains many operators that fall back to Spark row-based execution, Comet may insert
repeated columnar-to-row and row-to-columnar conversions that dominate stage runtime. Set
spark.comet.exec.transitionRevert.enabled=true to have Comet revert the entire stage to Spark row execution
when the number of columnar-to-row transitions exceeds
spark.comet.exec.transitionRevert.maxTransitions (default 2). This trades native execution of a small
subset of operators for eliminating conversion overhead across the stage. A stage is not reverted when it holds a
native aggregate whose intermediate buffer Spark cannot exchange with Comet across a stage boundary, because
reverting it would split that aggregate between the two engines.
Wide or Deeply Nested Schemas#
The cost of each conversion also grows sharply with schema shape: for wide or deeply nested schemas,
columnar-to-row conversion is especially expensive because the conversion work scales with the number of
columns and nested fields. If profiling shows these conversions dominating a query over such a schema, set
spark.comet.exec.transitionRevert.enabled=true and lower
spark.comet.exec.transitionRevert.maxTransitions (default 2) to 1. Note that this reverts every stage that
exceeds the threshold to Spark row-based execution — Comet removes the stage’s native operators rather than running a
mix of native and fallback operators joined by repeated conversions — which can be cheaper than paying the
expensive conversions again and again.
Experimental: Direct Columnar-to-Row Conversion#
When the JVM columnar-to-row operator is in use (spark.comet.exec.columnarToRow.native.enabled=false),
setting spark.comet.exec.columnarToRow.direct.enabled=true enables an experimental converter that writes
values straight from Arrow buffers into Spark’s row format without allocating an object per value. This is
most beneficial for decimal-heavy schemas, where the default conversion allocates a Decimal object per
value (and considerably more for decimals with precision above 18); microbenchmarks show up to 2x faster
conversion and a large reduction in garbage creation for such schemas. Schemas containing data types the
converter does not support fall back to the default conversion automatically.
Batches with fewer rows than spark.comet.exec.columnarToRow.direct.minBatchSize (default 128) also fall
back to the default conversion, since the direct converter’s per-batch setup does not pay off on very small
batches. This optimization is experimental: it only affects the operator’s non-codegen paths (including
broadcast relation builds), and the default conversion remains enabled unless explicitly opted in.
The direct converter is not faster for every schema: schemas with strings and no expensive decimals can get
slower with it enabled. For them the default conversion is already efficient, because its UnsafeProjection is
code generated for the schema, and a string column keeps the direct converter off the fixed-width path described
below. In the microbenchmarks above, a long, int, double, string schema at 8,192 rows per batch took 12.1 ns
per row with the direct converter against 9.4 ns per row with the default conversion, about 29% slower, even
though it allocated less. Measure a representative workload before enabling the direct converter, rather than
turning it on for a whole cluster.
The converter takes a fixed-width path when every column has a fixed width in Spark’s row format, that is,
when the schema has no strings and no decimals with precision above 18. That path converts the whole batch
as soon as it is set, so a consumer that stops early, such as a LIMIT, pays for every row of the batch where
the default conversion is lazy per row. It also keeps one buffer of the batch’s rows per task for the life of
the task, on heap and outside Comet’s memory accounting: for example about 6.7 MB for 100 fixed-width columns
at 8,192 rows per batch.