This is an automated email from the ASF dual-hosted git repository.
HappenLee 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 2a550292371 [fix](be) Preserve floating point values in CASE branch
selection (#67896)
2a550292371 is described below
commit 2a55029237141b5ed8e2177b262d493f9511089a
Author: HappenLee <[email protected]>
AuthorDate: Mon Sep 14 20:25:04 2026 +0800
[fix](be) Preserve floating point values in CASE branch selection (#67896)
### What problem does this PR solve?
Issue Number: N/A
Related PR: N/A
Problem Summary: A non-nullable FLOAT/DOUBLE CASE can return NaN for a
finite selected branch when an unselected branch contains Infinity or
NaN. For example, with ordinary CASE evaluation, selecting `1`, an
overflowing multiplication, and `2` over three rows returns `NaN,
Infinity, NaN` instead of `1, Infinity, 2`. The same result assembly
also loses the sign of selected negative zero.
The result assembly multiplies each branch value by a zero/one mask and
adds it to the result. IEEE-754 arithmetic makes `0 * Infinity` and `0 *
NaN` equal NaN. Use conditional stores to copy the selected floating
point value without arithmetic. DATE, DATETIME, DATEV2, DATETIMEV2,
TIMESTAMP_NS and TIMESTAMPTZ share this conditional-store loop,
replacing their previous ternary assignments. This form allows AVX2
masked loads/stores, while a ternary assignment can become a conditional
pointer load that inhibits vectorization.
Retain the existing default-value initialization and the arithmetic
accumulator for the other numeric types. Removing the redundant
initialization for the direct-store types is deferred.
Add bitwise unit tests, SQL regression tests and a benchmark that
directly calls the production result assembly function. The typed unit
tests cover all eight types with both uint8_t and uint16_t branch
indices.
### Release note
Fix incorrect FLOAT/DOUBLE CASE results caused by unselected non-finite
branch values, and preserve selected negative zero.
### Check List (For Author)
- Test
- [x] Regression test
- [x] Unit Test
- [x] Manual test (details below)
- [ ] No need to test or manual test.
Validation of the date/time follow-up:
- All 48 `VCaseSelectionTest*` tests pass via `./run-be-ut.sh -j 48
--run --filter='VCaseSelectionTest*'` with ASAN. Coverage includes all
eight types, both index widths, constants, boundary values, vector tails
and 255/257 branches.
- clang-format 16, header hygiene, `git diff --check` and clang-tidy
pass. For clang-tidy, the header was explicitly mapped to the generated
selection-test compiler flags to avoid failed automatic header
inference.
- `./build.sh --be -j 48` with ASAN compiled and linked the BE
successfully. Full build/packaging did not pass: the later
`kuromoji_build_dict` process reported an ASAN double-free during
OpenBLAS/OpenMP initialization (`dlsym` / `_dlerror_run`), before
entering main. The separate standard ASAN unit-test build and execution
passed.
- Default-value initialization and the remaining arithmetic accumulator
are unchanged.
Earlier validation of the floating point fix (before the date/time
follow-up):
- All 12 original floating point unit tests fail on the original
implementation and pass with ASAN after the fix. These cases are
retained in the expanded typed suite. Coverage includes non-finite
values, signed zero and subnormal values.
- `test_case_float_nonfinite` and `test_short_circuit_evaluation` pass.
The original implementation fails the new SQL regression. Golden output
was generated and independently verified through short-circuit
evaluation using the regression runner. These SQL suites were not rerun
for the date/time follow-up.
- ASAN BE build, clang-format 16, header hygiene and clang-tidy checks
pass for the earlier revision.
- RELEASE benchmarks built with Clang 21.1.8 and `-O3 -msse4.2 -mavx2`
on Xeon Platinum 8457C cover 42 scenarios. With identical input, a fixed
CPU, five repetitions per run and before/after/after/before ordering,
median CPU time decreases by approximately 5%–39% (18.5% geometric mean
reduction). This measures floating point result assembly on this AVX2
machine, including result allocation; these figures do not measure the
date/time follow-up.
- Disassembly of all four floating point/index-width specializations in
the linked binaries confirms `vmaskmovps/pd`, replacing floating point
multiply/add instructions without fast-math.
- Behavior changed:
- [ ] No.
- [x] Yes. Return the selected floating point value without
contamination from other branches or loss of negative zero. Date/time
selection preserves the existing results.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/benchmark/benchmark_case_expr.hpp | 93 ++++++++++++++
be/benchmark/benchmark_main.cpp | 1 +
be/src/exprs/vcase_expr.h | 14 ++-
be/test/exprs/vcase_expr_test.cpp | 140 +++++++++++++++++++++
.../test_case_float_nonfinite.out | 81 ++++++++++++
.../test_case_float_nonfinite.groovy | 64 ++++++++++
6 files changed, 389 insertions(+), 4 deletions(-)
diff --git a/be/benchmark/benchmark_case_expr.hpp
b/be/benchmark/benchmark_case_expr.hpp
new file mode 100644
index 00000000000..7bf695267a9
--- /dev/null
+++ b/be/benchmark/benchmark_case_expr.hpp
@@ -0,0 +1,93 @@
+// 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 <random>
+
+#include "core/data_type/data_type_number.h"
+#include "exprs/vcase_expr.h"
+
+namespace doris {
+
+// Keep the production result assembly visible in disassembly as well as
timing it.
+template <typename IndexType, PrimitiveType PT>
+NO_INLINE ColumnPtr run_case_selection(const VCaseExpr& expr, const IndexType*
indices,
+ std::vector<ColumnPtr>& columns, size_t
rows) {
+ return expr._execute_update_result_impl<IndexType,
ColumnVector<PT>>(indices, columns, rows);
+}
+
+template <typename IndexType, PrimitiveType PT>
+void BM_CaseFloatSelection(benchmark::State& state) {
+ const size_t rows = state.range(0);
+ const size_t branches = state.range(1);
+ const auto distribution = state.range(2);
+ TExprNode node;
+ node.__set_node_type(TExprNodeType::CASE_EXPR);
+ node.__set_type(DataTypeNumber<PT>().to_thrift());
+ node.__set_is_nullable(false);
+ node.case_expr.__set_has_else_expr(true);
+ VCaseExpr expr(node);
+ std::mt19937 rng(20260912);
+ std::vector<IndexType> indices(rows);
+ for (size_t row = 0; row < rows; ++row) {
+ // Interleaved, random, or 99% ELSE. All inputs are finite for
before/after comparison.
+ indices[row] = static_cast<IndexType>(distribution == 0 ? row %
branches
+ : distribution == 1
+ ? rng() % branches
+ : (row % 100 == 0 ?
rng() % branches : 0));
+ }
+ std::vector<ColumnPtr> columns;
+ for (size_t branch = 0; branch < branches; ++branch) {
+ auto column = ColumnVector<PT>::create(rows);
+ for (size_t row = 0; row < rows; ++row) {
+ column->get_data()[row] =
+ static_cast<typename ColumnVector<PT>::value_type>((row +
branch + 1) * 0.125);
+ }
+ columns.push_back(std::move(column));
+ }
+ for (auto _ : state) {
+ auto result = run_case_selection<IndexType, PT>(expr, indices.data(),
columns, rows);
+ benchmark::DoNotOptimize(result);
+ }
+ state.SetItemsProcessed(state.iterations() * rows);
+}
+
+inline void case_float_arguments(benchmark::internal::Benchmark* benchmark) {
+ for (int64_t rows : {31, 4096, 65536}) {
+ for (int64_t branches : {3, 16}) {
+ for (int64_t distribution : {0, 1, 2}) {
+ benchmark->Args({rows, branches, distribution});
+ }
+ }
+ }
+}
+
+BENCHMARK_TEMPLATE(BM_CaseFloatSelection, uint8_t,
TYPE_FLOAT)->Apply(case_float_arguments);
+BENCHMARK_TEMPLATE(BM_CaseFloatSelection, uint8_t,
TYPE_DOUBLE)->Apply(case_float_arguments);
+BENCHMARK_TEMPLATE(BM_CaseFloatSelection, uint16_t, TYPE_FLOAT)
+ ->Args({4096, 257, 0})
+ ->Args({4096, 257, 1})
+ ->Args({4096, 257, 2});
+BENCHMARK_TEMPLATE(BM_CaseFloatSelection, uint16_t, TYPE_DOUBLE)
+ ->Args({4096, 257, 0})
+ ->Args({4096, 257, 1})
+ ->Args({4096, 257, 2});
+
+} // namespace doris
diff --git a/be/benchmark/benchmark_main.cpp b/be/benchmark/benchmark_main.cpp
index 8f1ffd7efc8..acbc591effd 100644
--- a/be/benchmark/benchmark_main.cpp
+++ b/be/benchmark/benchmark_main.cpp
@@ -26,6 +26,7 @@
#include "benchmark_arrow_validation.hpp"
#include "benchmark_binary_arithmetic.hpp"
#include "benchmark_bit_pack.hpp"
+#include "benchmark_case_expr.hpp"
#include "benchmark_column_array_view.hpp"
#include "benchmark_column_array_view_distance.hpp"
#include "benchmark_fastunion.hpp"
diff --git a/be/src/exprs/vcase_expr.h b/be/src/exprs/vcase_expr.h
index 5bb2fbdefa3..7c3f27fb7ac 100644
--- a/be/src/exprs/vcase_expr.h
+++ b/be/src/exprs/vcase_expr.h
@@ -242,15 +242,21 @@ private:
then_columns[i].get())
->get_data()
.data();
- if constexpr (std::is_same_v<ColumnType, ColumnDate> ||
+ if constexpr (std::is_same_v<ColumnType, ColumnFloat32> ||
+ std::is_same_v<ColumnType, ColumnFloat64> ||
+ std::is_same_v<ColumnType, ColumnDate> ||
std::is_same_v<ColumnType, ColumnDateTime> ||
std::is_same_v<ColumnType, ColumnDateV2> ||
std::is_same_v<ColumnType, ColumnDateTimeV2> ||
std::is_same_v<ColumnType, ColumnTimeStampNs> ||
std::is_same_v<ColumnType, ColumnTimeStampTz>) {
- for (int row_idx = 0; row_idx < rows_count; row_idx++) {
- result_raw_data[row_idx] = (then_idx[row_idx] == i) ?
column_raw_data[row_idx]
- :
result_raw_data[row_idx];
+ // Arithmetic masking propagates unselected NaN/Infinity and
loses signed zero.
+ // Conditional stores also let the compiler vectorize without
loading from a
+ // selected source/destination pointer, as a ternary
assignment can do.
+ for (size_t row_idx = 0; row_idx < rows_count; row_idx++) {
+ if (then_idx[row_idx] == i) {
+ result_raw_data[row_idx] = column_raw_data[row_idx];
+ }
}
} else {
for (int row_idx = 0; row_idx < rows_count; row_idx++) {
diff --git a/be/test/exprs/vcase_expr_test.cpp
b/be/test/exprs/vcase_expr_test.cpp
new file mode 100644
index 00000000000..0412c0156c7
--- /dev/null
+++ b/be/test/exprs/vcase_expr_test.cpp
@@ -0,0 +1,140 @@
+// 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 "exprs/vcase_expr.h"
+
+#include <gtest/gtest.h>
+
+#include <array>
+#include <bit>
+#include <limits>
+#include <type_traits>
+
+#include "core/data_type/data_type_date.h"
+#include "core/data_type/data_type_date_or_datetime_v2.h"
+#include "core/data_type/data_type_date_time.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_timestamp_ns.h"
+#include "core/data_type/data_type_timestamptz.h"
+#include "core/type_limit.h"
+
+namespace doris {
+
+template <typename Index, PrimitiveType PT>
+struct CaseSelectionTypes {
+ using IndexType = Index;
+ using ColumnType = ColumnVector<PT>;
+ using DataType = typename PrimitiveTypeTraits<PT>::DataType;
+};
+
+using CaseSelectionTestTypes = ::testing::Types<
+ CaseSelectionTypes<uint8_t, TYPE_FLOAT>, CaseSelectionTypes<uint8_t,
TYPE_DOUBLE>,
+ CaseSelectionTypes<uint16_t, TYPE_FLOAT>, CaseSelectionTypes<uint16_t,
TYPE_DOUBLE>,
+ CaseSelectionTypes<uint8_t, TYPE_DATE>, CaseSelectionTypes<uint8_t,
TYPE_DATETIME>,
+ CaseSelectionTypes<uint8_t, TYPE_DATEV2>, CaseSelectionTypes<uint8_t,
TYPE_DATETIMEV2>,
+ CaseSelectionTypes<uint8_t, TYPE_TIMESTAMP_NS>,
+ CaseSelectionTypes<uint8_t, TYPE_TIMESTAMPTZ>,
CaseSelectionTypes<uint16_t, TYPE_DATE>,
+ CaseSelectionTypes<uint16_t, TYPE_DATETIME>,
CaseSelectionTypes<uint16_t, TYPE_DATEV2>,
+ CaseSelectionTypes<uint16_t, TYPE_DATETIMEV2>,
+ CaseSelectionTypes<uint16_t, TYPE_TIMESTAMP_NS>,
+ CaseSelectionTypes<uint16_t, TYPE_TIMESTAMPTZ>>;
+
+template <typename T>
+class VCaseSelectionTest : public ::testing::Test {
+protected:
+ using Index = typename T::IndexType;
+ using Column = typename T::ColumnType;
+ using Value = typename Column::value_type;
+ using Bits = std::conditional_t<sizeof(Value) == 4, uint32_t, uint64_t>;
+
+ void check_selection(size_t rows, size_t branches, bool constant) {
+ SCOPED_TRACE(::testing::Message()
+ << "rows=" << rows << " branches=" << branches << "
constant=" << constant);
+ TExprNode node;
+ node.__set_node_type(TExprNodeType::CASE_EXPR);
+ node.__set_type(typename T::DataType().to_thrift());
+ node.__set_is_nullable(false);
+ node.case_expr.__set_has_else_expr(true);
+ VCaseExpr expr(node);
+ const auto values = [] {
+ if constexpr (std::is_floating_point_v<Value>) {
+ // Arithmetic masking corrupts unselected infinities/NaNs, and
adding to +0 loses -0.
+ return std::array<Value, 9> {Value(1.25),
+ Value(-2.5),
+ Value(0.0),
+ Value(-0.0),
+
std::numeric_limits<Value>::infinity(),
+
-std::numeric_limits<Value>::infinity(),
+
std::numeric_limits<Value>::quiet_NaN(),
+
std::numeric_limits<Value>::denorm_min(),
+
std::numeric_limits<Value>::max()};
+ } else {
+ return std::array<Value, 3> {type_limit<Value>::min(),
Column::default_value(),
+ type_limit<Value>::max()};
+ }
+ }();
+ std::vector<Index> indices(rows);
+ for (size_t row = 0; row < rows; ++row) {
+ indices[row] = row % branches;
+ }
+ std::vector<ColumnPtr> columns;
+ for (size_t branch = 0; branch < branches; ++branch) {
+ auto column = Column::create(constant ? 1 : rows);
+ for (size_t row = 0; row < column->size(); ++row) {
+ column->get_data()[row] = values[(row / branches + branch) %
values.size()];
+ }
+ if (constant) {
+ columns.push_back(ColumnConst::create(std::move(column),
rows));
+ } else {
+ columns.push_back(std::move(column));
+ }
+ }
+ auto result = expr.template _execute_update_result_impl<Index,
Column>(indices.data(),
+
columns, rows);
+ const auto& actual = assert_cast<const Column&>(*result).get_data();
+ ASSERT_EQ(actual.size(), rows);
+ for (size_t row = 0; row < rows; ++row) {
+ const auto expected =
+ values[((constant ? 0 : row / branches) + indices[row]) %
values.size()];
+ // Compare bits to include NaN payloads, signed zero and subnormal
values.
+ ASSERT_EQ(std::bit_cast<Bits>(actual[row]),
std::bit_cast<Bits>(expected)) << row;
+ }
+ }
+};
+
+TYPED_TEST_SUITE(VCaseSelectionTest, CaseSelectionTestTypes);
+
+TYPED_TEST(VCaseSelectionTest, ValuesAndVectorTails) {
+ for (size_t rows : {0, 1, 3, 7, 8, 15, 16, 31, 32, 33, 4095, 4096, 4099}) {
+ this->check_selection(rows, 9, false);
+ }
+}
+
+TYPED_TEST(VCaseSelectionTest, ConstantBranches) {
+ for (size_t rows : {1, 31, 4099}) {
+ this->check_selection(rows, 9, true);
+ }
+}
+
+TYPED_TEST(VCaseSelectionTest, MaximumAndWideBranchIndices) {
+ // 255 columns still use uint8_t; 257 columns exercise indices beyond the
uint8_t range.
+ const size_t branches = sizeof(typename TypeParam::IndexType) == 1 ? 255 :
257;
+ this->check_selection(4099, branches, false);
+ this->check_selection(4099, branches, true);
+}
+
+} // namespace doris
diff --git
a/regression-test/data/query_p0/sql_functions/conditional_functions/test_case_float_nonfinite.out
b/regression-test/data/query_p0/sql_functions/conditional_functions/test_case_float_nonfinite.out
new file mode 100644
index 00000000000..74a2f239b99
--- /dev/null
+++
b/regression-test/data/query_p0/sql_functions/conditional_functions/test_case_float_nonfinite.out
@@ -0,0 +1,81 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !double_false_then --
+0 1 1 1
+1 Infinity -Infinity NaN
+2 2 2 2
+
+-- !double_false_else --
+0 1 1 1
+1 2 2 2
+2 Infinity -Infinity NaN
+
+-- !double_false_nullable --
+0 1
+1 Infinity
+2 \N
+
+-- !double_false_batches --
+1 1
+2 4097
+Infinity 1
+
+-- !float_false_then --
+0 1.0 1.0 1.0
+1 Infinity -Infinity NaN
+2 2.0 2.0 2.0
+
+-- !float_false_else --
+0 1.0 1.0 1.0
+1 2.0 2.0 2.0
+2 Infinity -Infinity NaN
+
+-- !float_false_nullable --
+0 1.0
+1 Infinity
+2 \N
+
+-- !float_false_batches --
+1.0 1
+2.0 4097
+Infinity 1
+
+-- !double_true_then --
+0 1 1 1
+1 Infinity -Infinity NaN
+2 2 2 2
+
+-- !double_true_else --
+0 1 1 1
+1 2 2 2
+2 Infinity -Infinity NaN
+
+-- !double_true_nullable --
+0 1
+1 Infinity
+2 \N
+
+-- !double_true_batches --
+1 1
+2 4097
+Infinity 1
+
+-- !float_true_then --
+0 1.0 1.0 1.0
+1 Infinity -Infinity NaN
+2 2.0 2.0 2.0
+
+-- !float_true_else --
+0 1.0 1.0 1.0
+1 2.0 2.0 2.0
+2 Infinity -Infinity NaN
+
+-- !float_true_nullable --
+0 1.0
+1 Infinity
+2 \N
+
+-- !float_true_batches --
+1.0 1
+2.0 4097
+Infinity 1
+
diff --git
a/regression-test/suites/query_p0/sql_functions/conditional_functions/test_case_float_nonfinite.groovy
b/regression-test/suites/query_p0/sql_functions/conditional_functions/test_case_float_nonfinite.groovy
new file mode 100644
index 00000000000..4df2bac1124
--- /dev/null
+++
b/regression-test/suites/query_p0/sql_functions/conditional_functions/test_case_float_nonfinite.groovy
@@ -0,0 +1,64 @@
+// 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.
+
+suite("test_case_float_nonfinite") {
+ for (def shortCircuit : [false, true]) {
+ sql "set short_circuit_evaluation = ${shortCircuit}"
+ for (def type : ["double", "float"]) {
+ // Every row overflows from finite operands. Keeping number in the
expression
+ // prevents constant folding and makes unselected branches contain
infinity too.
+ def infinity = "cast((cast(number as double) + cast(1e308 as
double)) * cast(1e308 as double) as ${type})"
+ def nan = "cast((${infinity}) - (${infinity}) as ${type})"
+ "qt_${type}_${shortCircuit}_then" """
+ select number,
+ case when number = 0 then cast(1 as ${type})
+ when number = 1 then ${infinity} else cast(2 as
${type}) end,
+ case when number = 0 then cast(1 as ${type})
+ when number = 1 then cast(-(${infinity}) as
${type}) else cast(2 as ${type}) end,
+ case when number = 0 then cast(1 as ${type})
+ when number = 1 then ${nan} else cast(2 as
${type}) end
+ from numbers("number" = "3") order by number
+ """
+ "qt_${type}_${shortCircuit}_else" """
+ select number,
+ case when number = 0 then cast(1 as ${type})
+ when number = 1 then cast(2 as ${type}) else
${infinity} end,
+ case when number = 0 then cast(1 as ${type})
+ when number = 1 then cast(2 as ${type}) else
cast(-(${infinity}) as ${type}) end,
+ case when number = 0 then cast(1 as ${type})
+ when number = 1 then cast(2 as ${type}) else
${nan} end
+ from numbers("number" = "3") order by number
+ """
+ "qt_${type}_${shortCircuit}_nullable" """
+ select number,
+ case when number = 0 then cast(1 as ${type})
+ when number = 1 then ${infinity} end
+ from numbers("number" = "3") order by number
+ """
+ // The overflowing branch is evaluated in the first batch. Finite
rows must
+ // have the same result there and in the tail batch where it is
never selected.
+ "qt_${type}_${shortCircuit}_batches" """
+ select result, count(*) from (
+ select case when number = 0 then cast(1 as ${type})
+ when number = 1 then ${infinity}
+ else cast(2 as ${type}) end as result
+ from numbers("number" = "4099")
+ ) t group by result order by result
+ """
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]