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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 765044e29ca branch-4.1:   [fix](column) avoid mutable nullable crc32c 
hashing #64944 (#65088)
765044e29ca is described below

commit 765044e29ca5622d9b0159c64592b8b74d4775a3
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Jul 2 08:10:48 2026 +0800

    branch-4.1:   [fix](column) avoid mutable nullable crc32c hashing #64944 
(#65088)
    
    Cherry-picked from #64944
    
    Co-authored-by: Mryange <[email protected]>
---
 be/src/core/column/column.h                  |  10 ++
 be/src/core/column/column_decimal.cpp        |  10 ++
 be/src/core/column/column_decimal.h          |   3 +
 be/src/core/column/column_nullable.cpp       |  20 ++-
 be/src/core/column/column_vector.cpp         |  23 ++-
 be/src/core/column/column_vector.h           |   4 +
 be/test/core/column/column_nullable_test.cpp | 222 +++++++++++++++++++++++++++
 7 files changed, 278 insertions(+), 14 deletions(-)

diff --git a/be/src/core/column/column.h b/be/src/core/column/column.h
index 033960ab5e0..9e25b42d0d1 100644
--- a/be/src/core/column/column.h
+++ b/be/src/core/column/column.h
@@ -405,6 +405,16 @@ public:
                                "Method update_crc32c_batch is not supported 
for " + get_name());
     }
 
+    // Hash NULL rows as this column type's default value, instead of skipping 
them like
+    // update_crc32c_batch(hashes, null_map). This keeps the legacy Nullable 
fixed-width hash
+    // semantics without mutating the source nested column.
+    virtual void update_crc32c_batch_default_on_null(uint32_t* __restrict 
hashes,
+                                                     const uint8_t* __restrict 
null_map) const {
+        throw doris::Exception(
+                ErrorCode::NOT_IMPLEMENTED_ERROR,
+                "Method update_crc32c_batch_default_on_null is not supported 
for " + get_name());
+    }
+
     // use range for one hash value to avoid virtual function call in loop
     virtual void update_crc32c_single(size_t start, size_t end, uint32_t& hash,
                                       const uint8_t* __restrict null_map) 
const {
diff --git a/be/src/core/column/column_decimal.cpp 
b/be/src/core/column/column_decimal.cpp
index 662b016059a..ca830a93e55 100644
--- a/be/src/core/column/column_decimal.cpp
+++ b/be/src/core/column/column_decimal.cpp
@@ -193,6 +193,16 @@ void ColumnDecimal<T>::update_crc32c_batch(uint32_t* 
__restrict hashes,
     }
 }
 
+template <PrimitiveType T>
+void ColumnDecimal<T>::update_crc32c_batch_default_on_null(
+        uint32_t* __restrict hashes, const uint8_t* __restrict null_map) const 
{
+    DCHECK(null_map != nullptr);
+    auto s = size();
+    for (size_t i = 0; i < s; ++i) {
+        hashes[i] = HashUtil::crc32c_fixed(null_map[i] ? value_type() : 
data[i], hashes[i]);
+    }
+}
+
 template <PrimitiveType T>
 void ColumnDecimal<T>::update_crc32c_single(size_t start, size_t end, 
uint32_t& hash,
                                             const uint8_t* __restrict 
null_map) const {
diff --git a/be/src/core/column/column_decimal.h 
b/be/src/core/column/column_decimal.h
index f474f1b8a8b..4d6344fdfcd 100644
--- a/be/src/core/column/column_decimal.h
+++ b/be/src/core/column/column_decimal.h
@@ -172,6 +172,9 @@ public:
     void update_crc32c_batch(uint32_t* __restrict hashes,
                              const uint8_t* __restrict null_map) const 
override;
 
+    void update_crc32c_batch_default_on_null(uint32_t* __restrict hashes,
+                                             const uint8_t* __restrict 
null_map) const override;
+
     void update_crc32c_single(size_t start, size_t end, uint32_t& hash,
                               const uint8_t* __restrict null_map) const 
override;
 
diff --git a/be/src/core/column/column_nullable.cpp 
b/be/src/core/column/column_nullable.cpp
index c7ce9ab8f26..7a560dde4b8 100644
--- a/be/src/core/column/column_nullable.cpp
+++ b/be/src/core/column/column_nullable.cpp
@@ -184,18 +184,16 @@ void ColumnNullable::update_crcs_with_value(uint32_t* 
__restrict hashes, doris::
 void ColumnNullable::update_crc32c_batch(uint32_t* __restrict hashes,
                                          const uint8_t* __restrict /* null_map 
*/) const {
     const auto* __restrict real_null_data = 
get_null_map_column().get_data().data();
-    if (_nested_column->support_replace_column_null_data()) {
-        // nullmap process is slow, replace null data to default value to 
avoid nullmap process
-        // This is an intentional in-place mutation inside a logically-const 
hash computation:
-        // null positions are overwritten with defaults so the inner hash loop 
needs no null checks.
-        // The invariant is that a column instance is not hashed concurrently 
through the same
-        // owner while this per-block hash path runs. Shared aliases are 
detached by mutate()
-        // before this normalized nested column is written back.
-        auto nested_mut = std::move(*static_cast<const 
IColumn::Ptr&>(_nested_column)).mutate();
-        nested_mut->replace_column_null_data(real_null_data);
-        
static_cast<IColumn::Ptr&>(const_cast<IColumn::WrappedPtr&>(_nested_column)) =
-                std::move(nested_mut);
+    if (!has_null()) {
         _nested_column->update_crc32c_batch(hashes, nullptr);
+        return;
+    }
+
+    if (_nested_column->support_replace_column_null_data()) {
+        // Keep the old optimized hash semantics: NULL rows are hashed as the 
nested type's
+        // default value. Do this in a read-only way; replacing NULL payloads 
in _nested_column
+        // from this const hash path can race with other readers of the same 
column.
+        _nested_column->update_crc32c_batch_default_on_null(hashes, 
real_null_data);
     } else {
         auto s = size();
         for (int i = 0; i < s; ++i) {
diff --git a/be/src/core/column/column_vector.cpp 
b/be/src/core/column/column_vector.cpp
index d0e532871df..dabe4716356 100644
--- a/be/src/core/column/column_vector.cpp
+++ b/be/src/core/column/column_vector.cpp
@@ -183,17 +183,22 @@ uint32_t ColumnVector<T>::_zlib_crc32_hash(uint32_t hash, 
size_t idx) const {
 }
 
 template <PrimitiveType T>
-uint32_t ColumnVector<T>::_crc32c_hash(uint32_t hash, size_t idx) const {
+uint32_t ColumnVector<T>::_crc32c_hash_value(uint32_t hash, const value_type& 
value) const {
     if constexpr (is_date_or_datetime(T)) {
         char buf[64];
-        const auto& date_val = (const VecDateTimeValue&)data[idx];
+        const auto& date_val = (const VecDateTimeValue&)value;
         auto len = date_val.to_buffer(buf);
         return crc32c_extend(hash, (const uint8_t*)buf, len);
     } else {
-        return HashUtil::crc32c_fixed(data[idx], hash);
+        return HashUtil::crc32c_fixed(value, hash);
     }
 }
 
+template <PrimitiveType T>
+uint32_t ColumnVector<T>::_crc32c_hash(uint32_t hash, size_t idx) const {
+    return _crc32c_hash_value(hash, data[idx]);
+}
+
 template <PrimitiveType T>
 void ColumnVector<T>::update_crc32c_batch(uint32_t* __restrict hashes,
                                           const uint8_t* __restrict null_map) 
const {
@@ -211,6 +216,18 @@ void ColumnVector<T>::update_crc32c_batch(uint32_t* 
__restrict hashes,
     }
 }
 
+template <PrimitiveType T>
+void ColumnVector<T>::update_crc32c_batch_default_on_null(
+        uint32_t* __restrict hashes, const uint8_t* __restrict null_map) const 
{
+    DCHECK(null_map != nullptr);
+    auto s = size();
+    auto default_value = this->default_value();
+    for (size_t i = 0; i < s; ++i) {
+        hashes[i] = null_map[i] ? _crc32c_hash_value(hashes[i], default_value)
+                                : _crc32c_hash(hashes[i], i);
+    }
+}
+
 template <PrimitiveType T>
 void ColumnVector<T>::update_crc32c_single(size_t start, size_t end, uint32_t& 
hash,
                                            const uint8_t* __restrict null_map) 
const {
diff --git a/be/src/core/column/column_vector.h 
b/be/src/core/column/column_vector.h
index a1bae982c43..699f0115eb3 100644
--- a/be/src/core/column/column_vector.h
+++ b/be/src/core/column/column_vector.h
@@ -254,6 +254,9 @@ public:
     void update_crc32c_batch(uint32_t* __restrict hashes,
                              const uint8_t* __restrict null_map) const 
override;
 
+    void update_crc32c_batch_default_on_null(uint32_t* __restrict hashes,
+                                             const uint8_t* __restrict 
null_map) const override;
+
     void update_hashes_with_value(uint64_t* __restrict hashes,
                                   const uint8_t* __restrict null_data) const 
override;
 
@@ -402,6 +405,7 @@ public:
 
 protected:
     uint32_t _zlib_crc32_hash(uint32_t hash, size_t idx) const;
+    uint32_t _crc32c_hash_value(uint32_t hash, const value_type& value) const;
     uint32_t _crc32c_hash(uint32_t hash, size_t idx) const;
     Container data;
 };
diff --git a/be/test/core/column/column_nullable_test.cpp 
b/be/test/core/column/column_nullable_test.cpp
index 799cc6a9826..01e9e05b793 100644
--- a/be/test/core/column/column_nullable_test.cpp
+++ b/be/test/core/column/column_nullable_test.cpp
@@ -21,15 +21,22 @@
 #include <gtest/gtest-test-part.h>
 #include <gtest/gtest.h>
 
+#include <atomic>
+#include <thread>
+
 #include "common/status.h"
+#include "core/block/block.h"
+#include "core/column/column_decimal.h"
 #include "core/column/column_nullable_test.h"
 #include "core/column/predicate_column.h"
 #include "core/data_type/data_type.h"
+#include "core/data_type/data_type_nullable.h"
 #include "core/data_type/data_type_number.h"
 #include "core/data_type/define_primitive_type.h"
 #include "core/field.h"
 #include "core/types.h"
 #include "testutil/column_helper.h"
+#include "util/hash_util.hpp"
 
 namespace doris {
 
@@ -131,6 +138,221 @@ TEST(ColumnNullableTest, 
SharedCreatePreservesImmutableSubcolumns) {
     EXPECT_EQ(null_map_alias->size(), 1);
 }
 
+TEST(ColumnNullableTest, UpdateCrc32cBatchKeepsBlockInsertable) {
+    auto nested = ColumnInt32::create();
+    nested->insert_value(1);
+    nested->insert_value(2);
+    nested->insert_value(3);
+    nested->insert_value(4);
+
+    auto null_map = ColumnUInt8::create();
+    null_map->insert_value(0);
+    null_map->insert_value(1);
+    null_map->insert_value(0);
+    null_map->insert_value(1);
+
+    ColumnPtr nullable = ColumnNullable::create(std::move(nested), 
std::move(null_map));
+    const auto& nullable_ref = assert_cast<const ColumnNullable&>(*nullable);
+    const auto* nested_before_hash = 
nullable_ref.get_nested_column_ptr().get();
+
+    auto nullable_type = 
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeInt32>());
+    Block block({ColumnWithTypeAndName(nullable, nullable_type, "v")});
+    ASSERT_TRUE(block.check_type_and_column().ok());
+
+    std::vector<uint32_t> hashes(block.rows(), 0);
+    nullable->update_crc32c_batch(hashes.data(), nullptr);
+
+    ASSERT_TRUE(block.check_type_and_column().ok());
+
+    const auto& nullable_after_hash =
+            assert_cast<const 
ColumnNullable&>(*block.get_by_position(0).column);
+    EXPECT_EQ(nullable_after_hash.get_nested_column_ptr().get(), 
nested_before_hash);
+
+    auto dst_block = block.clone_empty();
+    auto mutable_block = MutableBlock::create_unique(std::move(dst_block));
+    std::vector<uint32_t> indices = {0, 1, 2, 3};
+    ASSERT_TRUE(
+            mutable_block->add_rows(&block, indices.data(), indices.data() + 
indices.size()).ok());
+    EXPECT_EQ(mutable_block->rows(), block.rows());
+}
+
+TEST(ColumnNullableTest, UpdateCrc32cBatchDoesNotMutateSharedNestedColumn) {
+    auto nested_mut = ColumnInt32::create();
+    nested_mut->insert_value(10);
+    nested_mut->insert_value(20);
+    nested_mut->insert_value(30);
+    nested_mut->insert_value(40);
+    ColumnPtr nested = std::move(nested_mut);
+    ColumnPtr nested_alias = nested;
+
+    auto null_map_mut = ColumnUInt8::create();
+    null_map_mut->insert_value(0);
+    null_map_mut->insert_value(1);
+    null_map_mut->insert_value(0);
+    null_map_mut->insert_value(1);
+    ColumnPtr null_map = std::move(null_map_mut);
+
+    ColumnPtr nullable = ColumnNullable::create(nested, null_map);
+    const auto& nullable_ref = assert_cast<const ColumnNullable&>(*nullable);
+    EXPECT_EQ(nullable_ref.get_nested_column_ptr().get(), nested_alias.get());
+
+    auto expected_nested = nested->clone_resized(nested->size());
+    expected_nested->replace_column_null_data(
+            assert_cast<const ColumnUInt8&>(*null_map).get_data().data());
+    std::vector<uint32_t> expected_hashes(nullable->size(), 0);
+    expected_nested->update_crc32c_batch(expected_hashes.data(), nullptr);
+
+    std::vector<uint32_t> hashes(nullable->size(), 0);
+    nullable->update_crc32c_batch(hashes.data(), nullptr);
+    EXPECT_EQ(hashes, expected_hashes);
+
+    const auto& nullable_after_hash = assert_cast<const 
ColumnNullable&>(*nullable);
+    EXPECT_EQ(nullable_after_hash.get_nested_column_ptr().get(), 
nested_alias.get());
+    EXPECT_EQ(assert_cast<const ColumnInt32&>(*nested_alias).get_data()[1], 
20);
+    EXPECT_EQ(
+            assert_cast<const 
ColumnInt32&>(nullable_after_hash.get_nested_column()).get_data()[1],
+            20);
+
+    auto nullable_type = 
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeInt32>());
+    Block block({ColumnWithTypeAndName(nullable, nullable_type, "v")});
+    ASSERT_TRUE(block.check_type_and_column().ok());
+
+    auto dst_block = block.clone_empty();
+    auto mutable_block = MutableBlock::create_unique(std::move(dst_block));
+    std::vector<uint32_t> indices = {3, 2, 1, 0};
+    ASSERT_TRUE(
+            mutable_block->add_rows(&block, indices.data(), indices.data() + 
indices.size()).ok());
+    EXPECT_EQ(mutable_block->rows(), block.rows());
+}
+
+TEST(ColumnNullableTest, 
UpdateCrc32cBatchHashesNullAsNestedDefaultForWideType) {
+    auto nested_mut = ColumnInt64::create();
+    nested_mut->insert_value(10);
+    nested_mut->insert_value(20);
+    nested_mut->insert_value(30);
+    nested_mut->insert_value(40);
+    ColumnPtr nested = std::move(nested_mut);
+
+    auto null_map_mut = ColumnUInt8::create();
+    null_map_mut->insert_value(0);
+    null_map_mut->insert_value(1);
+    null_map_mut->insert_value(0);
+    null_map_mut->insert_value(1);
+    ColumnPtr null_map = std::move(null_map_mut);
+
+    ColumnPtr nullable = ColumnNullable::create(nested, null_map);
+
+    auto expected_nested = nested->clone_resized(nested->size());
+    expected_nested->replace_column_null_data(
+            assert_cast<const ColumnUInt8&>(*null_map).get_data().data());
+    constexpr uint32_t seed = 0xDEADBEEF;
+    std::vector<uint32_t> expected_hashes(nullable->size(), seed);
+    expected_nested->update_crc32c_batch(expected_hashes.data(), nullptr);
+
+    std::vector<uint32_t> hashes(nullable->size(), seed);
+    nullable->update_crc32c_batch(hashes.data(), nullptr);
+
+    EXPECT_EQ(hashes, expected_hashes);
+    EXPECT_NE(hashes[1], HashUtil::crc32c_null(seed));
+    EXPECT_NE(hashes[3], HashUtil::crc32c_null(seed));
+}
+
+TEST(ColumnNullableTest, UpdateCrc32cBatchHashesNullAsDecimalDefault) {
+    auto nested_mut = ColumnDecimal64::create(0, 2);
+    nested_mut->insert_value(Decimal64(1010));
+    nested_mut->insert_value(Decimal64(2020));
+    nested_mut->insert_value(Decimal64(3030));
+    nested_mut->insert_value(Decimal64(4040));
+    ColumnPtr nested = std::move(nested_mut);
+
+    auto null_map_mut = ColumnUInt8::create();
+    null_map_mut->insert_value(0);
+    null_map_mut->insert_value(1);
+    null_map_mut->insert_value(0);
+    null_map_mut->insert_value(1);
+    ColumnPtr null_map = std::move(null_map_mut);
+
+    ColumnPtr nullable = ColumnNullable::create(nested, null_map);
+
+    auto expected_nested = nested->clone_resized(nested->size());
+    expected_nested->replace_column_null_data(
+            assert_cast<const ColumnUInt8&>(*null_map).get_data().data());
+    constexpr uint32_t seed = 0xDEADBEEF;
+    std::vector<uint32_t> expected_hashes(nullable->size(), seed);
+    expected_nested->update_crc32c_batch(expected_hashes.data(), nullptr);
+
+    std::vector<uint32_t> hashes(nullable->size(), seed);
+    nullable->update_crc32c_batch(hashes.data(), nullptr);
+
+    EXPECT_EQ(hashes, expected_hashes);
+    EXPECT_NE(hashes[1], HashUtil::crc32c_null(seed));
+    EXPECT_NE(hashes[3], HashUtil::crc32c_null(seed));
+}
+
+TEST(ColumnNullableTest, ConcurrentUpdateCrc32cBatchAndInsertIndicesFrom) {
+    constexpr int rows = 4096;
+    constexpr int hash_threads = 4;
+    constexpr int insert_threads = 4;
+    constexpr int iterations = 2000;
+
+    auto nested = ColumnInt32::create();
+    auto null_map = ColumnUInt8::create();
+    for (int i = 0; i < rows; ++i) {
+        nested->insert_value(i);
+        null_map->insert_value(i % 3 == 0);
+    }
+
+    ColumnPtr nullable = ColumnNullable::create(std::move(nested), 
std::move(null_map));
+    std::vector<uint32_t> indices(rows);
+    for (uint32_t i = 0; i < rows; ++i) {
+        indices[i] = rows - i - 1;
+    }
+
+    std::atomic<bool> start = false;
+    std::atomic<bool> stop = false;
+    std::atomic<int> failures = 0;
+    std::vector<std::thread> threads;
+
+    for (int t = 0; t < hash_threads; ++t) {
+        threads.emplace_back([&] {
+            while (!start.load(std::memory_order_acquire)) {
+            }
+            std::vector<uint32_t> hashes(rows);
+            for (int i = 0; i < iterations && 
!stop.load(std::memory_order_relaxed); ++i) {
+                std::ranges::fill(hashes, 0);
+                nullable->update_crc32c_batch(hashes.data(), nullptr);
+            }
+        });
+    }
+
+    for (int t = 0; t < insert_threads; ++t) {
+        threads.emplace_back([&] {
+            while (!start.load(std::memory_order_acquire)) {
+            }
+            for (int i = 0; i < iterations && 
!stop.load(std::memory_order_relaxed); ++i) {
+                auto dst = ColumnNullable::create(ColumnInt32::create(), 
ColumnUInt8::create());
+                try {
+                    dst->insert_indices_from(*nullable, indices.data(), 
indices.data() + rows);
+                    if (dst->size() != rows) {
+                        failures.fetch_add(1, std::memory_order_relaxed);
+                        stop.store(true, std::memory_order_relaxed);
+                    }
+                } catch (...) {
+                    failures.fetch_add(1, std::memory_order_relaxed);
+                    stop.store(true, std::memory_order_relaxed);
+                }
+            }
+        });
+    }
+
+    start.store(true, std::memory_order_release);
+    for (auto& thread : threads) {
+        thread.join();
+    }
+
+    EXPECT_EQ(failures.load(std::memory_order_relaxed), 0);
+}
+
 TEST(ColumnNullableTest, append_data_by_selector) {
     auto srt_column = ColumnHelper::create_nullable_column<DataTypeInt64>(
             {1, 2, 3, 4, 5, 6, 7, 8, 9, 10},


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

Reply via email to