likun666661 commented on PR #50372:
URL: https://github.com/apache/arrow/pull/50372#issuecomment-5010517751

   Thank you again for the detailed review. I have completed a local follow-up
   prototype and benchmark pass addressing the two remaining concerns: the 
conservative
   "do not fold" contract and large-expression performance evidence.
   
   I have **not pushed these changes yet**. I would like to confirm the 
narrowed design
   with you before updating the PR.
   
   ## Proposed narrowed design
   
   I split the implementation into two conservative layers:
   
   1. **AST structural sharing**
      - Canonicalize structurally identical fields, literals, and safe built-in 
function
        subtrees into a DAG.
      - Use a structural key composed of node kind, function/field/literal 
identity,
        return type, and ordered child atom IDs.
      - Preserve operand order. There is no commutative normalization.
   
   2. **Same-basic-block Dex/LLVM value reuse**
      - Reuse a decomposed `ValueValidityPair` only for a reusable canonical 
AST node.
      - Reuse a generated `LValue` only when the same `Dex` was generated in 
the same
        LLVM `BasicBlock`.
      - Cache a generated value only when code generation ends in the block 
where it
        started.
   
   I removed the Boolean/`if` algebraic transformations from the local version:
   
   - no Boolean flattening or predicate deduplication;
   - no `if(c, x, x) -> x`;
   - no reuse across Boolean short-circuit blocks or `if` branches;
   - no reuse across separately compiled output expressions.
   
   This is intentionally a post-construction Gandiva pass. It canonicalizes an 
AST after
   the caller has already built it in `Projector::Make()` or `Filter::Make()`. 
It reduces
   decomposition, generated IR, LLVM optimization, and JIT work, but does not 
claim to
   remove caller-side `TreeExprBuilder` or SQL-to-Gandiva construction cost. 
That is an
   important difference from the Cloudberry article that initially motivated 
this work.
   
   ## Safety contract
   
   A function subtree is reusable only when:
   
   ```text
   the function is a Gandiva built-in
   and result nullability is not kResultNullInternal
   and it does not need ExecutionContext
   and it does not need a FunctionHolder
   and it cannot return errors
   and every child subtree is reusable
   ```
   
   The built-in-only rule prevents a custom function registered with default 
flags from
   being treated as pure accidentally.
   
   Functions such as `lower` and `upper` remain excluded in this initial 
version. They are
   deterministic at the SQL level, but Gandiva marks them `kNeedsContext`; their
   implementations allocate output from the execution-context arena and may set 
errors for
   invalid UTF-8, invalid lengths, or allocation failures. I think those 
functions need
   explicit effect/CSE-safety metadata rather than relaxing the context 
restriction
   globally.
   
   ## Affected modules
   
   | Module | Responsibility |
   | --- | --- |
   | `expr_cse.{h,cc}` | Structural keys, canonical AST nodes, shared safety 
predicate |
   | `function_registry.{h,cc}` | Distinguish built-ins from custom registered 
functions |
   | `projector.cc`, `filter.cc` | Fold before cache lookup, validation, and 
code generation |
   | `expr_decomposer.{h,cc}` | Cache safe decompositions by canonical node 
identity |
   | `llvm_generator.{h,cc}` | Reuse generated `LValue` only in the same basic 
block |
   | `engine.h`, `projector.cc` | Guard unoptimized IR using actual captured 
engine state |
   | CSE/projector/filter tests | Safety-contract, IR-shape, runtime, and cache 
regressions |
   | `micro_benchmarks.cc` | Fold, build, IR-size, and evaluation benchmarks |
   
   ## Module flow
   
   ```mermaid
   flowchart LR
     Caller["Gandiva caller"]
     Entry["Projector::Make / Filter::Make"]
     Folder["AST structural CSE"]
     Registry["FunctionRegistry safety/provenance"]
     Cache["Gandiva object cache"]
     Decomposer["ExprDecomposer"]
     Generator["LLVMGenerator::Visitor"]
     BlockCache["Basic-block Dex cache"]
     Engine["LLVM optimizer and JIT"]
   
     Caller --> Entry
     Entry --> Folder
     Folder --> Registry
     Entry --> Cache
     Entry --> Decomposer
     Decomposer --> Registry
     Decomposer --> Generator
     Generator --> BlockCache
     Generator --> Engine
   ```
   
   ## Build sequence
   
   ```mermaid
   sequenceDiagram
     participant C as Caller
     participant P as Projector or Filter
     participant F as AST folder
     participant R as FunctionRegistry
     participant D as ExprDecomposer
     participant G as LLVMGenerator
     participant E as Engine
   
     C->>P: Make(schema, expressions, config)
     P->>F: FoldCommonSubexpressions
     loop bottom-up AST traversal
       F->>R: LookupSignature and IsBuiltIn
       F->>F: intern safe structural node
     end
     F-->>P: folded AST DAG
     P->>D: Decompose folded AST
     D->>R: verify subtree reuse safety
     D-->>G: shared ValueValidityPair/Dex graph
     G->>G: reuse Dex value only in current BasicBlock
     G->>E: FinalizeModule
     E->>E: capture optional pre-optimization IR
     E->>E: optimize and JIT
     E-->>P: compiled code
     P-->>C: Projector or Filter
   ```
   
   ## Focused safety tests
   
   The local `FoldCommonSubexpressions()` test suite now directly covers:
   
   - positive sharing within one expression and across input expression objects;
   - `kResultNullInternal`;
   - `NeedsContext()`;
   - `NeedsFunctionHolder()`;
   - `CanReturnErrors()`;
   - a safe parent with an unsafe child;
   - unknown functions;
   - custom registered functions with default flags;
   - opaque `InExpressionNode`;
   - different literal values and types;
   - operand order (`add(a,b)` versus `add(b,a)`);
   - Boolean algebra not being applied;
   - `if` algebra not being applied.
   
   IR/runtime tests additionally verify:
   
   - two repeated `add` calls become one in the same block;
   - repeated values are not reused across `if` branches;
   - `if(c, x, x)` still contains its condition, branch blocks, and phi;
   - Boolean short-circuit CFG is preserved;
   - the nested repeated-`between` pattern still emits three copies of each 
comparison
     because the copies occur in different Boolean blocks;
   - filter cache behavior no longer relies on Boolean deduplication;
   - cached and mutable-configuration unoptimized-IR error paths.
   
   Local test results:
   
   | Test binary | Result |
   | --- | --- |
   | `gandiva-internals-test` | 169 passed |
   | `gandiva-projector-test` with `TZ=UTC` | 226 passed, 1 existing aarch64 
skip |
   | `gandiva-precompiled-test` | 132 passed |
   | `git diff --check` | passed |
   
   ## Benchmark method
   
   - Apple M4, Release build, LLVM 20.1.7;
   - Google Benchmark, three repetitions, CPU-time mean;
   - expression sizes 10, 100, and 1,000;
   - three patterns: deep unique, balanced repeated safe, and balanced repeated 
unsafe;
   - measured fold-only, Projector/Filter build, unoptimized IR bytes, and 
evaluation
     separately;
   - the disabled baseline bypassed only the AST folding calls, with identical 
inputs and
     LLVM settings;
   - input AST construction was paused outside the timed build section.
   
   ## Results at size 1,000
   
   | Pattern | Projector build, disabled -> enabled | Filter build, disabled -> 
enabled | Projector unoptimized IR |
   | --- | ---: | ---: | ---: |
   | Deep unique | 178.7 -> 186.4 ms (+4.3%) | 50.4 -> 52.1 ms (+3.4%) | 
116,902 -> 116,902 bytes |
   | Repeated safe | 81.6 -> 21.4 ms (-73.8%) | 78.5 -> 22.0 ms (-72.0%) | 
1,923,715 -> 5,963 bytes (-99.69%) |
   | Repeated unsafe | 4,256.4 -> 4,356.6 ms (+2.4%) | 4,450.3 -> 4,487.1 ms 
(+0.8%) | 2,350,279 -> 2,350,279 bytes |
   
   Repeated-safe scaling:
   
   | Size | Projector build | Filter build | IR reduction |
   | ---: | ---: | ---: | ---: |
   | 10 | 17.1 -> 18.2 ms (+6.5%) | 15.9 -> 16.0 ms (+0.5%) | -78.2% |
   | 100 | 20.7 -> 17.7 ms (-14.5%) | 19.0 -> 16.6 ms (-12.6%) | -97.2% |
   | 1,000 | 81.6 -> 21.4 ms (-73.8%) | 78.5 -> 22.0 ms (-72.0%) | -99.69% |
   
   Evaluation at size 1,000:
   
   | Pattern | Projector | Filter |
   | --- | ---: | ---: |
   | Deep unique | 254.44 -> 255.10 us (+0.3%) | 433.97 -> 434.44 us (+0.1%) |
   | Repeated safe | 19.42 -> 17.24 us (-11.2%) | 20.68 -> 18.97 us (-8.2%) |
   | Repeated unsafe | 525.48 -> 523.48 us (-0.4%) | 530.93 -> 527.66 us 
(-0.6%) |
   
   The strongest result is the reduction in compiler input and build time for 
repeated
   safe expressions. Unique expressions pay a small linear hash-consing cost, 
while unsafe
   expressions preserve their original IR.
   
   Before I push the follow-up, I would appreciate feedback on these boundaries:
   
   1. Is built-in-only reuse an acceptable initial safety contract?
   2. Is same-basic-block Dex reuse sufficiently conservative for the first 
version?
   3. Should AST interning and execution-value reuse be represented as separate 
concepts?
   4. Would explicit function effect/CSE-safety metadata be the preferred way 
to consider
      arena-allocating deterministic functions such as `lower` and `upper` 
later?
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to