[
https://issues.apache.org/jira/browse/CALCITE-7805?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Frank Chen updated CALCITE-7805:
--------------------------------
Description:
CALCITE-7782 and [PR #5263|https://github.com/apache/calcite/pull/5263]
eliminated quadratic operand-list copying while validating large ARRAY and MAP
constructors. That correction allows a 1,000,000-element string ARRAY to
complete instead of running for more than 20 minutes.
After that correction, validation is linear, but planning still creates many
temporary objects for every string literal. In the benchmark below, planning a
1,000,000-element string IN expression allocates approximately 15.4 GB during
one operation and takes approximately 7.8 seconds.
The allocation values below are cumulative allocation during one planning
operation, not retained memory or peak heap usage.
h2. Reproducer
Apache Druid exposes the remaining allocation through a large string IN
predicate:
{code:sql}
SELECT string1
FROM foo
WHERE string1 IN ('1', '2', ..., '1000000')
{code}
Druid rewrites the predicate to an expression containing a string ARRAY:
{code}
SCALAR_IN_ARRAY(string1, ARRAY['1', '2', ..., '1000000'])
{code}
The benchmark is {{InPlanningBenchmark.queryStringInSqlPlanOnly}}.
Parameters:
* {{inSubQueryThreshold = 2147483647}}
* {{rowsPerSegment = 500000}}
* 2 JMH forks
* 2 one-second warmup iterations per fork
* 5 one-second measurement iterations per fork
* GC profiler enabled
* Temurin 25.0.4.1
The Calcite baseline is commit {{38413ece6}}, which includes the merged
CALCITE-7782 change.
h2. Remaining allocation sources
Profiling identified five independent allocation paths.
h3. 1. Generated CAST calls repeat operator resolution
ARRAY validation creates generated CAST calls when an operand type differs from
the common element type. The CAST node already contains the built-in CAST
operator, but its validated type is not registered immediately. A later
{{deriveType}} call constructs temporary operand and argument-type lists and
searches the operator table for the same CAST operator.
The generated CAST can instead be validated directly through its existing
operator, preserving operand validation and validated-type registration while
avoiding redundant routine lookup.
h3. 2. Unchanged charset and collation create equivalent types
During least-restrictive character-type inference,
{{BasicSqlType.createWithCharsetAndCollation}} may be called with the charset
and collation already present on the immutable type. The current implementation
creates another {{BasicSqlType}} and charset wrapper before canonicalization
returns the original canonical type. The method can return the same instance
when these attributes are unchanged.
h3. 3. Canonicalization occurs after temporary type construction
{{RelDataTypeFactoryImpl}} has a global weak canonical-type interner, but it
accepts an already constructed type. It prevents duplicate types from being
retained but does not prevent temporary {{BasicSqlType}}, charset wrapper, and
digest allocation.
A small bounded front cache can reuse the most recently created base and
decorated type for each {{SqlTypeName}} before constructing another type. The
existing global weak interner remains authoritative.
h3. 4. SQL and Rex visitors eagerly copy unchanged operands
{{SqlShuttle}} and {{RexShuttle}} copy operand arrays or lists before knowing
whether any visited child changes. Read-only traversals of a large literal
ARRAY therefore allocate replacement operand collections and later return the
original node.
The visitors can use copy-on-write behavior and allocate an operand collection
only after the first child changes. Explicit unconditional-copy behavior and
immutable-result contracts remain unchanged.
h3. 5. String literal digest generation creates a temporary SQL string
{{RexLiteral}} asks {{NlsString}} to build a complete escaped SQL string and
then copies that string into the destination {{StringBuilder}}. {{NlsString}}
can provide an overload that writes directly into the caller's builder, while
the existing string-returning method delegates to the same implementation.
h2. Combined benchmark
The changes were measured together because their allocation reductions overlap
and isolated percentages must not be added.
|| String literals || CALCITE-7782 baseline || All proposed changes ||
Allocation saved ||
| 100,000 | 1.522 GB/op | 0.445 GB/op | 1.077 GB/op (70.8%) |
| 1,000,000 | 15.418 GB/op | 4.556 GB/op | 10.862 GB/op (70.5%) |
The benchmark uses ordered numeric strings such as {{'1'}}, {{'2'}}, and
{{'10'}}. The bounded type cache benefits from consecutive literals with the
same character length. Fixed-width strings should have similar locality
regardless of value order, while arbitrary strings with frequently alternating
lengths may show a smaller improvement.
h2. Proposed delivery
The implementation is split into five independently reviewable sub-tasks:
# [CALCITE-7806|https://issues.apache.org/jira/browse/CALCITE-7806] - Avoid
resolving generated CAST calls during ARRAY validation.
# [CALCITE-7807|https://issues.apache.org/jira/browse/CALCITE-7807] - Reuse
unchanged SQL types during least-restrictive inference.
# [CALCITE-7808|https://issues.apache.org/jira/browse/CALCITE-7808] - Cache
recently created and decorated SQL types.
# [CALCITE-7809|https://issues.apache.org/jira/browse/CALCITE-7809] - Avoid
copying unchanged SQL and Rex operands.
# [CALCITE-7810|https://issues.apache.org/jira/browse/CALCITE-7810] - Write Rex
string literal digests directly.
Each sub-task links its corresponding pull request and can be reviewed and
tested independently.
was:
CALCITE-7782 and [PR #5263|https://github.com/apache/calcite/pull/5263]
eliminated quadratic operand-list copying while validating large ARRAY and MAP
constructors. That correction allows a 1,000,000-element string ARRAY to
complete instead of running for more than 20 minutes.
After that correction, validation is linear, but planning still creates many
temporary objects for every string literal. In the benchmark below, planning a
1,000,000-element string IN expression allocates approximately 15.4 GB during
one operation and takes approximately 7.8 seconds.
The allocation values below are cumulative allocation during one planning
operation, not retained memory or peak heap usage.
h2. Reproducer
Apache Druid exposes the remaining allocation through a large string IN
predicate:
{code:sql}
SELECT string1
FROM foo
WHERE string1 IN ('1', '2', ..., '1000000')
{code}
Druid rewrites the predicate to an expression containing a string ARRAY:
{code}
SCALAR_IN_ARRAY(string1, ARRAY['1', '2', ..., '1000000'])
{code}
The benchmark is {{InPlanningBenchmark.queryStringInSqlPlanOnly}}.
Parameters:
* {{inSubQueryThreshold = 2147483647}}
* {{rowsPerSegment = 500000}}
* 2 JMH forks
* 2 one-second warmup iterations per fork
* 5 one-second measurement iterations per fork
* GC profiler enabled
* Temurin 25.0.4.1
The Calcite baseline is commit {{38413ece6}}, which includes the merged
CALCITE-7782 change.
h2. Remaining allocation sources
Profiling identified five independent allocation paths.
h3. 1. Generated CAST calls repeat operator resolution
ARRAY validation creates generated CAST calls when an operand type differs from
the common element type. The CAST node already contains the built-in CAST
operator, but its validated type is not registered immediately. A later
{{deriveType}} call constructs temporary operand and argument-type lists and
searches the operator table for the same CAST operator.
The generated CAST can instead be validated directly through its existing
operator, preserving operand validation and validated-type registration while
avoiding redundant routine lookup.
h3. 2. Unchanged charset and collation create equivalent types
During least-restrictive character-type inference,
{{BasicSqlType.createWithCharsetAndCollation}} may be called with the charset
and collation already present on the immutable type. The current implementation
creates another {{BasicSqlType}} and charset wrapper before canonicalization
returns the original canonical type. The method can return the same instance
when these attributes are unchanged.
h3. 3. Canonicalization occurs after temporary type construction
{{RelDataTypeFactoryImpl}} has a global weak canonical-type interner, but it
accepts an already constructed type. It prevents duplicate types from being
retained but does not prevent temporary {{BasicSqlType}}, charset wrapper, and
digest allocation.
A small bounded front cache can reuse the most recently created base and
decorated type for each {{SqlTypeName}} before constructing another type. The
existing global weak interner remains authoritative.
h3. 4. SQL and Rex visitors eagerly copy unchanged operands
{{SqlShuttle}} and {{RexShuttle}} copy operand arrays or lists before knowing
whether any visited child changes. Read-only traversals of a large literal
ARRAY therefore allocate replacement operand collections and later return the
original node.
The visitors can use copy-on-write behavior and allocate an operand collection
only after the first child changes. Explicit unconditional-copy behavior and
immutable-result contracts remain unchanged.
h3. 5. String literal digest generation creates a temporary SQL string
{{RexLiteral}} asks {{NlsString}} to build a complete escaped SQL string and
then copies that string into the destination {{StringBuilder}}. {{NlsString}}
can provide an overload that writes directly into the caller's builder, while
the existing string-returning method delegates to the same implementation.
h2. Combined benchmark
The changes were measured together because their allocation reductions overlap
and isolated percentages must not be added.
|| String literals || CALCITE-7782 baseline || All proposed changes ||
Allocation saved ||
| 100,000 | 1.522 GB/op | 0.445 GB/op | 1.077 GB/op (70.8%) |
| 1,000,000 | 15.418 GB/op | 4.556 GB/op | 10.862 GB/op (70.5%) |
The benchmark uses ordered numeric strings such as {{'1'}}, {{'2'}}, and
{{'10'}}. The bounded type cache benefits from consecutive literals with the
same character length. Fixed-width strings should have similar locality
regardless of value order, while arbitrary strings with frequently alternating
lengths may show a smaller improvement.
h2. Proposed delivery
The implementation is split into five independently reviewable pull requests:
# [PR #5280|https://github.com/apache/calcite/pull/5280] - Avoid resolving
generated CAST calls again during ARRAY validation.
# [PR #5281|https://github.com/apache/calcite/pull/5281] - Reuse immutable
{{BasicSqlType}} instances when charset and collation are unchanged.
# [PR #5282|https://github.com/apache/calcite/pull/5282] - Add bounded
recent-type caches before canonical type construction.
# [PR #5283|https://github.com/apache/calcite/pull/5283] - Make {{SqlShuttle}}
and {{RexShuttle}} operand copying copy-on-write.
# [PR #5284|https://github.com/apache/calcite/pull/5284] - Write string literal
SQL representations directly into the destination builder.
Each change preserves existing validation and canonicalization semantics and
can be reviewed and tested independently.
> Reduce temporary object allocation when validating large string ARRAYs to
> lower GC pressure
> -------------------------------------------------------------------------------------------
>
> Key: CALCITE-7805
> URL: https://issues.apache.org/jira/browse/CALCITE-7805
> Project: Calcite
> Issue Type: Improvement
> Components: core
> Affects Versions: 1.42.0
> Reporter: Frank Chen
> Priority: Major
> Labels: pull-request-available
>
> CALCITE-7782 and [PR #5263|https://github.com/apache/calcite/pull/5263]
> eliminated quadratic operand-list copying while validating large ARRAY and
> MAP constructors. That correction allows a 1,000,000-element string ARRAY to
> complete instead of running for more than 20 minutes.
> After that correction, validation is linear, but planning still creates many
> temporary objects for every string literal. In the benchmark below, planning
> a 1,000,000-element string IN expression allocates approximately 15.4 GB
> during one operation and takes approximately 7.8 seconds.
> The allocation values below are cumulative allocation during one planning
> operation, not retained memory or peak heap usage.
> h2. Reproducer
> Apache Druid exposes the remaining allocation through a large string IN
> predicate:
> {code:sql}
> SELECT string1
> FROM foo
> WHERE string1 IN ('1', '2', ..., '1000000')
> {code}
> Druid rewrites the predicate to an expression containing a string ARRAY:
> {code}
> SCALAR_IN_ARRAY(string1, ARRAY['1', '2', ..., '1000000'])
> {code}
> The benchmark is {{InPlanningBenchmark.queryStringInSqlPlanOnly}}.
> Parameters:
> * {{inSubQueryThreshold = 2147483647}}
> * {{rowsPerSegment = 500000}}
> * 2 JMH forks
> * 2 one-second warmup iterations per fork
> * 5 one-second measurement iterations per fork
> * GC profiler enabled
> * Temurin 25.0.4.1
> The Calcite baseline is commit {{38413ece6}}, which includes the merged
> CALCITE-7782 change.
> h2. Remaining allocation sources
> Profiling identified five independent allocation paths.
> h3. 1. Generated CAST calls repeat operator resolution
> ARRAY validation creates generated CAST calls when an operand type differs
> from the common element type. The CAST node already contains the built-in
> CAST operator, but its validated type is not registered immediately. A later
> {{deriveType}} call constructs temporary operand and argument-type lists and
> searches the operator table for the same CAST operator.
> The generated CAST can instead be validated directly through its existing
> operator, preserving operand validation and validated-type registration while
> avoiding redundant routine lookup.
> h3. 2. Unchanged charset and collation create equivalent types
> During least-restrictive character-type inference,
> {{BasicSqlType.createWithCharsetAndCollation}} may be called with the charset
> and collation already present on the immutable type. The current
> implementation creates another {{BasicSqlType}} and charset wrapper before
> canonicalization returns the original canonical type. The method can return
> the same instance when these attributes are unchanged.
> h3. 3. Canonicalization occurs after temporary type construction
> {{RelDataTypeFactoryImpl}} has a global weak canonical-type interner, but it
> accepts an already constructed type. It prevents duplicate types from being
> retained but does not prevent temporary {{BasicSqlType}}, charset wrapper,
> and digest allocation.
> A small bounded front cache can reuse the most recently created base and
> decorated type for each {{SqlTypeName}} before constructing another type. The
> existing global weak interner remains authoritative.
> h3. 4. SQL and Rex visitors eagerly copy unchanged operands
> {{SqlShuttle}} and {{RexShuttle}} copy operand arrays or lists before knowing
> whether any visited child changes. Read-only traversals of a large literal
> ARRAY therefore allocate replacement operand collections and later return the
> original node.
> The visitors can use copy-on-write behavior and allocate an operand
> collection only after the first child changes. Explicit unconditional-copy
> behavior and immutable-result contracts remain unchanged.
> h3. 5. String literal digest generation creates a temporary SQL string
> {{RexLiteral}} asks {{NlsString}} to build a complete escaped SQL string and
> then copies that string into the destination {{StringBuilder}}. {{NlsString}}
> can provide an overload that writes directly into the caller's builder, while
> the existing string-returning method delegates to the same implementation.
> h2. Combined benchmark
> The changes were measured together because their allocation reductions
> overlap and isolated percentages must not be added.
> || String literals || CALCITE-7782 baseline || All proposed changes ||
> Allocation saved ||
> | 100,000 | 1.522 GB/op | 0.445 GB/op | 1.077 GB/op (70.8%) |
> | 1,000,000 | 15.418 GB/op | 4.556 GB/op | 10.862 GB/op (70.5%) |
> The benchmark uses ordered numeric strings such as {{'1'}}, {{'2'}}, and
> {{'10'}}. The bounded type cache benefits from consecutive literals with the
> same character length. Fixed-width strings should have similar locality
> regardless of value order, while arbitrary strings with frequently
> alternating lengths may show a smaller improvement.
> h2. Proposed delivery
> The implementation is split into five independently reviewable sub-tasks:
> # [CALCITE-7806|https://issues.apache.org/jira/browse/CALCITE-7806] - Avoid
> resolving generated CAST calls during ARRAY validation.
> # [CALCITE-7807|https://issues.apache.org/jira/browse/CALCITE-7807] - Reuse
> unchanged SQL types during least-restrictive inference.
> # [CALCITE-7808|https://issues.apache.org/jira/browse/CALCITE-7808] - Cache
> recently created and decorated SQL types.
> # [CALCITE-7809|https://issues.apache.org/jira/browse/CALCITE-7809] - Avoid
> copying unchanged SQL and Rex operands.
> # [CALCITE-7810|https://issues.apache.org/jira/browse/CALCITE-7810] - Write
> Rex string literal digests directly.
> Each sub-task links its corresponding pull request and can be reviewed and
> tested independently.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)