This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new c29075a7e10 [opt](exprs) Remove unreachable mixed-width decimal 
registrations for add/subtract/mod (#66617)
c29075a7e10 is described below

commit c29075a7e10069705617289c22bfccf5b78bf32e
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Aug 11 22:51:37 2026 +0800

    [opt](exprs) Remove unreachable mixed-width decimal registrations for 
add/subtract/mod (#66617)
    
    > Split out of **https://github.com/apache/doris/pull/66510**. That PR
    carries the
    > whole BE build-time batch and its end-to-end measurements — **please
    refer to
    > #66510 for the complete benefit numbers**. This is the only piece of
    that batch
    > that touches the **function registration surface**, so it is broken
    out here to
    > get a proper review from the expression owners. It has no dependency
    on the
    > rest of the batch.
    
    ### What problem does this PR solve?
    
    Related PR: #66510
    
    Problem Summary:
    
    The mixed-width decimal registrations for `add` / `subtract` / `mod`
    cannot be
    reached at runtime, and each one costs a full set of template
    instantiations in
    some of the heaviest TUs in `be/src/exprs/function/`.
    
    FE casts *both* children of a decimal `Add` / `Subtract` / `Mod` to
    exactly the
    return type (`TypeCoercionUtils#processDecimalV3BinaryArithmetic`, since
    2.0 via
    #17393), so only same-width pairs can ever reach BE.
    
    For **add** and **subtract** the registrations were not merely
    unreachable, they
    were already broken: since #52837 the impl carried `PTypeB = TypeA`,
    which made
    every mixed-width variant register under the *same-width* factory key,
    the last
    one overwriting the diagonal. Mixed-width add/subtract has therefore
    been
    unresolvable on 4.0 / 4.1 / master for over a year with zero field
    reports.
    
    That key collapse also had a runtime consequence worth calling out: the
    entries
    that survived on the diagonal were the `<Type, DECIMAL256>` variants,
    whose
    plain (non-overflow-checked) path promoted `Decimal32/64/128` arithmetic
    to
    `Int256`. Collapsing the impl to a single type parameter puts same-width
    inputs
    back on their natural width.
    
    For **mod** there was no such typo — its 16 decimal combinations were
    all live in
    the factory — so this is a genuine narrowing of the registered signature
    set.
    It is still safe across the supported upgrade window (old FE + new BE):
    
    - Nereids has cast both children of `Mod` to exactly the return type
    since 2.0
      (#17393; briefly removed and restored within two days in May 2023, a
      master-only window);
    - the legacy planner's `ArithmeticExpr#analyzeDecimalV3Op` cast both
    children
      unconditionally for `MOD` (only `ADD`/`SUBTRACT` had the
    scale-only-comparison hole), and its builtin table only ever registered
      same-width decimal `MOD`;
    - supported upgrade sources for master (4.0 / 4.1) are Nereids-only —
    the legacy
      expression analyzer no longer exists there.
    
    A mixed-width lookup now fails loudly with function name, argument types
    and
    return type instead of silently resolving to the wrong instantiation.
    `Multiply` is exempt from the FE cast and keeps its full width cross
    product.
    
    ### Why this is a build-time win
    
    Dropping mixed-width removes roughly two thirds of the decimal
    instantiations of
    these TUs — a whole template family per width pair, not a handful of
    functions.
    
    Measured with the real build's own compile commands (Release, PCH on,
    the exact
    flags `build.sh --be` produces), recompiling each TU serialized and
    uncontended,
    alternating base/PR across three rounds. `multiply.cpp` is the control:
    this PR
    deliberately does not touch it, so it must not move.
    
    | TU | wall before | wall after | delta | peak RSS before | after |
    delta |
    |---|---:|---:|---:|---:|---:|---:|
    | `plus.cpp` | 10.19s | 6.06s | **-40.5%** | 1518M | 1159M | **-23.7%**
    |
    | `minus.cpp` | 9.94s | 5.89s | **-40.7%** | 1523M | 1162M | **-23.7%**
    |
    | `modulo.cpp` | 8.46s | 5.89s | **-30.4%** | 1394M | 1151M | **-17.4%**
    |
    | `multiply.cpp` *(control)* | 15.95s | 15.88s | -0.4% | 1959M | 1961M |
    +0.1% |
    
    Run-to-run spread is tight (`plus.cpp` base 9.93 / 10.19 / 10.24s, after
    6.01 /
    6.06 / 6.11s) and the control is flat, so these deltas are the change
    rather than
    scheduling noise. The ~350MB drop in peak RSS per TU is worth as much as
    the wall
    time if you build at high `-j`.
    
    **What this does not claim.** These are 3 TUs out of 8382 in a cold BE
    build, and
    35.5s out of 7237s of total TU CPU. The ~11s of CPU saved is about
    **0.15% of a
    full build** — real, but well below what a single end-to-end run can
    resolve, so
    I am deliberately not quoting an end-to-end percentage for it. The value
    of this
    PR is the per-TU cost of these three files, the peak-memory drop, the
    removal of
    code that cannot be reached, and the contract test that keeps it gone.
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test
        - [x] Regression test — the 56 decimal regression suites match the
          pre-change baseline suite for suite (the only failure is a known,
    unrelated S3-credential `outfile` case that also fails on the baseline).
        - [x] Unit Test — new `BinaryArithmeticRegistrationTest`
    (`be/test/exprs/function/binary_arithmetic_registration_test.cpp`, 5/5
    passing) pins the registration surface: same-width add/subtract/mod
    lookups must resolve, mixed-width ones must return `nullptr` (there is
    no
    bare-name fallback), multiply's 4x4 cross product must stay, and
    DecimalV2
    stays resolvable. If someone re-adds mixed-width registrations — paying
    ~2/3 of those TUs' instantiations for unreachable code — or drops a
          reachable signature, this fails fast.
        - [x] Manual test — microbenchmark A/B, below.
    
    #### Runtime A/B: no regression on any case
    
    The first commit adds `benchmark_binary_arithmetic` (13 cases through
    the real
    `SimpleFunctionFactory` dispatch: multiply int64/same-width/mixed-width/
    vector_constant/constant_constant, plus add and subtract in int64
    vec_vec,
    same-width DECIMAL32 and DECIMAL64 vec_vec, vector_constant and one-row
    constant_constant). Two `benchmark_test` binaries were built from one
    tree
    differing only in `be/src/exprs/function/`, then run alternately so both
    share
    thermal and scheduling conditions. Figures are the better of two rounds'
    5-repetition medians, CPU ns/iteration, 4096-row blocks, macOS arm64 /
    clang 20
    / Release.
    
    | case | before | after | delta |
    |---|---:|---:|---:|
    | `add_d64_d64_const_const` | 98.1 | 98.9 | +0.8% |
    | `subtract_d64_d64_const_const` | 97.8 | 96.7 | -1.2% |
    | `multiply_d64_d64_const_const` | 107.8 | 107.5 | -0.3% |
    | `add_d64_d64_vec_vec` | 1578.5 | 1551.0 | -1.7% |
    | `add_d32_d32_vec_vec` | 1487.8 | 1474.4 | -0.9% |
    | `subtract_d64_d64_vec_vec` | 1558.9 | 1537.8 | -1.4% |
    | `add_d64_d64_vec_const` | 5877.4 | 5770.7 | -1.8% |
    | `add_int64_vec_vec` | 565.1 | 545.2 | -3.5% |
    | `subtract_int64_vec_vec` | 568.2 | 550.3 | -3.1% |
    | `multiply_d64_d64_vec_vec` | 14816.0 | 14806.1 | -0.1% |
    | `multiply_d32_d64_vec_vec` | 14746.9 | 14705.4 | -0.3% |
    | `multiply_d64_d64_vec_const` | 14673.3 | 14641.3 | -0.2% |
    | `multiply_int64_vec_vec` | 1211.6 | 1189.2 | -1.8% |
    
    Every case is inside the +-3.5% round-to-round noise band; nothing
    regresses.
    
    - Behavior changed:
        - [x] Yes.
    1. A **mixed-width** decimal `add`/`subtract`/`mod` lookup now fails
    loudly (`Could not find function ...` with argument and return types)
    instead of resolving. For add/subtract nothing changes in practice —
               those keys were already unreachable. For `mod` this is a real
               narrowing, argued safe above; FE never emits such a call.
    2. On the **plain, non-overflow-checked** branch, same-width decimal
    `add`/`subtract` now compute at their natural width instead of being
    promoted to `Int256`. Results are unchanged; the intermediate width
               is not.
    
    - Does this need documentation?
        - [x] No.
    
    ### Proactive disclosure
    
    - **The claimed narrow-width restoration is not visible in the A/B, by
      construction.** Same-width decimal add/subtract come out flat. The
    `<Type, DECIMAL256>` promotion sits on the *plain*, non-overflow-checked
      branch of the kernel, and the benchmark runner pins
    `check_overflow_for_decimal` to the production default (`true`), so it
    never
    executes that branch. The restoration is a code-level fact you can read
    in the
    diff; it is not something this A/B measured, and I would rather say so
    than
      let the table imply otherwise.
    - **`mod` is the only place where a signature genuinely disappears.**
    add and
    subtract were already unresolvable. If a reviewer disagrees with the mod
    argument, that is the single hunk to contest, and the contract UT is
    what
      would need updating.
    - Runtime numbers were measured on macOS arm64 / clang 20 only, on a
    binary that
    links system malloc rather than tcmalloc (an arm64 branch-range
    workaround).
    Nothing here is platform-specific — it is registration and template code
    — but
      the numbers are single-platform.
    - A companion change that removed the hand-written `constant_constant`
    paths was
    originally part of this PR and has been **taken back out**: isolating it
    showed
    it carried a reproducible +60-70% per-call regression on the
    constant-folding
    path, while this registration change measured flat. It will be proposed
      separately, on its own evidence.
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 be/benchmark/benchmark_binary_arithmetic.hpp       | 346 +++++++++++++++++++++
 be/benchmark/benchmark_main.cpp                    |   1 +
 be/src/exprs/function/minus.cpp                    |  65 ++--
 be/src/exprs/function/modulo.cpp                   |  74 ++---
 be/src/exprs/function/plus.cpp                     |  65 ++--
 .../binary_arithmetic_registration_test.cpp        | 116 +++++++
 6 files changed, 525 insertions(+), 142 deletions(-)

diff --git a/be/benchmark/benchmark_binary_arithmetic.hpp 
b/be/benchmark/benchmark_binary_arithmetic.hpp
new file mode 100644
index 00000000000..6a3a0156bc2
--- /dev/null
+++ b/be/benchmark/benchmark_binary_arithmetic.hpp
@@ -0,0 +1,346 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <benchmark/benchmark.h>
+
+#include <cstdint>
+#include <random>
+#include <stdexcept>
+#include <string>
+
+#include "core/block/block.h"
+#include "core/column/column_const.h"
+#include "core/column/column_decimal.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_decimal.h"
+#include "core/data_type/data_type_number.h"
+#include "exprs/function/simple_function_factory.h"
+#include "exprs/function_context.h"
+
+namespace doris {
+namespace {
+
+// Runtime guardrails for the binary-arithmetic template refactors
+// (compile-opt Phase 2/4/5): every case goes through the real
+// SimpleFunctionFactory dispatch, so kernel swaps and dead-registration
+// removals are covered end to end. check_overflow_for_decimal is pinned to
+// the production default (true). Data is deterministic and sized so the
+// overflow check never actually throws.
+
+constexpr size_t kBinaryArithmeticRows = 4096;
+
+// Drives one binary arithmetic function on a prepared two-column block.
+// Constructed outside the timed loop; run_once is the measured unit.
+struct BinaryArithmeticRunner {
+    Block block;
+    FunctionBasePtr func;
+    std::unique_ptr<FunctionContext> ctx;
+    uint32_t result_idx;
+
+    BinaryArithmeticRunner(const std::string& name, ColumnPtr col_a, 
DataTypePtr type_a,
+                           ColumnPtr col_b, DataTypePtr type_b, DataTypePtr 
res_type) {
+        block.insert({std::move(col_a), type_a, "a"});
+        block.insert({std::move(col_b), type_b, "b"});
+        func = SimpleFunctionFactory::instance().get_function(
+                name, block.get_columns_with_type_and_name(), res_type);
+        if (func == nullptr) {
+            throw std::runtime_error(name + " not found for benchmark argument 
types");
+        }
+        ctx = FunctionContext::create_context(nullptr, res_type, {type_a, 
type_b});
+        ctx->set_check_overflow_for_decimal(true);
+        if (!func->open(ctx.get(), FunctionContext::FRAGMENT_LOCAL).ok() ||
+            !func->open(ctx.get(), FunctionContext::THREAD_LOCAL).ok()) {
+            throw std::runtime_error(name + " open failed");
+        }
+        block.insert({nullptr, res_type, "result"});
+        result_idx = block.columns() - 1;
+    }
+
+    void run_once(size_t rows) {
+        Status st = func->execute(ctx.get(), block, {0, 1}, result_idx, rows);
+        if (!st.ok()) {
+            throw std::runtime_error(st.to_string());
+        }
+        benchmark::DoNotOptimize(block.get_by_position(result_idx).column);
+    }
+};
+
+template <PrimitiveType PT>
+ColumnPtr make_decimal_bench_column(size_t rows, UInt32 scale, int64_t 
native_lo, int64_t native_hi,
+                                    uint64_t seed) {
+    auto col = ColumnDecimal<PT>::create(rows, scale);
+    std::mt19937_64 rng(seed);
+    std::uniform_int_distribution<int64_t> dist(native_lo, native_hi - 1);
+    auto& data = col->get_data();
+    for (size_t i = 0; i < rows; ++i) {
+        data[i] = typename ColumnDecimal<PT>::value_type(dist(rng));
+    }
+    return col;
+}
+
+ColumnPtr make_int64_bench_column(size_t rows, uint64_t seed) {
+    auto col = ColumnVector<TYPE_BIGINT>::create(rows);
+    std::mt19937_64 rng(seed);
+    std::uniform_int_distribution<int64_t> dist(1, 999'999);
+    auto& data = col->get_data();
+    for (size_t i = 0; i < rows; ++i) {
+        data[i] = dist(rng);
+    }
+    return col;
+}
+
+// BIGINT * BIGINT -> BIGINT, vector_vector.
+void BM_multiply_int64_vec_vec(benchmark::State& state) {
+    auto type = std::make_shared<DataTypeInt64>();
+    BinaryArithmeticRunner runner(
+            "multiply", make_int64_bench_column(kBinaryArithmeticRows, 
0x1001), type,
+            make_int64_bench_column(kBinaryArithmeticRows, 0x1002), type, 
type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+// DECIMAL64(18,4) * DECIMAL64(18,4) -> DECIMAL128(36,8): the same-width kernel
+// that survives every phase of the refactor. Native values stay < 1e9 so the
+// int128 product never trips the overflow check, and 4+4 == 8 means no scale
+// adjustment (the common FE-planned shape).
+void BM_multiply_d64_d64_vec_vec(benchmark::State& state) {
+    auto type_a = std::make_shared<DataTypeDecimal64>(18, 4);
+    auto res_type = std::make_shared<DataTypeDecimal128>(36, 8);
+    BinaryArithmeticRunner runner("multiply",
+                                  make_decimal_bench_column<TYPE_DECIMAL64>(
+                                          kBinaryArithmeticRows, 4, 10'000, 
999'999'999, 0x2001),
+                                  type_a,
+                                  make_decimal_bench_column<TYPE_DECIMAL64>(
+                                          kBinaryArithmeticRows, 4, 10'000, 
999'999'999, 0x2002),
+                                  type_a, res_type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+// DECIMAL32(9,2) * DECIMAL64(18,4) -> DECIMAL128(27,6): the mixed-width shape
+// Phase 5 wants to eliminate; its rows/s before/after is the A/B material for
+// the FE cast-to-same-width decision.
+void BM_multiply_d32_d64_vec_vec(benchmark::State& state) {
+    auto type_a = std::make_shared<DataTypeDecimal32>(9, 2);
+    auto type_b = std::make_shared<DataTypeDecimal64>(18, 4);
+    auto res_type = std::make_shared<DataTypeDecimal128>(27, 6);
+    BinaryArithmeticRunner runner("multiply",
+                                  make_decimal_bench_column<TYPE_DECIMAL32>(
+                                          kBinaryArithmeticRows, 2, 100, 
9'999'999, 0x3001),
+                                  type_a,
+                                  make_decimal_bench_column<TYPE_DECIMAL64>(
+                                          kBinaryArithmeticRows, 4, 10'000, 
999'999'999, 0x3002),
+                                  type_b, res_type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+// DECIMAL64 column * DECIMAL64 constant: the vector_constant fast path.
+void BM_multiply_d64_d64_vec_const(benchmark::State& state) {
+    auto type_a = std::make_shared<DataTypeDecimal64>(18, 4);
+    auto res_type = std::make_shared<DataTypeDecimal128>(36, 8);
+    auto const_col = ColumnConst::create(
+            make_decimal_bench_column<TYPE_DECIMAL64>(1, 4, 12'345, 12'346, 
0x4002),
+            kBinaryArithmeticRows);
+    BinaryArithmeticRunner runner("multiply",
+                                  make_decimal_bench_column<TYPE_DECIMAL64>(
+                                          kBinaryArithmeticRows, 4, 10'000, 
999'999'999, 0x4001),
+                                  type_a, std::move(const_col), type_a, 
res_type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+// DECIMAL64 constant * DECIMAL64 constant, one row: measures per-call cost of
+// the constant_constant path Phase 2b removes in favor of the default
+// unwrap-execute-rewrap implementation. Items == calls, not rows.
+void BM_multiply_d64_d64_const_const(benchmark::State& state) {
+    auto type_a = std::make_shared<DataTypeDecimal64>(18, 4);
+    auto res_type = std::make_shared<DataTypeDecimal128>(36, 8);
+    auto const_a = ColumnConst::create(
+            make_decimal_bench_column<TYPE_DECIMAL64>(1, 4, 54'321, 54'322, 
0x5001), 1);
+    auto const_b = ColumnConst::create(
+            make_decimal_bench_column<TYPE_DECIMAL64>(1, 4, 12'345, 12'346, 
0x5002), 1);
+    BinaryArithmeticRunner runner("multiply", std::move(const_a), type_a, 
std::move(const_b),
+                                  type_a, res_type);
+    for (auto _ : state) {
+        runner.run_once(1);
+    }
+    state.SetItemsProcessed(state.iterations());
+}
+
+// --- add / subtract -------------------------------------------------------
+//
+// FE casts both children of a decimal Add/Subtract to exactly the return type,
+// so - unlike multiply - only the same-width shapes below are reachable, and
+// they are the ones the mixed-width registration removal rewrites.
+//
+// Before that removal the same-width factory keys were won by the
+// <Type, DECIMAL256> variants; collapsing the impl to a single type parameter
+// puts them back on their natural width. Do not expect these cases to show
+// that: the runner pins check_overflow_for_decimal to the production default
+// (true), and the Int256 promotion sat on the plain, non-overflow-checked
+// branch of the same kernel. What they do pin is that rewriting the
+// registration surface costs the reachable shapes nothing.
+
+// BIGINT +/- BIGINT -> BIGINT, vector_vector: the integral kernel, untouched 
by
+// the decimal registration change and therefore the control case.
+void BM_add_int64_vec_vec(benchmark::State& state) {
+    auto type = std::make_shared<DataTypeInt64>();
+    BinaryArithmeticRunner runner("add", 
make_int64_bench_column(kBinaryArithmeticRows, 0x6001),
+                                  type, 
make_int64_bench_column(kBinaryArithmeticRows, 0x6002),
+                                  type, type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+void BM_subtract_int64_vec_vec(benchmark::State& state) {
+    auto type = std::make_shared<DataTypeInt64>();
+    BinaryArithmeticRunner runner(
+            "subtract", make_int64_bench_column(kBinaryArithmeticRows, 
0x7001), type,
+            make_int64_bench_column(kBinaryArithmeticRows, 0x7002), type, 
type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+// DECIMAL32(9,2) + DECIMAL32(9,2) -> DECIMAL32(9,2): the narrowest same-width
+// shape, i.e. the one that paid the largest promotion penalty when the
+// DECIMAL256 variant owned this key. Native values stay well under 1e9 so the
+// sum never trips the overflow check.
+void BM_add_d32_d32_vec_vec(benchmark::State& state) {
+    auto type = std::make_shared<DataTypeDecimal32>(9, 2);
+    BinaryArithmeticRunner runner("add",
+                                  make_decimal_bench_column<TYPE_DECIMAL32>(
+                                          kBinaryArithmeticRows, 2, 100, 
9'999'999, 0x8001),
+                                  type,
+                                  make_decimal_bench_column<TYPE_DECIMAL32>(
+                                          kBinaryArithmeticRows, 2, 100, 
9'999'999, 0x8002),
+                                  type, type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+// DECIMAL64(18,4) +/- DECIMAL64(18,4) -> DECIMAL64(18,4): the same-width shape
+// FE actually plans for decimal add/subtract.
+void BM_add_d64_d64_vec_vec(benchmark::State& state) {
+    auto type = std::make_shared<DataTypeDecimal64>(18, 4);
+    BinaryArithmeticRunner runner("add",
+                                  make_decimal_bench_column<TYPE_DECIMAL64>(
+                                          kBinaryArithmeticRows, 4, 10'000, 
999'999'999, 0x9001),
+                                  type,
+                                  make_decimal_bench_column<TYPE_DECIMAL64>(
+                                          kBinaryArithmeticRows, 4, 10'000, 
999'999'999, 0x9002),
+                                  type, type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+void BM_subtract_d64_d64_vec_vec(benchmark::State& state) {
+    auto type = std::make_shared<DataTypeDecimal64>(18, 4);
+    BinaryArithmeticRunner runner("subtract",
+                                  make_decimal_bench_column<TYPE_DECIMAL64>(
+                                          kBinaryArithmeticRows, 4, 10'000, 
999'999'999, 0xa001),
+                                  type,
+                                  make_decimal_bench_column<TYPE_DECIMAL64>(
+                                          kBinaryArithmeticRows, 4, 10'000, 
999'999'999, 0xa002),
+                                  type, type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+// DECIMAL64 column + DECIMAL64 constant: the vector_constant fast path.
+void BM_add_d64_d64_vec_const(benchmark::State& state) {
+    auto type = std::make_shared<DataTypeDecimal64>(18, 4);
+    auto const_col = ColumnConst::create(
+            make_decimal_bench_column<TYPE_DECIMAL64>(1, 4, 12'345, 12'346, 
0xb002),
+            kBinaryArithmeticRows);
+    BinaryArithmeticRunner runner("add",
+                                  make_decimal_bench_column<TYPE_DECIMAL64>(
+                                          kBinaryArithmeticRows, 4, 10'000, 
999'999'999, 0xb001),
+                                  type, std::move(const_col), type, type);
+    for (auto _ : state) {
+        runner.run_once(kBinaryArithmeticRows);
+    }
+    state.SetItemsProcessed(state.iterations() * kBinaryArithmeticRows);
+}
+
+// Constant +/- constant, one row: per-call cost of the path that loses its
+// hand-written constant_constant specialization in favour of the default
+// unwrap-execute-rewrap implementation. Items == calls, not rows.
+void BM_add_d64_d64_const_const(benchmark::State& state) {
+    auto type = std::make_shared<DataTypeDecimal64>(18, 4);
+    auto const_a = ColumnConst::create(
+            make_decimal_bench_column<TYPE_DECIMAL64>(1, 4, 54'321, 54'322, 
0xc001), 1);
+    auto const_b = ColumnConst::create(
+            make_decimal_bench_column<TYPE_DECIMAL64>(1, 4, 12'345, 12'346, 
0xc002), 1);
+    BinaryArithmeticRunner runner("add", std::move(const_a), type, 
std::move(const_b), type, type);
+    for (auto _ : state) {
+        runner.run_once(1);
+    }
+    state.SetItemsProcessed(state.iterations());
+}
+
+void BM_subtract_d64_d64_const_const(benchmark::State& state) {
+    auto type = std::make_shared<DataTypeDecimal64>(18, 4);
+    auto const_a = ColumnConst::create(
+            make_decimal_bench_column<TYPE_DECIMAL64>(1, 4, 54'321, 54'322, 
0xd001), 1);
+    auto const_b = ColumnConst::create(
+            make_decimal_bench_column<TYPE_DECIMAL64>(1, 4, 12'345, 12'346, 
0xd002), 1);
+    BinaryArithmeticRunner runner("subtract", std::move(const_a), type, 
std::move(const_b), type,
+                                  type);
+    for (auto _ : state) {
+        runner.run_once(1);
+    }
+    state.SetItemsProcessed(state.iterations());
+}
+
+BENCHMARK(BM_multiply_int64_vec_vec);
+BENCHMARK(BM_multiply_d64_d64_vec_vec);
+BENCHMARK(BM_multiply_d32_d64_vec_vec);
+BENCHMARK(BM_multiply_d64_d64_vec_const);
+BENCHMARK(BM_multiply_d64_d64_const_const);
+
+BENCHMARK(BM_add_int64_vec_vec);
+BENCHMARK(BM_subtract_int64_vec_vec);
+BENCHMARK(BM_add_d32_d32_vec_vec);
+BENCHMARK(BM_add_d64_d64_vec_vec);
+BENCHMARK(BM_subtract_d64_d64_vec_vec);
+BENCHMARK(BM_add_d64_d64_vec_const);
+BENCHMARK(BM_add_d64_d64_const_const);
+BENCHMARK(BM_subtract_d64_d64_const_const);
+
+} // namespace
+} // namespace doris
diff --git a/be/benchmark/benchmark_main.cpp b/be/benchmark/benchmark_main.cpp
index 7cce267d01e..aed5c6eb84e 100644
--- a/be/benchmark/benchmark_main.cpp
+++ b/be/benchmark/benchmark_main.cpp
@@ -23,6 +23,7 @@
 #include <vector>
 
 #include "benchmark_arrow_validation.hpp"
+#include "benchmark_binary_arithmetic.hpp"
 #include "benchmark_bit_pack.hpp"
 #include "benchmark_column_array_view.hpp"
 #include "benchmark_column_array_view_distance.hpp"
diff --git a/be/src/exprs/function/minus.cpp b/be/src/exprs/function/minus.cpp
index a7f911f6984..9635c39951f 100644
--- a/be/src/exprs/function/minus.cpp
+++ b/be/src/exprs/function/minus.cpp
@@ -22,18 +22,20 @@
 #include "exprs/function/simple_function_factory.h"
 
 namespace doris {
-template <PrimitiveType TypeA, PrimitiveType TypeB>
+// FE casts both children of decimal subtract to exactly the same type as the
+// return type (TypeCoercionUtils#processDecimalV3BinaryArithmetic), so only
+// same-width pairs are reachable at runtime. Keep a single type parameter so
+// mixed-width instantiations cannot be registered again.
+template <PrimitiveType Type>
 struct MinusDecimalImpl {
-    static_assert(is_decimal(TypeA) && is_decimal(TypeB));
-    static_assert((TypeA == TYPE_DECIMALV2 && TypeB == TYPE_DECIMALV2) ||
-                  (TypeA != TYPE_DECIMALV2 && TypeB != TYPE_DECIMALV2));
+    static_assert(is_decimal(Type));
 
     constexpr static bool need_replace_null_data_to_default = true;
     static constexpr auto name = "subtract";
-    static constexpr PrimitiveType PTypeA = TypeA;
-    static constexpr PrimitiveType PTypeB = TypeA;
-    using ArgNativeTypeA = typename 
PrimitiveTypeTraits<TypeA>::CppType::NativeType;
-    using ArgNativeTypeB = typename 
PrimitiveTypeTraits<TypeB>::CppType::NativeType;
+    static constexpr PrimitiveType PTypeA = Type;
+    static constexpr PrimitiveType PTypeB = Type;
+    using ArgNativeTypeA = typename 
PrimitiveTypeTraits<Type>::CppType::NativeType;
+    using ArgNativeTypeB = typename 
PrimitiveTypeTraits<Type>::CppType::NativeType;
 
     template <PrimitiveType Result>
         requires(is_decimal(Result) && Result != TYPE_DECIMALV2)
@@ -67,44 +69,17 @@ struct MinusImpl {
 };
 
 void register_function_minus(SimpleFunctionFactory& factory) {
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMALV2, 
TYPE_DECIMALV2>>>>();
-
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL256>>>>();
-
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL256>>>>();
-
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL256>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMALV2>>>>();
 
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL256>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL32>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL64>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL128I>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<MinusDecimalImpl<TYPE_DECIMAL256>>>>();
 
     
factory.register_function<FunctionPlusMinus<PlusMinusIntegralImpl<MinusImpl<TYPE_TINYINT>>>>();
     
factory.register_function<FunctionPlusMinus<PlusMinusIntegralImpl<MinusImpl<TYPE_SMALLINT>>>>();
diff --git a/be/src/exprs/function/modulo.cpp b/be/src/exprs/function/modulo.cpp
index 8942475e75b..78ebf9afde8 100644
--- a/be/src/exprs/function/modulo.cpp
+++ b/be/src/exprs/function/modulo.cpp
@@ -558,25 +558,27 @@ struct PModuloNumericImpl {
     }
 };
 
-template <PrimitiveType TypeA, PrimitiveType TypeB>
+// FE casts both children of decimal mod to exactly the same type as the return
+// type (TypeCoercionUtils#processDecimalV3BinaryArithmetic), so only 
same-width
+// pairs are reachable at runtime. Keep a single type parameter so mixed-width
+// instantiations cannot be registered again.
+template <PrimitiveType Type>
 struct ModuloDecimalImpl {
-    static_assert(is_decimal(TypeA) && is_decimal(TypeB));
-    static_assert((TypeA == TYPE_DECIMALV2 && TypeB == TYPE_DECIMALV2) ||
-                  (TypeA != TYPE_DECIMALV2 && TypeB != TYPE_DECIMALV2));
+    static_assert(is_decimal(Type));
     static constexpr auto name = "mod";
     static constexpr auto is_pmod = false;
-    using ArgA = typename PrimitiveTypeTraits<TypeA>::CppType;
-    using ArgB = typename PrimitiveTypeTraits<TypeB>::CppType;
-    using ArgNativeTypeA = typename 
PrimitiveTypeTraits<TypeA>::CppType::NativeType;
-    using ArgNativeTypeB = typename 
PrimitiveTypeTraits<TypeB>::CppType::NativeType;
-    using DataTypeA = typename PrimitiveTypeTraits<TypeA>::DataType;
-    using DataTypeB = typename PrimitiveTypeTraits<TypeB>::DataType;
-    using ColumnTypeA = typename PrimitiveTypeTraits<TypeA>::ColumnType;
-    using ColumnTypeB = typename PrimitiveTypeTraits<TypeB>::ColumnType;
+    using ArgA = typename PrimitiveTypeTraits<Type>::CppType;
+    using ArgB = typename PrimitiveTypeTraits<Type>::CppType;
+    using ArgNativeTypeA = typename 
PrimitiveTypeTraits<Type>::CppType::NativeType;
+    using ArgNativeTypeB = typename 
PrimitiveTypeTraits<Type>::CppType::NativeType;
+    using DataTypeA = typename PrimitiveTypeTraits<Type>::DataType;
+    using DataTypeB = typename PrimitiveTypeTraits<Type>::DataType;
+    using ColumnTypeA = typename PrimitiveTypeTraits<Type>::ColumnType;
+    using ColumnTypeB = typename PrimitiveTypeTraits<Type>::ColumnType;
 
     static DataTypes get_variadic_argument_types() {
-        return {std::make_shared<typename 
PrimitiveTypeTraits<TypeA>::DataType>(),
-                std::make_shared<typename 
PrimitiveTypeTraits<TypeB>::DataType>()};
+        return {std::make_shared<typename 
PrimitiveTypeTraits<Type>::DataType>(),
+                std::make_shared<typename 
PrimitiveTypeTraits<Type>::DataType>()};
     }
 
     static inline DecimalV2Value apply(DecimalV2Value a, DecimalV2Value b, 
UInt8& is_null) {
@@ -935,44 +937,12 @@ void register_function_modulo(SimpleFunctionFactory& 
factory) {
     
factory.register_function<FunctionMod<ModNumericImpl<PModuloNumericImpl<TYPE_BIGINT>>>>();
     
factory.register_function<FunctionMod<ModNumericImpl<PModuloNumericImpl<TYPE_DOUBLE>>>>();
 
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMALV2, 
TYPE_DECIMALV2>>>>();
-
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL256>>>>();
-
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL256>>>>();
-
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL256>>>>();
-
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<
-            FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL256>>>>();
+    
factory.register_function<FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMALV2>>>>();
+
+    
factory.register_function<FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL32>>>>();
+    
factory.register_function<FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL64>>>>();
+    
factory.register_function<FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL128I>>>>();
+    
factory.register_function<FunctionMod<ModDecimalImpl<ModuloDecimalImpl<TYPE_DECIMAL256>>>>();
     factory.register_alias("mod", "fmod");
 }
 
diff --git a/be/src/exprs/function/plus.cpp b/be/src/exprs/function/plus.cpp
index a67812eab15..a87353e0009 100644
--- a/be/src/exprs/function/plus.cpp
+++ b/be/src/exprs/function/plus.cpp
@@ -30,19 +30,21 @@ struct PlusImpl {
     NO_SANITIZE_UNDEFINED static inline Arg apply(Arg a, Arg b) { return a + 
b; }
 };
 
-template <PrimitiveType TypeA, PrimitiveType TypeB>
+// FE casts both children of decimal add to exactly the same type as the return
+// type (TypeCoercionUtils#processDecimalV3BinaryArithmetic), so only 
same-width
+// pairs are reachable at runtime. Keep a single type parameter so mixed-width
+// instantiations cannot be registered again.
+template <PrimitiveType Type>
 struct PlusDecimalImpl {
-    static_assert(is_decimal(TypeA) && is_decimal(TypeB));
-    static_assert((TypeA == TYPE_DECIMALV2 && TypeB == TYPE_DECIMALV2) ||
-                  (TypeA != TYPE_DECIMALV2 && TypeB != TYPE_DECIMALV2));
+    static_assert(is_decimal(Type));
 
     constexpr static bool need_replace_null_data_to_default = true;
 
     static constexpr auto name = "add";
-    static constexpr PrimitiveType PTypeA = TypeA;
-    static constexpr PrimitiveType PTypeB = TypeA;
-    using ArgNativeTypeA = typename 
PrimitiveTypeTraits<TypeA>::CppType::NativeType;
-    using ArgNativeTypeB = typename 
PrimitiveTypeTraits<TypeB>::CppType::NativeType;
+    static constexpr PrimitiveType PTypeA = Type;
+    static constexpr PrimitiveType PTypeB = Type;
+    using ArgNativeTypeA = typename 
PrimitiveTypeTraits<Type>::CppType::NativeType;
+    using ArgNativeTypeB = typename 
PrimitiveTypeTraits<Type>::CppType::NativeType;
 
     template <PrimitiveType Result>
         requires(is_decimal(Result) && Result != TYPE_DECIMALV2)
@@ -69,44 +71,17 @@ struct PlusDecimalImpl {
 };
 
 void register_function_plus(SimpleFunctionFactory& factory) {
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMALV2, 
TYPE_DECIMALV2>>>>();
-
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL32, 
TYPE_DECIMAL256>>>>();
-
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL64, 
TYPE_DECIMAL256>>>>();
-
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL128I, 
TYPE_DECIMAL256>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMALV2>>>>();
 
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL32>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL64>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL128I>>>>();
-    factory.register_function<FunctionPlusMinus<
-            PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL256, 
TYPE_DECIMAL256>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL32>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL64>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL128I>>>>();
+    factory.register_function<
+            
FunctionPlusMinus<PlusMinusDecimalImpl<PlusDecimalImpl<TYPE_DECIMAL256>>>>();
 
     
factory.register_function<FunctionPlusMinus<PlusMinusIntegralImpl<PlusImpl<TYPE_TINYINT>>>>();
     
factory.register_function<FunctionPlusMinus<PlusMinusIntegralImpl<PlusImpl<TYPE_SMALLINT>>>>();
diff --git a/be/test/exprs/function/binary_arithmetic_registration_test.cpp 
b/be/test/exprs/function/binary_arithmetic_registration_test.cpp
new file mode 100644
index 00000000000..78e53a05579
--- /dev/null
+++ b/be/test/exprs/function/binary_arithmetic_registration_test.cpp
@@ -0,0 +1,116 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "core/data_type/data_type_decimal.h"
+#include "core/data_type/data_type_nullable.h"
+#include "exprs/function/simple_function_factory.h"
+
+namespace doris {
+
+// Pins the decimal registration surface of the binary arithmetic functions.
+//
+// FE casts both children of Add/Subtract/Mod to exactly the return type
+// (TypeCoercionUtils#processDecimalV3BinaryArithmetic), so BE registers only
+// same-width decimal pairs for them. Multiply is exempt from that cast and
+// must keep the full width cross product. If someone re-adds mixed-width
+// add/subtract/mod registrations (paying ~2/3 of the template instantiations
+// of those TUs for unreachable code) or drops a reachable signature, these
+// tests fail.
+class BinaryArithmeticRegistrationTest : public testing::Test {
+protected:
+    static FunctionBasePtr lookup(const std::string& name, const DataTypePtr& 
left,
+                                  const DataTypePtr& right, const DataTypePtr& 
return_type) {
+        ColumnsWithTypeAndName arguments {{nullptr, left, ""}, {nullptr, 
right, ""}};
+        return SimpleFunctionFactory::instance().get_function(name, arguments, 
return_type);
+    }
+
+    static std::vector<DataTypePtr> v3_widths() {
+        return {std::make_shared<DataTypeDecimal<TYPE_DECIMAL32>>(),
+                std::make_shared<DataTypeDecimal<TYPE_DECIMAL64>>(),
+                std::make_shared<DataTypeDecimal<TYPE_DECIMAL128I>>(),
+                std::make_shared<DataTypeDecimal<TYPE_DECIMAL256>>()};
+    }
+};
+
+TEST_F(BinaryArithmeticRegistrationTest, same_width_add_subtract_resolvable) {
+    for (const auto& name : {std::string("add"), std::string("subtract")}) {
+        for (const auto& type : v3_widths()) {
+            auto function = lookup(name, type, type, type);
+            ASSERT_NE(function, nullptr) << name << " " << type->get_name();
+            EXPECT_EQ(function->get_name(), name) << type->get_name();
+        }
+    }
+}
+
+TEST_F(BinaryArithmeticRegistrationTest, same_width_mod_resolvable) {
+    for (const auto& type : v3_widths()) {
+        // FunctionMod always infers a nullable return type (mod by zero -> 
NULL).
+        auto function = lookup("mod", type, type, make_nullable(type));
+        ASSERT_NE(function, nullptr) << type->get_name();
+        EXPECT_EQ(function->get_name(), "mod") << type->get_name();
+    }
+}
+
+TEST_F(BinaryArithmeticRegistrationTest, 
mixed_width_add_subtract_mod_unresolvable) {
+    auto widths = v3_widths();
+    for (const auto& left : widths) {
+        for (const auto& right : widths) {
+            if (left->get_primitive_type() == right->get_primitive_type()) {
+                continue;
+            }
+            for (const auto& name : {std::string("add"), 
std::string("subtract")}) {
+                EXPECT_EQ(lookup(name, left, right, left), nullptr)
+                        << name << " " << left->get_name() << " x " << 
right->get_name();
+            }
+            EXPECT_EQ(lookup("mod", left, right, make_nullable(left)), nullptr)
+                    << "mod " << left->get_name() << " x " << 
right->get_name();
+        }
+    }
+}
+
+TEST_F(BinaryArithmeticRegistrationTest, multiply_full_cross_product_retained) 
{
+    auto widths = v3_widths();
+    for (const auto& left : widths) {
+        for (const auto& right : widths) {
+            auto function = lookup("multiply", left, right, left);
+            ASSERT_NE(function, nullptr)
+                    << "multiply " << left->get_name() << " x " << 
right->get_name();
+            EXPECT_EQ(function->get_name(), "multiply");
+        }
+    }
+}
+
+TEST_F(BinaryArithmeticRegistrationTest, decimalv2_still_resolvable) {
+    auto type = std::make_shared<DataTypeDecimalV2>();
+    for (const auto& name :
+         {std::string("add"), std::string("subtract"), 
std::string("multiply")}) {
+        auto function = lookup(name, type, type, type);
+        ASSERT_NE(function, nullptr) << name;
+        EXPECT_EQ(function->get_name(), name);
+    }
+    auto mod_function = lookup("mod", type, type, make_nullable(type));
+    ASSERT_NE(mod_function, nullptr);
+    EXPECT_EQ(mod_function->get_name(), "mod");
+}
+
+} // namespace doris


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to