[ 
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)

Reply via email to