@@ -53,7 +53,8 @@ relation whose format could change mid-session could not be read back reliably.
5353codec is a runtime config, but each batch records the codec it was written with, so data cached
5454under one setting stays readable after the setting changes. Turning
5555` spark.comet.exec.inMemoryCache.enabled ` off at runtime only sends cached scans back to Spark's
56- execution path; the cached data stays readable either way.
56+ execution path, where Spark operators read them through the row reader described under
57+ [ Limitations] ( #limitations ) ; the cached data stays readable either way.
5758
5859## Storage format
5960
@@ -175,19 +176,45 @@ registrator.
175176
176177## Limitations
177178
178- Reads that feed ** Spark** operators rather than Comet ones are slower than Spark's own cache
179- format, and the narrower the read, the wider the gap. Measured by the same benchmark over the same
180- 5M-row relation, with Comet off so that Spark operators consume the cached data:
181-
182- | Read shape | Spark's cache format | Comet's cache format | Slowdown |
183- | ----------------------- | -------------------: | -------------------: | -------: |
184- | Row count only (0 of 6) | 35 ms | 183 ms | 5.2x |
185- | 1 of 6 columns | 54 ms | 257 ms | 4.8x |
186- | 3 of 6 columns | 98 ms | 331 ms | 3.4x |
187- | 6 of 6 columns | 410 ms | 623 ms | 1.5x |
188-
189- This is why the feature is off by default. The cause is not yet established;
190- [ #5485 ] ( https://github.com/apache/datafusion-comet/issues/5485 ) tracks it.
179+ Reads that feed ** Spark** operators rather than Comet ones can be slower than Spark's own cache
180+ format, because every cached batch is decoded from Arrow before Spark reads it. How a Spark
181+ operator reads a relation cached in Comet's format depends on the scan below it:
182+
183+ - With native execution enabled (` spark.comet.exec.enabled=true ` ), the cache is scanned by
184+ ` CometInMemoryTableScan ` , and a Spark operator above it reads the scan's batches through
185+ ` CometColumnarToRow ` , as it would above any other Comet operator.
186+ - With Comet enabled but native execution disabled, the cache is scanned by Spark's
187+ ` InMemoryTableScanExec ` . When the operator directly above the scan takes part in whole-stage code
188+ generation, as filters, projections and aggregates do, Comet puts Spark's ` ColumnarToRowExec `
189+ between the two, and the generated code reads the cached Arrow vectors directly, with no
190+ intermediate row. The plan shows this as a ` ColumnarToRow ` above the ` InMemoryTableScan ` . It
191+ needs ` spark.sql.inMemoryColumnarStorage.enableVectorizedReader ` (on by default) and whole-stage
192+ code generation, and applies to relations of at most ` spark.sql.codegen.maxFields ` fields (100 by
193+ default, counting nested fields), beyond which Spark reads a cached relation only as rows. It is
194+ not applied in plan-only mode (` spark.comet.explain.planOnly.enabled ` ), where Spark executes its
195+ own plan unchanged.
196+ - Otherwise the scan's row reader decodes each batch and writes its rows into one reused
197+ ` UnsafeRow ` . That covers Comet or ` spark.comet.exec.inMemoryCache.enabled ` turned off at runtime,
198+ and operators that do not take part in code generation, such as exchanges and limits, or a query
199+ that returns the cached rows as they are.
200+
201+ Measured by the same benchmark over the same 5M-row relation, with native execution off so that
202+ Spark operators consume the cached data, Comet disabled for the row reader and enabled for the fused
203+ reader (Apple M4, JDK 17, Spark 4.1; the average of two runs):
204+
205+ | Read shape | Spark's cache format | Comet's format, row reader | Comet's format, fused reader |
206+ | ----------------------- | -------------------: | -------------------------: | ---------------------------: |
207+ | Row count only (0 of 6) | 63 ms | 57 ms | 35 ms |
208+ | 1 of 6 columns | 63 ms | 77 ms | 54 ms |
209+ | 3 of 6 columns | 113 ms | 176 ms | 133 ms |
210+ | 6 of 6 columns | 306 ms | 500 ms | 334 ms |
211+
212+ The fused reader is faster than Spark's own format for the narrowest reads and within 20% of it for
213+ the others. The row reader takes up to 1.6 times as long, and it is the only reader for relations
214+ wider than ` spark.sql.codegen.maxFields ` : reading every column of relations of 100, 200 and 1500
215+ nullable ` bigint ` columns took 2.2 to 2.5 times as long as from Spark's format. These gaps are why
216+ the feature is still off by default;
217+ [ #5485 ] ( https://github.com/apache/datafusion-comet/issues/5485 ) tracks them.
191218
192219Comet's serializer exists because Spark's own Arrow cache format
193220([ SPARK-57268] ( https://issues.apache.org/jira/browse/SPARK-57268 ) ) is only available from Spark
0 commit comments