diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt index dd17720595a7..808356a5ddb1 100644 --- a/cpp/src/arrow/CMakeLists.txt +++ b/cpp/src/arrow/CMakeLists.txt @@ -628,6 +628,7 @@ if(ARROW_COMPUTE) endif() add_arrow_benchmark(builder_benchmark) +add_arrow_benchmark(compare_benchmark) add_arrow_benchmark(memory_pool_benchmark) add_arrow_benchmark(type_benchmark) diff --git a/cpp/src/arrow/array/diff_test.cc b/cpp/src/arrow/array/diff_test.cc index b80ed2fd9555..a5022be59d74 100644 --- a/cpp/src/arrow/array/diff_test.cc +++ b/cpp/src/arrow/array/diff_test.cc @@ -35,6 +35,7 @@ #include "arrow/testing/random.h" #include "arrow/testing/util.h" #include "arrow/type.h" +#include "arrow/util/logging.h" namespace arrow { @@ -105,11 +106,12 @@ class DiffTest : public ::testing::Test { } void AssertInsertIs(const std::string& insert_json) { - ASSERT_ARRAYS_EQUAL(*ArrayFromJSON(boolean(), insert_json), *insert_); + AssertArraysEqual(*ArrayFromJSON(boolean(), insert_json), *insert_, /*verbose=*/true); } void AssertRunLengthIs(const std::string& run_lengths_json) { - ASSERT_ARRAYS_EQUAL(*ArrayFromJSON(int64(), run_lengths_json), *run_lengths_); + AssertArraysEqual(*ArrayFromJSON(int64(), run_lengths_json), *run_lengths_, + /*verbose=*/true); } void BaseAndTargetFromRandomFilter(std::shared_ptr values, @@ -127,6 +129,40 @@ class DiffTest : public ::testing::Test { target_ = out_datum.make_array(); } + void TestBasicsWithUnions(UnionMode::type mode) { + ASSERT_OK_AND_ASSIGN( + auto type, + UnionType::Make({field("foo", utf8()), field("bar", int32())}, {2, 5}, mode)); + + // insert one + base_ = ArrayFromJSON(type, R"([[2, "!"], [5, 3], [5, 13]])"); + target_ = ArrayFromJSON(type, R"([[2, "!"], [2, "?"], [5, 3], [5, 13]])"); + DoDiff(); + AssertInsertIs("[false, true]"); + AssertRunLengthIs("[1, 2]"); + + // delete one + base_ = ArrayFromJSON(type, R"([[2, "!"], [2, "?"], [5, 3], [5, 13]])"); + target_ = ArrayFromJSON(type, R"([[2, "!"], [5, 3], [5, 13]])"); + DoDiff(); + AssertInsertIs("[false, false]"); + AssertRunLengthIs("[1, 2]"); + + // change one + base_ = ArrayFromJSON(type, R"([[5, 3], [2, "!"], [5, 13]])"); + target_ = ArrayFromJSON(type, R"([[2, "3"], [2, "!"], [5, 13]])"); + DoDiff(); + AssertInsertIs("[false, false, true]"); + AssertRunLengthIs("[0, 0, 2]"); + + // null out one + base_ = ArrayFromJSON(type, R"([[2, "!"], [5, 3], [5, 13]])"); + target_ = ArrayFromJSON(type, R"([[2, "!"], [5, 3], null])"); + DoDiff(); + AssertInsertIs("[false, false, true]"); + AssertRunLengthIs("[2, 0, 0]"); + } + random::RandomArrayGenerator rng_; std::shared_ptr edits_; std::shared_ptr base_, target_; @@ -251,6 +287,36 @@ TEST_F(DiffTest, CompareRandomStrings) { } } +TEST_F(DiffTest, BasicsWithBooleans) { + // insert one + base_ = ArrayFromJSON(boolean(), R"([true, true, true])"); + target_ = ArrayFromJSON(boolean(), R"([true, false, true, true])"); + DoDiff(); + AssertInsertIs("[false, true]"); + AssertRunLengthIs("[1, 2]"); + + // delete one + base_ = ArrayFromJSON(boolean(), R"([true, false, true, true])"); + target_ = ArrayFromJSON(boolean(), R"([true, true, true])"); + DoDiff(); + AssertInsertIs("[false, false]"); + AssertRunLengthIs("[1, 2]"); + + // change one + base_ = ArrayFromJSON(boolean(), R"([false, false, true])"); + target_ = ArrayFromJSON(boolean(), R"([true, false, true])"); + DoDiff(); + AssertInsertIs("[false, false, true]"); + AssertRunLengthIs("[0, 0, 2]"); + + // null out one + base_ = ArrayFromJSON(boolean(), R"([true, false, true])"); + target_ = ArrayFromJSON(boolean(), R"([true, false, null])"); + DoDiff(); + AssertInsertIs("[false, false, true]"); + AssertRunLengthIs("[2, 0, 0]"); +} + TEST_F(DiffTest, BasicsWithStrings) { // insert one base_ = ArrayFromJSON(utf8(), R"(["give", "a", "break"])"); @@ -345,37 +411,9 @@ TEST_F(DiffTest, BasicsWithStructs) { AssertRunLengthIs("[2, 0, 0]"); } -TEST_F(DiffTest, BasicsWithUnions) { - auto type = sparse_union({field("foo", utf8()), field("bar", int32())}, {2, 5}); - - // insert one - base_ = ArrayFromJSON(type, R"([[2, "!"], [5, 3], [5, 13]])"); - target_ = ArrayFromJSON(type, R"([[2, "!"], [2, "?"], [5, 3], [5, 13]])"); - DoDiff(); - AssertInsertIs("[false, true]"); - AssertRunLengthIs("[1, 2]"); - - // delete one - base_ = ArrayFromJSON(type, R"([[2, "!"], [2, "?"], [5, 3], [5, 13]])"); - target_ = ArrayFromJSON(type, R"([[2, "!"], [5, 3], [5, 13]])"); - DoDiff(); - AssertInsertIs("[false, false]"); - AssertRunLengthIs("[1, 2]"); - - // change one - base_ = ArrayFromJSON(type, R"([[5, 3], [2, "!"], [5, 13]])"); - target_ = ArrayFromJSON(type, R"([[2, "3"], [2, "!"], [5, 13]])"); - DoDiff(); - AssertInsertIs("[false, false, true]"); - AssertRunLengthIs("[0, 0, 2]"); +TEST_F(DiffTest, BasicsWithSparseUnions) { TestBasicsWithUnions(UnionMode::SPARSE); } - // null out one - base_ = ArrayFromJSON(type, R"([[2, "!"], [5, 3], [5, 13]])"); - target_ = ArrayFromJSON(type, R"([[2, "!"], [5, 3], null])"); - DoDiff(); - AssertInsertIs("[false, false, true]"); - AssertRunLengthIs("[2, 0, 0]"); -} +TEST_F(DiffTest, BasicsWithDenseUnions) { TestBasicsWithUnions(UnionMode::DENSE); } TEST_F(DiffTest, UnifiedDiffFormatter) { // no changes diff --git a/cpp/src/arrow/compare.cc b/cpp/src/arrow/compare.cc index 622f5cb5c5f6..b696c76a9c99 100644 --- a/cpp/src/arrow/compare.cc +++ b/cpp/src/arrow/compare.cc @@ -38,8 +38,10 @@ #include "arrow/tensor.h" #include "arrow/type.h" #include "arrow/type_traits.h" +#include "arrow/util/bit_block_counter.h" #include "arrow/util/bit_util.h" #include "arrow/util/bitmap_ops.h" +#include "arrow/util/bitmap_reader.h" #include "arrow/util/checked_cast.h" #include "arrow/util/logging.h" #include "arrow/util/macros.h" @@ -49,700 +51,438 @@ namespace arrow { using internal::BitmapEquals; +using internal::BitmapReader; +using internal::BitmapUInt64Reader; using internal::checked_cast; +using internal::OptionalBitBlockCounter; +using internal::OptionalBitmapEquals; // ---------------------------------------------------------------------- // Public method implementations namespace { -// These helper functions assume we already checked the arrays have equal -// sizes and null bitmaps. +bool CompareArrayRanges(const ArrayData& left, const ArrayData& right, + int64_t left_start_idx, int64_t left_end_idx, + int64_t right_start_idx, const EqualOptions& options, + bool floating_approximate); -template -inline bool BaseFloatingEquals(const NumericArray& left, - const NumericArray& right, - EqualityFunc&& equals) { - using T = typename ArrowType::c_type; - - const T* left_data = left.raw_values(); - const T* right_data = right.raw_values(); - - if (left.null_count() > 0) { - for (int64_t i = 0; i < left.length(); ++i) { - if (left.IsNull(i)) continue; - if (!equals(left_data[i], right_data[i])) { - return false; - } - } - } else { - for (int64_t i = 0; i < left.length(); ++i) { - if (!equals(left_data[i], right_data[i])) { - return false; - } - } - } - return true; -} - -template -inline bool FloatingEquals(const NumericArray& left, - const NumericArray& right, - const EqualOptions& opts) { - using T = typename ArrowType::c_type; - - if (opts.nans_equal()) { - return BaseFloatingEquals(left, right, [](T x, T y) -> bool { - return (x == y) || (std::isnan(x) && std::isnan(y)); - }); - } else { - return BaseFloatingEquals(left, right, - [](T x, T y) -> bool { return x == y; }); - } -} - -template -inline bool FloatingApproxEquals(const NumericArray& left, - const NumericArray& right, - const EqualOptions& opts) { - using T = typename ArrowType::c_type; - const T epsilon = static_cast(opts.atol()); - - if (opts.nans_equal()) { - return BaseFloatingEquals(left, right, [epsilon](T x, T y) -> bool { - return (fabs(x - y) <= epsilon) || (x == y) || (std::isnan(x) && std::isnan(y)); - }); - } else { - return BaseFloatingEquals(left, right, [epsilon](T x, T y) -> bool { - return (fabs(x - y) <= epsilon) || (x == y); - }); - } -} - -// RangeEqualsVisitor assumes the range sizes are equal - -class RangeEqualsVisitor { +class RangeDataEqualsImpl { public: - RangeEqualsVisitor(const Array& right, int64_t left_start_idx, int64_t left_end_idx, - int64_t right_start_idx) - : right_(right), + // PRE-CONDITIONS: + // - the types are equal + // - the ranges are in bounds + RangeDataEqualsImpl(const EqualOptions& options, bool floating_approximate, + const ArrayData& left, const ArrayData& right, + int64_t left_start_idx, int64_t right_start_idx, + int64_t range_length) + : options_(options), + floating_approximate_(floating_approximate), + left_(left), + right_(right), left_start_idx_(left_start_idx), - left_end_idx_(left_end_idx), right_start_idx_(right_start_idx), + range_length_(range_length), result_(false) {} - template - inline Status CompareValues(const ArrayType& left) { - const auto& right = checked_cast(right_); - - for (int64_t i = left_start_idx_, o_i = right_start_idx_; i < left_end_idx_; - ++i, ++o_i) { - const bool is_null = left.IsNull(i); - if (is_null != right.IsNull(o_i) || - (!is_null && left.Value(i) != right.Value(o_i))) { - result_ = false; - return Status::OK(); + bool Compare() { + // Compare null bitmaps + if (left_start_idx_ == 0 && right_start_idx_ == 0 && range_length_ == left_.length && + range_length_ == right_.length) { + // If we're comparing entire arrays, we can first compare the cached null counts + if (left_.GetNullCount() != right_.GetNullCount()) { + return false; } } - result_ = true; - return Status::OK(); + if (!OptionalBitmapEquals(left_.buffers[0], left_.offset + left_start_idx_, + right_.buffers[0], right_.offset + right_start_idx_, + range_length_)) { + return false; + } + // Compare values + return CompareWithType(*left_.type); } - template - bool CompareWithOffsets(const ArrayType& left, - CompareValuesFunc&& compare_values) const { - const auto& right = checked_cast(right_); - - for (int64_t i = left_start_idx_, o_i = right_start_idx_; i < left_end_idx_; - ++i, ++o_i) { - const bool is_null = left.IsNull(i); - if (is_null != right.IsNull(o_i)) { - return false; - } - if (is_null) continue; - const auto begin_offset = left.value_offset(i); - const auto end_offset = left.value_offset(i + 1); - const auto right_begin_offset = right.value_offset(o_i); - const auto right_end_offset = right.value_offset(o_i + 1); - // Underlying can't be equal if the size isn't equal - if (end_offset - begin_offset != right_end_offset - right_begin_offset) { - return false; - } - - if (!compare_values(left, right, begin_offset, right_begin_offset, - end_offset - begin_offset)) { - return false; - } + bool CompareWithType(const DataType& type) { + result_ = true; + if (range_length_ != 0) { + ARROW_CHECK_OK(VisitTypeInline(type, this)); } - return true; + return result_; } - template - bool CompareBinaryRange(const BinaryArrayType& left) const { - using offset_type = typename BinaryArrayType::offset_type; + Status Visit(const NullType&) { return Status::OK(); } - auto compare_values = [](const BinaryArrayType& left, const BinaryArrayType& right, - offset_type left_offset, offset_type right_offset, - offset_type nvalues) { - if (nvalues == 0) { - return true; - } - return std::memcmp(left.value_data()->data() + left_offset, - right.value_data()->data() + right_offset, - static_cast(nvalues)) == 0; - }; - return CompareWithOffsets(left, compare_values); + template + enable_if_primitive_ctype Visit(const TypeClass& type) { + return ComparePrimitive(type); } - template - bool CompareLists(const ListArrayType& left) { - using offset_type = typename ListArrayType::offset_type; - const auto& right = checked_cast(right_); - const std::shared_ptr& left_values = left.values(); - const std::shared_ptr& right_values = right.values(); - - auto compare_values = [&](const ListArrayType& left, const ListArrayType& right, - offset_type left_offset, offset_type right_offset, - offset_type nvalues) { - if (nvalues == 0) { - return true; - } - return left_values->RangeEquals(left_offset, left_offset + nvalues, right_offset, - right_values); - }; - return CompareWithOffsets(left, compare_values); - } - - bool CompareMaps(const MapArray& left) { - // We need a specific comparison helper for maps to avoid comparing - // struct field names (which are indifferent for maps) - using offset_type = typename MapArray::offset_type; - const auto& right = checked_cast(right_); - const auto left_keys = left.keys(); - const auto left_items = left.items(); - const auto right_keys = right.keys(); - const auto right_items = right.items(); - - auto compare_values = [&](const MapArray& left, const MapArray& right, - offset_type left_offset, offset_type right_offset, - offset_type nvalues) { - if (nvalues == 0) { - return true; - } - return left_keys->RangeEquals(left_offset, left_offset + nvalues, right_offset, - right_keys) && - left_items->RangeEquals(left_offset, left_offset + nvalues, right_offset, - right_items); - }; - return CompareWithOffsets(left, compare_values); + template + enable_if_t::value, Status> Visit(const TypeClass& type) { + return ComparePrimitive(type); } - bool CompareStructs(const StructArray& left) { - const auto& right = checked_cast(right_); - bool equal_fields = true; - for (int64_t i = left_start_idx_, o_i = right_start_idx_; i < left_end_idx_; - ++i, ++o_i) { - if (left.IsNull(i) != right.IsNull(o_i)) { - return false; - } - if (left.IsNull(i)) continue; - for (int j = 0; j < left.num_fields(); ++j) { - // TODO: really we should be comparing stretches of non-null data rather - // than looking at one value at a time. - equal_fields = left.field(j)->RangeEquals(i, i + 1, o_i, right.field(j)); - if (!equal_fields) { - return false; - } - } - } - return true; - } - - bool CompareUnions(const UnionArray& left) const { - const auto& right = checked_cast(right_); - - const UnionMode::type union_mode = left.mode(); - if (union_mode != right.mode()) { - return false; - } - - const auto& left_type = checked_cast(*left.type()); - - const std::vector& child_ids = left_type.child_ids(); - - const int8_t* left_codes = left.raw_type_codes(); - const int8_t* right_codes = right.raw_type_codes(); - - for (int64_t i = left_start_idx_, o_i = right_start_idx_; i < left_end_idx_; - ++i, ++o_i) { - if (left.IsNull(i) != right.IsNull(o_i)) { - return false; - } - if (left.IsNull(i)) continue; - if (left_codes[i] != right_codes[o_i]) { - return false; - } - - auto child_num = child_ids[left_codes[i]]; - - // TODO(wesm): really we should be comparing stretches of non-null data - // rather than looking at one value at a time. - if (union_mode == UnionMode::SPARSE) { - if (!left.field(child_num)->RangeEquals(i, i + 1, o_i, right.field(child_num))) { - return false; + Status Visit(const BooleanType&) { + const uint8_t* left_bits = left_.GetValues(1, 0); + const uint8_t* right_bits = right_.GetValues(1, 0); + auto compare_runs = [&](int64_t i, int64_t length) -> bool { + if (length <= 8) { + // Avoid the BitmapUInt64Reader overhead for very small runs + for (int64_t j = i; j < i + length; ++j) { + if (BitUtil::GetBit(left_bits, left_start_idx_ + left_.offset + j) != + BitUtil::GetBit(right_bits, right_start_idx_ + right_.offset + j)) { + return false; + } } + return true; } else { - const int32_t offset = - checked_cast(left).raw_value_offsets()[i]; - const int32_t o_offset = - checked_cast(right).raw_value_offsets()[o_i]; - if (!left.field(child_num)->RangeEquals(offset, offset + 1, o_offset, - right.field(child_num))) { - return false; + BitmapUInt64Reader left_reader(left_bits, left_start_idx_ + left_.offset + i, + length); + BitmapUInt64Reader right_reader(right_bits, right_start_idx_ + right_.offset + i, + length); + while (left_reader.position() < length) { + if (left_reader.NextWord() != right_reader.NextWord()) { + return false; + } } + DCHECK_EQ(right_reader.position(), length); } - } - return true; - } - - Status Visit(const BinaryArray& left) { - result_ = CompareBinaryRange(left); + return true; + }; + VisitValidRuns(compare_runs); return Status::OK(); } - Status Visit(const LargeBinaryArray& left) { - result_ = CompareBinaryRange(left); - return Status::OK(); - } + Status Visit(const FloatType& type) { return CompareFloating(type); } - Status Visit(const FixedSizeBinaryArray& left) { - const auto& right = checked_cast(right_); + Status Visit(const DoubleType& type) { return CompareFloating(type); } - int32_t width = left.byte_width(); + // Also matches StringType + Status Visit(const BinaryType& type) { return CompareBinary(type); } - const uint8_t* left_data = nullptr; - const uint8_t* right_data = nullptr; + // Also matches LargeStringType + Status Visit(const LargeBinaryType& type) { return CompareBinary(type); } - if (left.values()) { - left_data = left.raw_values(); - } - - if (right.values()) { - right_data = right.raw_values(); - } - - for (int64_t i = left_start_idx_, o_i = right_start_idx_; i < left_end_idx_; - ++i, ++o_i) { - const bool is_null = left.IsNull(i); - if (is_null != right.IsNull(o_i)) { - result_ = false; - return Status::OK(); - } - if (is_null) continue; + Status Visit(const FixedSizeBinaryType& type) { + const auto byte_width = type.byte_width(); + const uint8_t* left_data = left_.GetValues(1, 0); + const uint8_t* right_data = right_.GetValues(1, 0); - if (std::memcmp(left_data + width * i, right_data + width * o_i, width)) { - result_ = false; - return Status::OK(); - } + if (left_data != nullptr && right_data != nullptr) { + auto compare_runs = [&](int64_t i, int64_t length) -> bool { + return memcmp(left_data + (left_start_idx_ + left_.offset + i) * byte_width, + right_data + (right_start_idx_ + right_.offset + i) * byte_width, + length * byte_width) == 0; + }; + VisitValidRuns(compare_runs); + } else { + auto compare_runs = [&](int64_t i, int64_t length) -> bool { return true; }; + VisitValidRuns(compare_runs); } - result_ = true; return Status::OK(); } - Status Visit(const Decimal128Array& left) { - return Visit(checked_cast(left)); - } + // Also matches MapType + Status Visit(const ListType& type) { return CompareList(type); } - Status Visit(const Decimal256Array& left) { - return Visit(checked_cast(left)); - } + Status Visit(const LargeListType& type) { return CompareList(type); } - Status Visit(const NullArray& left) { - ARROW_UNUSED(left); - result_ = true; - return Status::OK(); - } + Status Visit(const FixedSizeListType& type) { + const auto list_size = type.list_size(); + const ArrayData& left_data = *left_.child_data[0]; + const ArrayData& right_data = *right_.child_data[0]; - template - typename std::enable_if::value, Status>::type Visit( - const T& left) { - return CompareValues(left); - } - - Status Visit(const ListArray& left) { - result_ = CompareLists(left); + auto compare_runs = [&](int64_t i, int64_t length) -> bool { + RangeDataEqualsImpl impl(options_, floating_approximate_, left_data, right_data, + (left_start_idx_ + left_.offset + i) * list_size, + (right_start_idx_ + right_.offset + i) * list_size, + length * list_size); + return impl.Compare(); + }; + VisitValidRuns(compare_runs); return Status::OK(); } - Status Visit(const LargeListArray& left) { - result_ = CompareLists(left); - return Status::OK(); - } + Status Visit(const StructType& type) { + const int32_t num_fields = type.num_fields(); - Status Visit(const FixedSizeListArray& left) { - const auto& right = checked_cast(right_); - result_ = left.values()->RangeEquals( - left.value_offset(left_start_idx_), left.value_offset(left_end_idx_), - right.value_offset(right_start_idx_), right.values()); + auto compare_runs = [&](int64_t i, int64_t length) -> bool { + for (int32_t f = 0; f < num_fields; ++f) { + RangeDataEqualsImpl impl(options_, floating_approximate_, *left_.child_data[f], + *right_.child_data[f], + left_start_idx_ + left_.offset + i, + right_start_idx_ + right_.offset + i, length); + if (!impl.Compare()) { + return false; + } + } + return true; + }; + VisitValidRuns(compare_runs); return Status::OK(); } - Status Visit(const MapArray& left) { - result_ = CompareMaps(left); - return Status::OK(); - } + Status Visit(const SparseUnionType& type) { + const auto& child_ids = type.child_ids(); + const int8_t* left_codes = left_.GetValues(1); + const int8_t* right_codes = right_.GetValues(1); - Status Visit(const StructArray& left) { - result_ = CompareStructs(left); + // Unions don't have a null bitmap + for (int64_t i = 0; i < range_length_; ++i) { + const auto type_id = left_codes[left_start_idx_ + i]; + if (type_id != right_codes[right_start_idx_ + i]) { + result_ = false; + break; + } + const auto child_num = child_ids[type_id]; + // XXX can we instead detect runs of same-child union values? + RangeDataEqualsImpl impl( + options_, floating_approximate_, *left_.child_data[child_num], + *right_.child_data[child_num], left_start_idx_ + left_.offset + i, + right_start_idx_ + right_.offset + i, 1); + if (!impl.Compare()) { + result_ = false; + break; + } + } return Status::OK(); } - Status Visit(const UnionArray& left) { - result_ = CompareUnions(left); + Status Visit(const DenseUnionType& type) { + const auto& child_ids = type.child_ids(); + const int8_t* left_codes = left_.GetValues(1); + const int8_t* right_codes = right_.GetValues(1); + const int32_t* left_offsets = left_.GetValues(2); + const int32_t* right_offsets = right_.GetValues(2); + + for (int64_t i = 0; i < range_length_; ++i) { + const auto type_id = left_codes[left_start_idx_ + i]; + if (type_id != right_codes[right_start_idx_ + i]) { + result_ = false; + break; + } + const auto child_num = child_ids[type_id]; + RangeDataEqualsImpl impl( + options_, floating_approximate_, *left_.child_data[child_num], + *right_.child_data[child_num], left_offsets[left_start_idx_ + i], + right_offsets[right_start_idx_ + i], 1); + if (!impl.Compare()) { + result_ = false; + break; + } + } return Status::OK(); } - Status Visit(const DictionaryArray& left) { - const auto& right = checked_cast(right_); - if (!left.dictionary()->Equals(right.dictionary())) { - result_ = false; - return Status::OK(); + Status Visit(const DictionaryType& type) { + // Compare dictionaries + result_ &= CompareArrayRanges( + *left_.dictionary, *right_.dictionary, + /*left_start_idx=*/0, + /*left_end_idx=*/std::max(left_.dictionary->length, right_.dictionary->length), + /*right_start_idx=*/0, options_, floating_approximate_); + if (result_) { + // Compare indices + result_ &= CompareWithType(*type.index_type()); } - result_ = left.indices()->RangeEquals(left_start_idx_, left_end_idx_, - right_start_idx_, right.indices()); return Status::OK(); } - Status Visit(const ExtensionArray& left) { - result_ = (right_.type()->Equals(*left.type()) && - ArrayRangeEquals(*left.storage(), - *static_cast(right_).storage(), - left_start_idx_, left_end_idx_, right_start_idx_)); + Status Visit(const ExtensionType& type) { + // Compare storages + result_ &= CompareWithType(*type.storage_type()); return Status::OK(); } - bool result() const { return result_; } - protected: - const Array& right_; - int64_t left_start_idx_; - int64_t left_end_idx_; - int64_t right_start_idx_; - - bool result_; -}; - -static bool IsEqualPrimitive(const PrimitiveArray& left, const PrimitiveArray& right) { - const int byte_width = internal::GetByteWidth(*left.type()); - - const uint8_t* left_data = nullptr; - const uint8_t* right_data = nullptr; - - if (left.values()) { - left_data = left.values()->data() + left.offset() * byte_width; + template + Status ComparePrimitive(const TypeClass&) { + const CType* left_values = left_.GetValues(1); + const CType* right_values = right_.GetValues(1); + VisitValidRuns([&](int64_t i, int64_t length) { + return memcmp(left_values + left_start_idx_ + i, + right_values + right_start_idx_ + i, length * sizeof(CType)) == 0; + }); + return Status::OK(); } - if (right.values()) { - right_data = right.values()->data() + right.offset() * byte_width; - } + template + Status CompareFloating(const TypeClass&) { + using T = typename TypeClass::c_type; + const T* left_values = left_.GetValues(1); + const T* right_values = right_.GetValues(1); - if (byte_width == 0) { - // Special case 0-width data, as the data pointers may be null - for (int64_t i = 0; i < left.length(); ++i) { - if (left.IsNull(i) != right.IsNull(i)) { - return false; - } - } - return true; - } else if (left.null_count() > 0) { - for (int64_t i = 0; i < left.length(); ++i) { - const bool left_null = left.IsNull(i); - const bool right_null = right.IsNull(i); - if (left_null != right_null) { - return false; + if (floating_approximate_) { + const T epsilon = static_cast(options_.atol()); + if (options_.nans_equal()) { + VisitValues([&](int64_t i) { + const T x = left_values[i + left_start_idx_]; + const T y = right_values[i + right_start_idx_]; + return (fabs(x - y) <= epsilon) || (x == y) || (std::isnan(x) && std::isnan(y)); + }); + } else { + VisitValues([&](int64_t i) { + const T x = left_values[i + left_start_idx_]; + const T y = right_values[i + right_start_idx_]; + return (fabs(x - y) <= epsilon) || (x == y); + }); } - if (!left_null && memcmp(left_data, right_data, byte_width) != 0) { - return false; + } else { + if (options_.nans_equal()) { + VisitValues([&](int64_t i) { + const T x = left_values[i + left_start_idx_]; + const T y = right_values[i + right_start_idx_]; + return (x == y) || (std::isnan(x) && std::isnan(y)); + }); + } else { + VisitValues([&](int64_t i) { + const T x = left_values[i + left_start_idx_]; + const T y = right_values[i + right_start_idx_]; + return x == y; + }); } - left_data += byte_width; - right_data += byte_width; } - return true; - } else { - auto number_of_bytes_to_compare = static_cast(byte_width * left.length()); - return memcmp(left_data, right_data, number_of_bytes_to_compare) == 0; - } -} - -// A bit confusing: ArrayEqualsVisitor inherits from RangeEqualsVisitor but -// doesn't share the same preconditions. -// When RangeEqualsVisitor is called, we only know the range sizes equal. -// When ArrayEqualsVisitor is called, we know the sizes and null bitmaps are equal. - -class ArrayEqualsVisitor : public RangeEqualsVisitor { - public: - explicit ArrayEqualsVisitor(const Array& right, const EqualOptions& opts) - : RangeEqualsVisitor(right, 0, right.length(), 0), opts_(opts) {} - - Status Visit(const NullArray& left) { - ARROW_UNUSED(left); - result_ = true; return Status::OK(); } - Status Visit(const BooleanArray& left) { - const auto& right = checked_cast(right_); - - if (left.null_count() > 0) { - const uint8_t* left_data = left.values()->data(); - const uint8_t* right_data = right.values()->data(); + template + Status CompareBinary(const TypeClass&) { + const uint8_t* left_data = left_.GetValues(2, 0); + const uint8_t* right_data = right_.GetValues(2, 0); - for (int64_t i = 0; i < left.length(); ++i) { - if (left.IsValid(i) && BitUtil::GetBit(left_data, i + left.offset()) != - BitUtil::GetBit(right_data, i + right.offset())) { - result_ = false; - return Status::OK(); - } - } - result_ = true; + if (left_data != nullptr && right_data != nullptr) { + const auto compare_ranges = [&](int64_t left_offset, int64_t right_offset, + int64_t length) -> bool { + return memcmp(left_data + left_offset, right_data + right_offset, length) == 0; + }; + CompareWithOffsets(1, compare_ranges); } else { - result_ = BitmapEquals(left.values()->data(), left.offset(), right.values()->data(), - right.offset(), left.length()); + // One of the arrays is an array of empty strings and nulls. + // We just need to compare the offsets. + // (note we must not call memcmp() with null data pointers) + CompareWithOffsets(1, [](...) { return true; }); } return Status::OK(); } - template - typename std::enable_if::value && - !std::is_base_of::value && - !std::is_base_of::value && - !std::is_base_of::value, - Status>::type - Visit(const T& left) { - result_ = IsEqualPrimitive(left, checked_cast(right_)); - return Status::OK(); - } - - // TODO nan-aware specialization for half-floats + template + Status CompareList(const TypeClass&) { + const ArrayData& left_data = *left_.child_data[0]; + const ArrayData& right_data = *right_.child_data[0]; - Status Visit(const FloatArray& left) { - result_ = - FloatingEquals(left, checked_cast(right_), opts_); - return Status::OK(); - } + const auto compare_ranges = [&](int64_t left_offset, int64_t right_offset, + int64_t length) -> bool { + RangeDataEqualsImpl impl(options_, floating_approximate_, left_data, right_data, + left_offset, right_offset, length); + return impl.Compare(); + }; - Status Visit(const DoubleArray& left) { - result_ = - FloatingEquals(left, checked_cast(right_), opts_); + CompareWithOffsets(1, compare_ranges); return Status::OK(); } - template - bool ValueOffsetsEqual(const ArrayType& left) { - using offset_type = typename ArrayType::offset_type; - - const auto& right = checked_cast(right_); + template + void CompareWithOffsets(int offsets_buffer_index, CompareRanges&& compare_ranges) { + const offset_type* left_offsets = + left_.GetValues(offsets_buffer_index) + left_start_idx_; + const offset_type* right_offsets = + right_.GetValues(offsets_buffer_index) + right_start_idx_; - if (left.offset() == 0 && right.offset() == 0) { - return left.value_offsets()->Equals(*right.value_offsets(), - (left.length() + 1) * sizeof(offset_type)); - } else { - // One of the arrays is sliced; logic is more complicated because the - // value offsets are not both 0-based - auto left_offsets = - reinterpret_cast(left.value_offsets()->data()) + - left.offset(); - auto right_offsets = - reinterpret_cast(right.value_offsets()->data()) + - right.offset(); - - for (int64_t i = 0; i < left.length() + 1; ++i) { - if (left_offsets[i] - left_offsets[0] != right_offsets[i] - right_offsets[0]) { + const auto compare_runs = [&](int64_t i, int64_t length) { + for (int64_t j = i; j < i + length; ++j) { + if (left_offsets[j + 1] - left_offsets[j] != + right_offsets[j + 1] - right_offsets[j]) { return false; } } - return true; - } - } - - template - bool CompareBinary(const BinaryArrayType& left) { - const auto& right = checked_cast(right_); - - bool equal_offsets = ValueOffsetsEqual(left); - if (!equal_offsets) { - return false; - } - - if (!left.value_data() && !(right.value_data())) { - return true; - } - if (left.value_offset(left.length()) == left.value_offset(0)) { - return true; - } - - const uint8_t* left_data = left.value_data()->data(); - const uint8_t* right_data = right.value_data()->data(); - - if (left.null_count() == 0) { - // Fast path for null count 0, single memcmp - if (left.offset() == 0 && right.offset() == 0) { - return std::memcmp(left_data, right_data, - left.raw_value_offsets()[left.length()]) == 0; - } else { - const int64_t total_bytes = - left.value_offset(left.length()) - left.value_offset(0); - return std::memcmp(left_data + left.value_offset(0), - right_data + right.value_offset(0), - static_cast(total_bytes)) == 0; - } - } else { - // ARROW-537: Only compare data in non-null slots - auto left_offsets = left.raw_value_offsets(); - auto right_offsets = right.raw_value_offsets(); - for (int64_t i = 0; i < left.length(); ++i) { - if (left.IsNull(i)) { - continue; - } - if (std::memcmp(left_data + left_offsets[i], right_data + right_offsets[i], - left.value_length(i))) { - return false; - } + if (!compare_ranges(left_offsets[i], right_offsets[i], + left_offsets[i + length] - left_offsets[i])) { + return false; } return true; - } - } - - template - bool CompareList(const ListArrayType& left) { - const auto& right = checked_cast(right_); - - bool equal_offsets = ValueOffsetsEqual(left); - if (!equal_offsets) { - return false; - } - - return left.values()->RangeEquals(left.value_offset(0), - left.value_offset(left.length()), - right.value_offset(0), right.values()); - } - - Status Visit(const BinaryArray& left) { - result_ = CompareBinary(left); - return Status::OK(); - } - - Status Visit(const LargeBinaryArray& left) { - result_ = CompareBinary(left); - return Status::OK(); - } + }; - Status Visit(const ListArray& left) { - result_ = CompareList(left); - return Status::OK(); + VisitValidRuns(compare_runs); } - Status Visit(const LargeListArray& left) { - result_ = CompareList(left); - return Status::OK(); + template + void VisitValues(CompareValues&& compare_values) { + internal::VisitBitBlocksVoid( + left_.buffers[0], left_.offset + left_start_idx_, range_length_, + [&](int64_t position) { result_ &= compare_values(position); }, []() {}); } - Status Visit(const FixedSizeListArray& left) { - const auto& right = checked_cast(right_); - result_ = - left.values()->RangeEquals(left.value_offset(0), left.value_offset(left.length()), - right.value_offset(0), right.values()); - return Status::OK(); - } + // Visit and compare runs of non-null values + template + void VisitValidRuns(CompareRuns&& compare_runs) { + int64_t i = 0; + const uint8_t* left_null_bitmap = left_.GetValues(0, 0); + // TODO may use BitRunReader or equivalent? + OptionalBitBlockCounter bit_blocks(left_null_bitmap, left_.offset + left_start_idx_, + range_length_); - Status Visit(const DictionaryArray& left) { - const auto& right = checked_cast(right_); - if (!left.dictionary()->Equals(right.dictionary())) { - result_ = false; - } else { - result_ = left.indices()->Equals(right.indices()); + while (result_ && i < range_length_) { + const auto block = bit_blocks.NextBlock(); + if (block.AllSet()) { + if (!compare_runs(i, block.length)) { + result_ = false; + return; + } + } else if (block.NoneSet()) { + // Nothing to do + } else { + // Compare non-null values one at a time + for (int64_t j = i; j < i + block.length; ++j) { + if (BitUtil::GetBit(left_null_bitmap, left_.offset + left_start_idx_ + j)) { + if (!compare_runs(j, 1)) { + result_ = false; + return; + } + } + } + } + i += block.length; } - return Status::OK(); } - template - typename std::enable_if::value, - Status>::type - Visit(const T& left) { - return RangeEqualsVisitor::Visit(left); - } + const EqualOptions& options_; + const bool floating_approximate_; + const ArrayData& left_; + const ArrayData& right_; + const int64_t left_start_idx_; + const int64_t right_start_idx_; + const int64_t range_length_; - Status Visit(const ExtensionArray& left) { - result_ = (right_.type()->Equals(*left.type()) && - ArrayEquals(*left.storage(), - *static_cast(right_).storage())); - return Status::OK(); - } - - protected: - const EqualOptions opts_; + bool result_; }; -class ApproxEqualsVisitor : public ArrayEqualsVisitor { - public: - explicit ApproxEqualsVisitor(const Array& right, const EqualOptions& opts) - : ArrayEqualsVisitor(right, opts) {} - - using ArrayEqualsVisitor::Visit; - - // TODO half-floats - - Status Visit(const FloatArray& left) { - result_ = FloatingApproxEquals( - left, checked_cast(right_), opts_); - return Status::OK(); - } - - Status Visit(const DoubleArray& left) { - result_ = FloatingApproxEquals( - left, checked_cast(right_), opts_); - return Status::OK(); +bool CompareArrayRanges(const ArrayData& left, const ArrayData& right, + int64_t left_start_idx, int64_t left_end_idx, + int64_t right_start_idx, const EqualOptions& options, + bool floating_approximate) { + if (left.type->id() != right.type->id() || + !TypeEquals(*left.type, *right.type, false /* check_metadata */)) { + return false; } -}; -static bool BaseDataEquals(const Array& left, const Array& right) { - if (left.length() != right.length() || left.null_count() != right.null_count() || - left.type_id() != right.type_id()) { + const int64_t range_length = left_end_idx - left_start_idx; + DCHECK_GE(range_length, 0); + if (left_start_idx + range_length > left.length) { + // Left range too small return false; } - // ARROW-2567: Ensure that not only the type id but also the type equality - // itself is checked. - if (!TypeEquals(*left.type(), *right.type(), false /* check_metadata */)) { + if (right_start_idx + range_length > right.length) { + // Right range too small return false; } - if (left.null_count() > 0 && left.null_count() < left.length()) { - return BitmapEquals(left.null_bitmap()->data(), left.offset(), - right.null_bitmap()->data(), right.offset(), left.length()); - } - return true; -} - -template -inline bool ArrayEqualsImpl(const Array& left, const Array& right, Extra&&... extra) { - bool are_equal; - // The arrays are the same object - if (&left == &right) { - are_equal = true; - } else if (!BaseDataEquals(left, right)) { - are_equal = false; - } else if (left.length() == 0) { - are_equal = true; - } else if (left.null_count() == left.length()) { - are_equal = true; - } else { - VISITOR visitor(right, std::forward(extra)...); - auto error = VisitArrayInline(left, &visitor); - if (!error.ok()) { - DCHECK(false) << "Arrays are not comparable: " << error.ToString(); - } - are_equal = visitor.result(); + if (&left == &right && left_start_idx == right_start_idx) { + return true; } - return are_equal; + // Compare values + RangeDataEqualsImpl impl(options, floating_approximate, left, right, left_start_idx, + right_start_idx, range_length); + return impl.Compare(); } class TypeEqualsVisitor { @@ -1006,7 +746,11 @@ class ScalarEqualsVisitor { const EqualOptions options_; }; -Status PrintDiff(const Array& left, const Array& right, std::ostream* os) { +Status PrintDiff(const Array& left, const Array& right, std::ostream* os); + +Status PrintDiff(const Array& left, const Array& right, int64_t left_offset, + int64_t left_length, int64_t right_offset, int64_t right_length, + std::ostream* os) { if (os == nullptr) { return Status::OK(); } @@ -1039,48 +783,66 @@ Status PrintDiff(const Array& left, const Array& right, std::ostream* os) { return Status::OK(); } - ARROW_ASSIGN_OR_RAISE(auto edits, Diff(left, right, default_memory_pool())); + const auto left_slice = left.Slice(left_offset, left_length); + const auto right_slice = right.Slice(right_offset, right_length); + ARROW_ASSIGN_OR_RAISE(auto edits, + Diff(*left_slice, *right_slice, default_memory_pool())); ARROW_ASSIGN_OR_RAISE(auto formatter, MakeUnifiedDiffFormatter(*left.type(), os)); - return formatter(*edits, left, right); + return formatter(*edits, *left_slice, *right_slice); } -} // namespace +Status PrintDiff(const Array& left, const Array& right, std::ostream* os) { + return PrintDiff(left, right, 0, left.length(), 0, right.length(), os); +} -bool ArrayEquals(const Array& left, const Array& right, const EqualOptions& opts) { - bool are_equal = ArrayEqualsImpl(left, right, opts); +bool ArrayRangeEquals(const Array& left, const Array& right, int64_t left_start_idx, + int64_t left_end_idx, int64_t right_start_idx, + const EqualOptions& options, bool floating_approximate) { + bool are_equal = + CompareArrayRanges(*left.data(), *right.data(), left_start_idx, left_end_idx, + right_start_idx, options, floating_approximate); if (!are_equal) { - ARROW_IGNORE_EXPR(PrintDiff(left, right, opts.diff_sink())); + ARROW_IGNORE_EXPR(PrintDiff( + left, right, left_start_idx, left_end_idx, right_start_idx, + right_start_idx + (left_end_idx - left_start_idx), options.diff_sink())); } return are_equal; } -bool ArrayApproxEquals(const Array& left, const Array& right, const EqualOptions& opts) { - bool are_equal = ArrayEqualsImpl(left, right, opts); - if (!are_equal) { - DCHECK_OK(PrintDiff(left, right, opts.diff_sink())); +} // namespace + +bool ArrayRangeEquals(const Array& left, const Array& right, int64_t left_start_idx, + int64_t left_end_idx, int64_t right_start_idx, + const EqualOptions& options) { + const bool floating_approximate = false; + return ArrayRangeEquals(left, right, left_start_idx, left_end_idx, right_start_idx, + options, floating_approximate); +} + +bool ArrayRangeApproxEquals(const Array& left, const Array& right, int64_t left_start_idx, + int64_t left_end_idx, int64_t right_start_idx, + const EqualOptions& options) { + const bool floating_approximate = true; + return ArrayRangeEquals(left, right, left_start_idx, left_end_idx, right_start_idx, + options, floating_approximate); +} + +bool ArrayEquals(const Array& left, const Array& right, const EqualOptions& opts) { + if (left.length() != right.length()) { + ARROW_IGNORE_EXPR(PrintDiff(left, right, opts.diff_sink())); + return false; } - return are_equal; + const bool floating_approximate = false; + return ArrayRangeEquals(left, right, 0, left.length(), 0, opts, floating_approximate); } -bool ArrayRangeEquals(const Array& left, const Array& right, int64_t left_start_idx, - int64_t left_end_idx, int64_t right_start_idx) { - bool are_equal; - if (&left == &right) { - are_equal = true; - } else if (left.type_id() != right.type_id() || - !TypeEquals(*left.type(), *right.type(), false /* check_metadata */)) { - are_equal = false; - } else if (left.length() == 0) { - are_equal = true; - } else { - RangeEqualsVisitor visitor(right, left_start_idx, left_end_idx, right_start_idx); - auto error = VisitArrayInline(left, &visitor); - if (!error.ok()) { - DCHECK(false) << "Arrays are not comparable: " << error.ToString(); - } - are_equal = visitor.result(); +bool ArrayApproxEquals(const Array& left, const Array& right, const EqualOptions& opts) { + if (left.length() != right.length()) { + ARROW_IGNORE_EXPR(PrintDiff(left, right, opts.diff_sink())); + return false; } - return are_equal; + const bool floating_approximate = true; + return ArrayRangeEquals(left, right, 0, left.length(), 0, opts, floating_approximate); } namespace { diff --git a/cpp/src/arrow/compare.h b/cpp/src/arrow/compare.h index f7899b7c5c67..73a8401460b7 100644 --- a/cpp/src/arrow/compare.h +++ b/cpp/src/arrow/compare.h @@ -83,22 +83,29 @@ class EqualOptions { bool ARROW_EXPORT ArrayEquals(const Array& left, const Array& right, const EqualOptions& = EqualOptions::Defaults()); -bool ARROW_EXPORT TensorEquals(const Tensor& left, const Tensor& right, - const EqualOptions& = EqualOptions::Defaults()); - -/// EXPERIMENTAL: Returns true if the given sparse tensors are exactly equal -bool ARROW_EXPORT SparseTensorEquals(const SparseTensor& left, const SparseTensor& right, - const EqualOptions& = EqualOptions::Defaults()); - /// Returns true if the arrays are approximately equal. For non-floating point /// types, this is equivalent to ArrayEquals(left, right) bool ARROW_EXPORT ArrayApproxEquals(const Array& left, const Array& right, const EqualOptions& = EqualOptions::Defaults()); -/// Returns true if indicated equal-length segment of arrays is exactly equal +/// Returns true if indicated equal-length segment of arrays are exactly equal bool ARROW_EXPORT ArrayRangeEquals(const Array& left, const Array& right, int64_t start_idx, int64_t end_idx, - int64_t other_start_idx); + int64_t other_start_idx, + const EqualOptions& = EqualOptions::Defaults()); + +/// Returns true if indicated equal-length segment of arrays are approximately equal +bool ARROW_EXPORT ArrayRangeApproxEquals(const Array& left, const Array& right, + int64_t start_idx, int64_t end_idx, + int64_t other_start_idx, + const EqualOptions& = EqualOptions::Defaults()); + +bool ARROW_EXPORT TensorEquals(const Tensor& left, const Tensor& right, + const EqualOptions& = EqualOptions::Defaults()); + +/// EXPERIMENTAL: Returns true if the given sparse tensors are exactly equal +bool ARROW_EXPORT SparseTensorEquals(const SparseTensor& left, const SparseTensor& right, + const EqualOptions& = EqualOptions::Defaults()); /// Returns true if the type metadata are exactly equal /// \param[in] left a DataType diff --git a/cpp/src/arrow/compare_benchmark.cc b/cpp/src/arrow/compare_benchmark.cc new file mode 100644 index 000000000000..2699f90f69e1 --- /dev/null +++ b/cpp/src/arrow/compare_benchmark.cc @@ -0,0 +1,164 @@ +// 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 +#include +#include + +#include "benchmark/benchmark.h" + +#include "arrow/array.h" +#include "arrow/compare.h" +#include "arrow/testing/gtest_util.h" +#include "arrow/testing/random.h" +#include "arrow/util/benchmark_util.h" +#include "arrow/util/logging.h" +#include "arrow/util/macros.h" + +namespace arrow { + +constexpr auto kSeed = 0x94378165; + +static void BenchmarkArrayRangeEquals(const std::shared_ptr& array, + benchmark::State& state) { + const auto left_array = array; + // Make sure pointer equality can't be used as a shortcut + // (should we would deep-copy child_data and buffers?) + const auto right_array = + MakeArray(array->data()->Copy())->Slice(1, array->length() - 1); + + for (auto _ : state) { + const bool are_ok = ArrayRangeEquals(*left_array, *right_array, + /*left_start_idx=*/1, + /*left_end_idx=*/array->length() - 2, + /*right_start_idx=*/0); + if (ARROW_PREDICT_FALSE(!are_ok)) { + ARROW_LOG(FATAL) << "Arrays should have compared equal"; + } + } +} + +static void ArrayRangeEqualsInt32(benchmark::State& state) { + RegressionArgs args(state, /*size_is_bytes=*/false); + + auto rng = random::RandomArrayGenerator(kSeed); + auto array = rng.Int32(args.size, 0, 100, args.null_proportion); + + BenchmarkArrayRangeEquals(array, state); +} + +static void ArrayRangeEqualsFloat32(benchmark::State& state) { + RegressionArgs args(state, /*size_is_bytes=*/false); + + auto rng = random::RandomArrayGenerator(kSeed); + auto array = rng.Float32(args.size, 0, 100, args.null_proportion); + + BenchmarkArrayRangeEquals(array, state); +} + +static void ArrayRangeEqualsBoolean(benchmark::State& state) { + RegressionArgs args(state, /*size_is_bytes=*/false); + + auto rng = random::RandomArrayGenerator(kSeed); + auto array = rng.Boolean(args.size, /*true_probability=*/0.3, args.null_proportion); + + BenchmarkArrayRangeEquals(array, state); +} + +static void ArrayRangeEqualsString(benchmark::State& state) { + RegressionArgs args(state, /*size_is_bytes=*/false); + + auto rng = random::RandomArrayGenerator(kSeed); + auto array = + rng.String(args.size, /*min_length=*/0, /*max_length=*/15, args.null_proportion); + + BenchmarkArrayRangeEquals(array, state); +} + +static void ArrayRangeEqualsFixedSizeBinary(benchmark::State& state) { + RegressionArgs args(state, /*size_is_bytes=*/false); + + auto rng = random::RandomArrayGenerator(kSeed); + auto array = rng.FixedSizeBinary(args.size, /*byte_width=*/8, args.null_proportion); + + BenchmarkArrayRangeEquals(array, state); +} + +static void ArrayRangeEqualsListOfInt32(benchmark::State& state) { + RegressionArgs args(state, /*size_is_bytes=*/false); + + auto rng = random::RandomArrayGenerator(kSeed); + auto values = rng.Int32(args.size * 10, 0, 100, args.null_proportion); + // Force empty list null entries, since it is overwhelmingly the common case. + auto array = rng.List(*values, /*size=*/args.size, args.null_proportion, + /*force_empty_nulls=*/true); + + BenchmarkArrayRangeEquals(array, state); +} + +static void ArrayRangeEqualsStruct(benchmark::State& state) { + // struct + RegressionArgs args(state, /*size_is_bytes=*/false); + + auto rng = random::RandomArrayGenerator(kSeed); + auto values1 = rng.Int32(args.size, 0, 100, args.null_proportion); + auto values2 = + rng.String(args.size, /*min_length=*/0, /*max_length=*/15, args.null_proportion); + auto null_bitmap = rng.NullBitmap(args.size, args.null_proportion); + auto array = *StructArray::Make({values1, values2}, + std::vector{"ints", "strs"}, null_bitmap); + + BenchmarkArrayRangeEquals(array, state); +} + +static void ArrayRangeEqualsSparseUnion(benchmark::State& state) { + // sparse_union + RegressionArgs args(state, /*size_is_bytes=*/false); + + auto rng = random::RandomArrayGenerator(kSeed); + auto values1 = rng.Int32(args.size, 0, 100, args.null_proportion); + auto values2 = + rng.String(args.size, /*min_length=*/0, /*max_length=*/15, args.null_proportion); + auto array = rng.SparseUnion({values1, values2}, args.size); + + BenchmarkArrayRangeEquals(array, state); +} + +static void ArrayRangeEqualsDenseUnion(benchmark::State& state) { + // dense_union + RegressionArgs args(state, /*size_is_bytes=*/false); + + auto rng = random::RandomArrayGenerator(kSeed); + auto values1 = rng.Int32(args.size, 0, 100, args.null_proportion); + auto values2 = + rng.String(args.size, /*min_length=*/0, /*max_length=*/15, args.null_proportion); + auto array = rng.DenseUnion({values1, values2}, args.size); + + BenchmarkArrayRangeEquals(array, state); +} + +BENCHMARK(ArrayRangeEqualsInt32)->Apply(RegressionSetArgs); +BENCHMARK(ArrayRangeEqualsFloat32)->Apply(RegressionSetArgs); +BENCHMARK(ArrayRangeEqualsBoolean)->Apply(RegressionSetArgs); +BENCHMARK(ArrayRangeEqualsString)->Apply(RegressionSetArgs); +BENCHMARK(ArrayRangeEqualsFixedSizeBinary)->Apply(RegressionSetArgs); +BENCHMARK(ArrayRangeEqualsListOfInt32)->Apply(RegressionSetArgs); +BENCHMARK(ArrayRangeEqualsStruct)->Apply(RegressionSetArgs); +BENCHMARK(ArrayRangeEqualsSparseUnion)->Apply(RegressionSetArgs); +BENCHMARK(ArrayRangeEqualsDenseUnion)->Apply(RegressionSetArgs); + +} // namespace arrow diff --git a/cpp/src/arrow/compute/kernels/aggregate_benchmark.cc b/cpp/src/arrow/compute/kernels/aggregate_benchmark.cc index 5b95d7b526a4..4de5764c7a12 100644 --- a/cpp/src/arrow/compute/kernels/aggregate_benchmark.cc +++ b/cpp/src/arrow/compute/kernels/aggregate_benchmark.cc @@ -20,11 +20,11 @@ #include #include "arrow/compute/api.h" -#include "arrow/memory_pool.h" #include "arrow/testing/gtest_util.h" #include "arrow/testing/random.h" #include "arrow/util/benchmark_util.h" #include "arrow/util/bit_util.h" +#include "arrow/util/bitmap_reader.h" namespace arrow { namespace compute { diff --git a/cpp/src/arrow/compute/kernels/aggregate_test.cc b/cpp/src/arrow/compute/kernels/aggregate_test.cc index 4d9526cbf56d..ad2633b4f97a 100644 --- a/cpp/src/arrow/compute/kernels/aggregate_test.cc +++ b/cpp/src/arrow/compute/kernels/aggregate_test.cc @@ -31,6 +31,7 @@ #include "arrow/compute/kernels/test_util.h" #include "arrow/type.h" #include "arrow/type_traits.h" +#include "arrow/util/bitmap_reader.h" #include "arrow/util/checked_cast.h" #include "arrow/testing/gtest_common.h" diff --git a/cpp/src/arrow/ipc/feather_test.cc b/cpp/src/arrow/ipc/feather_test.cc index a7ee31f18c17..93387da4dfa6 100644 --- a/cpp/src/arrow/ipc/feather_test.cc +++ b/cpp/src/arrow/ipc/feather_test.cc @@ -286,10 +286,8 @@ TEST_P(TestFeather, PrimitiveNullRoundTrip) { std::vector> expected_fields; for (int i = 0; i < batch->num_columns(); ++i) { ASSERT_EQ(batch->column_name(i), reader_->schema()->field(i)->name()); - StringArray str_values(batch->column(i)->length(), nullptr, nullptr, - batch->column(i)->null_bitmap(), - batch->column(i)->null_count()); - AssertArraysEqual(str_values, *result->column(i)->chunk(0)); + ASSERT_OK_AND_ASSIGN(auto expected, MakeArrayOfNull(utf8(), batch->num_rows())); + AssertArraysEqual(*expected, *result->column(i)->chunk(0)); } } else { AssertTablesEqual(*table, *result); diff --git a/cpp/src/arrow/testing/random.cc b/cpp/src/arrow/testing/random.cc index 32007f810e97..5d2525041c14 100644 --- a/cpp/src/arrow/testing/random.cc +++ b/cpp/src/arrow/testing/random.cc @@ -25,6 +25,7 @@ #include #include "arrow/array.h" +#include "arrow/array/builder_primitive.h" #include "arrow/buffer.h" #include "arrow/testing/gtest_util.h" #include "arrow/type.h" @@ -32,9 +33,13 @@ #include "arrow/type_traits.h" #include "arrow/util/bit_util.h" #include "arrow/util/bitmap_reader.h" +#include "arrow/util/checked_cast.h" #include "arrow/util/logging.h" namespace arrow { + +using internal::checked_cast; + namespace random { namespace { @@ -267,6 +272,29 @@ std::shared_ptr RandomArrayGenerator::StringWithRepeats(int64_t size, return result; } +std::shared_ptr RandomArrayGenerator::FixedSizeBinary(int64_t size, + int32_t byte_width, + double null_probability) { + if (null_probability < 0 || null_probability > 1) { + ABORT_NOT_OK(Status::Invalid("null_probability must be between 0 and 1")); + } + + // Visual Studio does not implement uniform_int_distribution for char types. + using GenOpt = GenerateOptions>; + GenOpt options(seed(), static_cast('A'), static_cast('z'), + null_probability); + + int64_t null_count = 0; + auto null_bitmap = *AllocateEmptyBitmap(size); + auto data_buffer = *AllocateBuffer(size * byte_width); + options.GenerateBitmap(null_bitmap->mutable_data(), size, &null_count); + options.GenerateData(data_buffer->mutable_data(), size * byte_width); + + auto type = fixed_size_binary(byte_width); + return std::make_shared(type, size, std::move(data_buffer), + std::move(null_bitmap), null_count); +} + std::shared_ptr RandomArrayGenerator::Offsets(int64_t size, int32_t first_offset, int32_t last_offset, double null_probability, @@ -321,6 +349,43 @@ std::shared_ptr RandomArrayGenerator::List(const Array& values, int64_t s return *::arrow::ListArray::FromArrays(*offsets, values); } +std::shared_ptr RandomArrayGenerator::SparseUnion(const ArrayVector& fields, + int64_t size) { + DCHECK_GT(fields.size(), 0); + // Trivial type codes map + std::vector type_codes(fields.size()); + std::iota(type_codes.begin(), type_codes.end(), 0); + + // Generate array of type ids + auto type_ids = Int8(size, 0, static_cast(fields.size() - 1)); + return *SparseUnionArray::Make(*type_ids, fields, type_codes); +} + +std::shared_ptr RandomArrayGenerator::DenseUnion(const ArrayVector& fields, + int64_t size) { + DCHECK_GT(fields.size(), 0); + // Trivial type codes map + std::vector type_codes(fields.size()); + std::iota(type_codes.begin(), type_codes.end(), 0); + + // Generate array of type ids + auto type_ids = Int8(size, 0, static_cast(fields.size() - 1)); + + // Generate array of offsets + const auto& concrete_ids = checked_cast(*type_ids); + Int32Builder offsets_builder; + ABORT_NOT_OK(offsets_builder.Reserve(size)); + std::vector last_offsets(fields.size(), 0); + for (int64_t i = 0; i < size; ++i) { + const auto field_id = concrete_ids.Value(i); + offsets_builder.UnsafeAppend(last_offsets[field_id]++); + } + std::shared_ptr offsets; + ABORT_NOT_OK(offsets_builder.Finish(&offsets)); + + return *DenseUnionArray::Make(*type_ids, *offsets, fields, type_codes); +} + namespace { struct RandomArrayGeneratorOfImpl { diff --git a/cpp/src/arrow/testing/random.h b/cpp/src/arrow/testing/random.h index e2ec405543f2..a1a16c07cf2c 100644 --- a/cpp/src/arrow/testing/random.h +++ b/cpp/src/arrow/testing/random.h @@ -283,6 +283,16 @@ class ARROW_TESTING_EXPORT RandomArrayGenerator { int32_t min_length, int32_t max_length, double null_probability = 0); + /// \brief Generate a random FixedSizeBinaryArray + /// + /// \param[in] size the size of the array to generate + /// \param[in] byte_width the byte width of fixed-size binary items + /// \param[in] null_probability the probability of a row being null + /// + /// \return a generated Array + std::shared_ptr FixedSizeBinary(int64_t size, int32_t byte_width, + double null_probability = 0); + /// \brief Generate a random ListArray /// /// \param[in] values The underlying values array @@ -294,6 +304,25 @@ class ARROW_TESTING_EXPORT RandomArrayGenerator { std::shared_ptr List(const Array& values, int64_t size, double null_probability, bool force_empty_nulls = false); + /// \brief Generate a random SparseUnionArray + /// + /// The type ids are chosen randomly, according to a uniform distribution, + /// amongst the given child fields. + /// + /// \param[in] fields Vector of Arrays containing the data for each union field + /// \param[in] size The size of the generated sparse union array + std::shared_ptr SparseUnion(const ArrayVector& fields, int64_t size); + + /// \brief Generate a random DenseUnionArray + /// + /// The type ids are chosen randomly, according to a uniform distribution, + /// amongst the given child fields. The offsets are incremented along + /// each child field. + /// + /// \param[in] fields Vector of Arrays containing the data for each union field + /// \param[in] size The size of the generated sparse union array + std::shared_ptr DenseUnion(const ArrayVector& fields, int64_t size); + /// \brief Generate a random Array of the specified type, size, and null_probability. /// /// Generation parameters other than size and null_probability are determined based on diff --git a/cpp/src/arrow/util/bit_run_reader.cc b/cpp/src/arrow/util/bit_run_reader.cc index da5ba649dd80..eda6088eb325 100644 --- a/cpp/src/arrow/util/bit_run_reader.cc +++ b/cpp/src/arrow/util/bit_run_reader.cc @@ -45,7 +45,7 @@ BitRunReader::BitRunReader(const uint8_t* bitmap, int64_t start_offset, int64_t // Prepare for inversion in NextRun. // Clear out any preceding bits. - word_ = word_ & ~BitUtil::LeastSignficantBitMask(position_); + word_ = word_ & ~BitUtil::LeastSignificantBitMask(position_); } #endif diff --git a/cpp/src/arrow/util/bit_run_reader.h b/cpp/src/arrow/util/bit_run_reader.h index 6dba30e0bc21..893f4f249845 100644 --- a/cpp/src/arrow/util/bit_run_reader.h +++ b/cpp/src/arrow/util/bit_run_reader.h @@ -40,10 +40,14 @@ struct BitRun { } }; -static inline bool operator==(const BitRun& lhs, const BitRun& rhs) { +inline bool operator==(const BitRun& lhs, const BitRun& rhs) { return lhs.length == rhs.length && lhs.set == rhs.set; } +inline bool operator!=(const BitRun& lhs, const BitRun& rhs) { + return lhs.length != rhs.length || lhs.set != rhs.set; +} + class BitRunReaderLinear { public: BitRunReaderLinear(const uint8_t* bitmap, int64_t start_offset, int64_t length) @@ -96,7 +100,7 @@ class ARROW_EXPORT BitRunReader { int64_t start_bit_offset = start_position & 63; // Invert the word for proper use of CountTrailingZeros and // clear bits so CountTrailingZeros can do it magic. - word_ = ~word_ & ~BitUtil::LeastSignficantBitMask(start_bit_offset); + word_ = ~word_ & ~BitUtil::LeastSignificantBitMask(start_bit_offset); // Go forward until the next change from unset to set. int64_t new_bits = BitUtil::CountTrailingZeros(word_) - start_bit_offset; @@ -162,5 +166,7 @@ class ARROW_EXPORT BitRunReader { using BitRunReader = BitRunReaderLinear; #endif +// TODO SetBitRunReader? + } // namespace internal } // namespace arrow diff --git a/cpp/src/arrow/util/bit_util.h b/cpp/src/arrow/util/bit_util.h index 3fe24b46412c..74f7e61e9cc1 100644 --- a/cpp/src/arrow/util/bit_util.h +++ b/cpp/src/arrow/util/bit_util.h @@ -141,7 +141,7 @@ constexpr bool IsMultipleOf8(int64_t n) { return (n & 7) == 0; } // Returns a mask for the bit_index lower order bits. // Only valid for bit_index in the range [0, 64). -constexpr uint64_t LeastSignficantBitMask(int64_t bit_index) { +constexpr uint64_t LeastSignificantBitMask(int64_t bit_index) { return (static_cast(1) << bit_index) - 1; } diff --git a/cpp/src/arrow/util/bit_util_test.cc b/cpp/src/arrow/util/bit_util_test.cc index ef6f1254348a..d9d775c73247 100644 --- a/cpp/src/arrow/util/bit_util_test.cc +++ b/cpp/src/arrow/util/bit_util_test.cc @@ -33,6 +33,7 @@ #include "arrow/array/array_base.h" #include "arrow/array/data.h" #include "arrow/buffer.h" +#include "arrow/buffer_builder.h" #include "arrow/result.h" #include "arrow/status.h" #include "arrow/testing/gtest_common.h" @@ -86,6 +87,31 @@ void BitmapFromVector(const std::vector& values, int64_t bit_offset, WriteVectorToWriter(writer, values); } +std::shared_ptr BitmapFromString(const std::string& s) { + TypedBufferBuilder builder; + ABORT_NOT_OK(builder.Reserve(s.size())); + for (const char c : s) { + switch (c) { + case '0': + builder.UnsafeAppend(false); + break; + case '1': + builder.UnsafeAppend(true); + break; + case ' ': + case '\t': + case '\n': + case '\r': + break; + default: + ARROW_LOG(FATAL) << "Unexpected character in bitmap string"; + } + } + std::shared_ptr buffer; + ABORT_NOT_OK(builder.Finish(&buffer)); + return buffer; +} + #define ASSERT_READER_SET(reader) \ do { \ ASSERT_TRUE(reader.IsSet()); \ @@ -201,6 +227,121 @@ TEST(BitmapReader, DoesNotReadOutOfBounds) { internal::BitmapReader r3(nullptr, 0, 0); } +class TestBitmapUInt64Reader : public ::testing::Test { + public: + void AssertWords(const Buffer& buffer, int64_t start_offset, int64_t length, + const std::vector& expected) { + internal::BitmapUInt64Reader reader(buffer.data(), start_offset, length); + ASSERT_EQ(reader.position(), 0); + ASSERT_EQ(reader.length(), length); + for (const uint64_t word : expected) { + ASSERT_EQ(reader.NextWord(), word); + } + ASSERT_EQ(reader.position(), length); + } + + void Check(const Buffer& buffer, int64_t start_offset, int64_t length) { + internal::BitmapUInt64Reader reader(buffer.data(), start_offset, length); + for (int64_t i = 0; i < length; i += 64) { + ASSERT_EQ(reader.position(), i); + const auto nbits = std::min(64, length - i); + uint64_t word = reader.NextWord(); + for (int64_t j = 0; j < nbits; ++j) { + ASSERT_EQ(word & 1, BitUtil::GetBit(buffer.data(), start_offset + i + j)); + word >>= 1; + } + } + ASSERT_EQ(reader.position(), length); + } + + void CheckExtensive(const Buffer& buffer) { + for (const int64_t offset : kTestOffsets) { + for (int64_t length : kTestOffsets) { + if (offset + length <= buffer.size()) { + Check(buffer, offset, length); + length = buffer.size() - offset - length; + if (offset + length <= buffer.size()) { + Check(buffer, offset, length); + } + } + } + } + } + + protected: + const std::vector kTestOffsets = {0, 1, 6, 7, 8, 33, 62, 63, 64, 65}; +}; + +TEST_F(TestBitmapUInt64Reader, Empty) { + for (const int64_t offset : kTestOffsets) { + // Does not access invalid memory + internal::BitmapUInt64Reader reader(nullptr, offset, 0); + ASSERT_EQ(reader.position(), 0); + ASSERT_EQ(reader.length(), 0); + } +} + +TEST_F(TestBitmapUInt64Reader, Small) { + auto buffer = BitmapFromString( + "11111111 10000000 00000000 00000000 00000000 00000000 00000001 11111111" + "11111111 10000000 00000000 00000000 00000000 00000000 00000001 11111111" + "11111111 10000000 00000000 00000000 00000000 00000000 00000001 11111111" + "11111111 10000000 00000000 00000000 00000000 00000000 00000001 11111111"); + + // One word + AssertWords(*buffer, 0, 9, {0x1ff}); + AssertWords(*buffer, 1, 9, {0xff}); + AssertWords(*buffer, 7, 9, {0x3}); + AssertWords(*buffer, 8, 9, {0x1}); + AssertWords(*buffer, 9, 9, {0x0}); + + AssertWords(*buffer, 54, 10, {0x3fe}); + AssertWords(*buffer, 54, 9, {0x1fe}); + AssertWords(*buffer, 54, 8, {0xfe}); + + AssertWords(*buffer, 55, 9, {0x1ff}); + AssertWords(*buffer, 56, 8, {0xff}); + AssertWords(*buffer, 57, 7, {0x7f}); + AssertWords(*buffer, 63, 1, {0x1}); + + AssertWords(*buffer, 0, 64, {0xff800000000001ffULL}); + + // One straddling word + AssertWords(*buffer, 54, 12, {0xffe}); + AssertWords(*buffer, 63, 2, {0x3}); + + // One word (start_offset >= 64) + AssertWords(*buffer, 96, 64, {0x000001ffff800000ULL}); + + // Two words + AssertWords(*buffer, 0, 128, {0xff800000000001ffULL, 0xff800000000001ffULL}); + AssertWords(*buffer, 0, 127, {0xff800000000001ffULL, 0x7f800000000001ffULL}); + AssertWords(*buffer, 1, 127, {0xffc00000000000ffULL, 0x7fc00000000000ffULL}); + AssertWords(*buffer, 1, 128, {0xffc00000000000ffULL, 0xffc00000000000ffULL}); + AssertWords(*buffer, 63, 128, {0xff000000000003ffULL, 0xff000000000003ffULL}); + AssertWords(*buffer, 63, 65, {0xff000000000003ffULL, 0x1}); + + // More than two words + AssertWords(*buffer, 0, 256, + {0xff800000000001ffULL, 0xff800000000001ffULL, 0xff800000000001ffULL, + 0xff800000000001ffULL}); + AssertWords(*buffer, 1, 255, + {0xffc00000000000ffULL, 0xffc00000000000ffULL, 0xffc00000000000ffULL, + 0x7fc00000000000ffULL}); + AssertWords(*buffer, 63, 193, + {0xff000000000003ffULL, 0xff000000000003ffULL, 0xff000000000003ffULL, 0x1}); + AssertWords(*buffer, 63, 192, + {0xff000000000003ffULL, 0xff000000000003ffULL, 0xff000000000003ffULL}); + + CheckExtensive(*buffer); +} + +TEST_F(TestBitmapUInt64Reader, Random) { + random::RandomArrayGenerator rng(42); + auto buffer = rng.NullBitmap(500, 0.5); + CheckExtensive(*buffer); +} + namespace internal { void PrintTo(const internal::BitRun& run, std::ostream* os) { *os << run.ToString(); // whatever needed to print bar to os diff --git a/cpp/src/arrow/util/bitmap.h b/cpp/src/arrow/util/bitmap.h index c98b0c694845..a6b948c79b49 100644 --- a/cpp/src/arrow/util/bitmap.h +++ b/cpp/src/arrow/util/bitmap.h @@ -110,8 +110,8 @@ class ARROW_EXPORT Bitmap : public util::ToStringOstreamable, /// /// TODO(bkietz) allow for early termination template ::value_type> + typename Word = typename std::decay< + internal::call_traits::argument_type<0, Visitor&&>>::type::value_type> static int64_t VisitWords(const Bitmap (&bitmaps_arg)[N], Visitor&& visitor) { constexpr int64_t kBitWidth = sizeof(Word) * 8; diff --git a/cpp/src/arrow/util/bitmap_generate.h b/cpp/src/arrow/util/bitmap_generate.h index e160dce337e5..5a146f64db3e 100644 --- a/cpp/src/arrow/util/bitmap_generate.h +++ b/cpp/src/arrow/util/bitmap_generate.h @@ -24,7 +24,6 @@ #include "arrow/memory_pool.h" #include "arrow/result.h" #include "arrow/util/bit_util.h" -#include "arrow/util/bitmap_reader.h" #include "arrow/util/visibility.h" namespace arrow { diff --git a/cpp/src/arrow/util/bitmap_ops.cc b/cpp/src/arrow/util/bitmap_ops.cc index eb2aec73e811..87f6a0633a49 100644 --- a/cpp/src/arrow/util/bitmap_ops.cc +++ b/cpp/src/arrow/util/bitmap_ops.cc @@ -429,6 +429,26 @@ bool BitmapEquals(const uint8_t* left, int64_t left_offset, const uint8_t* right return true; } +bool OptionalBitmapEquals(const uint8_t* left, int64_t left_offset, const uint8_t* right, + int64_t right_offset, int64_t length) { + if (left == nullptr && right == nullptr) { + return true; + } else if (left != nullptr && right != nullptr) { + return BitmapEquals(left, left_offset, right, right_offset, length); + } else if (left != nullptr) { + return CountSetBits(left, left_offset, length) == length; + } else { + return CountSetBits(right, right_offset, length) == length; + } +} + +bool OptionalBitmapEquals(const std::shared_ptr& left, int64_t left_offset, + const std::shared_ptr& right, int64_t right_offset, + int64_t length) { + return OptionalBitmapEquals(left ? left->data() : nullptr, left_offset, + right ? right->data() : nullptr, right_offset, length); +} + namespace { template