From c78348cc18adba2a721d174d21630908758c0473 Mon Sep 17 00:00:00 2001 From: William Dealtry Date: Mon, 16 Dec 2024 18:20:21 +0000 Subject: [PATCH 1/5] Refactor aggregator set data, add statistics --- cpp/arcticdb/CMakeLists.txt | 7 +- cpp/arcticdb/codec/codec.cpp | 3 + cpp/arcticdb/codec/encode_v1.cpp | 4 +- cpp/arcticdb/codec/encode_v2.cpp | 6 +- cpp/arcticdb/codec/encoded_field.hpp | 11 +- cpp/arcticdb/codec/protobuf_mappings.cpp | 5 + cpp/arcticdb/codec/test/test_codec.cpp | 80 ++++ cpp/arcticdb/column_store/column.hpp | 14 + cpp/arcticdb/column_store/memory_segment.hpp | 4 + .../column_store/memory_segment_impl.cpp | 14 + .../column_store/memory_segment_impl.hpp | 2 + cpp/arcticdb/column_store/statistics.hpp | 211 +++++++++ .../column_store/test/test_column.cpp | 113 +++++ .../column_store/test/test_statistics.cpp | 222 ++++++++++ cpp/arcticdb/entity/protobuf_mappings.cpp | 14 +- cpp/arcticdb/entity/types_proto.cpp | 11 - cpp/arcticdb/entity/types_proto.hpp | 10 - cpp/arcticdb/pipeline/frame_utils.hpp | 417 ++++++++++-------- cpp/arcticdb/pipeline/write_frame.cpp | 4 + cpp/arcticdb/storage/memory_layout.hpp | 37 +- cpp/arcticdb/storage/storages.hpp | 2 +- cpp/arcticdb/stream/protobuf_mappings.cpp | 57 ++- cpp/arcticdb/stream/protobuf_mappings.hpp | 7 + .../stream/test/test_protobuf_mappings.cpp | 69 +++ .../version/test/test_version_store.cpp | 2 +- cpp/arcticdb/version/version_store_api.hpp | 7 +- cpp/proto/arcticc/pb2/encoding.proto | 16 + 27 files changed, 1116 insertions(+), 233 deletions(-) create mode 100644 cpp/arcticdb/column_store/statistics.hpp create mode 100644 cpp/arcticdb/column_store/test/test_statistics.cpp create mode 100644 cpp/arcticdb/stream/test/test_protobuf_mappings.cpp diff --git a/cpp/arcticdb/CMakeLists.txt b/cpp/arcticdb/CMakeLists.txt index 9b88cf8acf..f236731c33 100644 --- a/cpp/arcticdb/CMakeLists.txt +++ b/cpp/arcticdb/CMakeLists.txt @@ -416,6 +416,7 @@ set(arcticdb_srcs column_store/key_segment.cpp column_store/memory_segment_impl.cpp column_store/memory_segment_impl.cpp + column_store/statistics.hpp column_store/string_pool.cpp entity/data_error.cpp entity/field_collection.cpp @@ -910,6 +911,7 @@ if(${TEST}) column_store/test/test_column_data_random_accessor.cpp column_store/test/test_index_filtering.cpp column_store/test/test_memory_segment.cpp + column_store/test/test_statistics.cpp entity/test/test_atom_key.cpp entity/test/test_key_serialization.cpp entity/test/test_metrics.cpp @@ -938,10 +940,12 @@ if(${TEST}) storage/test/test_storage_factory.cpp storage/test/test_storage_exceptions.cpp storage/test/test_azure_storage.cpp + storage/test/common.hpp storage/test/test_storage_operations.cpp stream/test/stream_test_common.cpp stream/test/test_aggregator.cpp stream/test/test_append_map.cpp + stream/test/test_protobuf_mappings.cpp stream/test/test_row_builder.cpp stream/test/test_segment_aggregator.cpp stream/test/test_types.cpp @@ -976,8 +980,7 @@ if(${TEST}) version/test/test_version_map_batch.cpp version/test/test_version_store.cpp version/test/version_map_model.hpp - python/python_handlers.cpp - storage/test/common.hpp) + python/python_handlers.cpp) set(EXECUTABLE_PERMS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) # 755 diff --git a/cpp/arcticdb/codec/codec.cpp b/cpp/arcticdb/codec/codec.cpp index 30f3c0e8d7..99c0839c4e 100644 --- a/cpp/arcticdb/codec/codec.cpp +++ b/cpp/arcticdb/codec/codec.cpp @@ -467,6 +467,8 @@ void decode_v2(const Segment& segment, auto& col = res.column(static_cast(*col_index)); data += decode_field(res.field(*col_index).type(), *encoded_field, data, col, col.opt_sparse_map(), hdr.encoding_version()); + col.set_statistics(encoded_field->get_statistics()); + seg_row_count = std::max(seg_row_count, calculate_last_row(col)); } else { data += encoding_sizes::field_compressed_size(*encoded_field) + sizeof(ColumnMagic); @@ -533,6 +535,7 @@ void decode_v1(const Segment& segment, hdr.encoding_version() ); seg_row_count = std::max(seg_row_count, calculate_last_row(col)); + col.set_statistics(field.get_statistics()); ARCTICDB_TRACE(log::codec(), "Decoded column {} to position {}", i, data - begin); } else { data += encoding_sizes::field_compressed_size(field); diff --git a/cpp/arcticdb/codec/encode_v1.cpp b/cpp/arcticdb/codec/encode_v1.cpp index a711d4c922..8ad187b12f 100644 --- a/cpp/arcticdb/codec/encode_v1.cpp +++ b/cpp/arcticdb/codec/encode_v1.cpp @@ -137,7 +137,8 @@ namespace arcticdb { encoded_fields.reserve(encoded_buffer_size, in_mem_seg.num_columns()); ARCTICDB_TRACE(log::codec(), "Encoding fields"); for (std::size_t column_index = 0; column_index < in_mem_seg.num_columns(); ++column_index) { - auto column_data = in_mem_seg.column_data(column_index); + const auto& column = in_mem_seg.column(column_index); + auto column_data = column.data(); auto* column_field = encoded_fields.add_field(column_data.num_blocks()); if(column_data.num_blocks() > 0) { encoder.encode(codec_opts, column_data, *column_field, *out_buffer, pos); @@ -147,6 +148,7 @@ namespace arcticdb { auto* ndarray = column_field->mutable_ndarray(); ndarray->set_items_count(0); } + column_field->set_statistics(column.get_statistics()); } encode_string_pool(in_mem_seg, segment_header, codec_opts, *out_buffer, pos); } diff --git a/cpp/arcticdb/codec/encode_v2.cpp b/cpp/arcticdb/codec/encode_v2.cpp index d15c943524..2a926350f2 100644 --- a/cpp/arcticdb/codec/encode_v2.cpp +++ b/cpp/arcticdb/codec/encode_v2.cpp @@ -356,8 +356,12 @@ static void encode_encoded_fields( ARCTICDB_TRACE(log::codec(), "Encoding fields"); for (std::size_t column_index = 0; column_index < in_mem_seg.num_columns(); ++column_index) { write_magic(*out_buffer, pos); - auto column_data = in_mem_seg.column_data(column_index); + const auto& column = in_mem_seg.column(column_index); + auto column_data = column.data(); auto* column_field = encoded_fields.add_field(column_data.num_blocks()); + if(column.has_statistics()) + column_field->set_statistics(column.get_statistics()); + ARCTICDB_TRACE(log::codec(),"Beginning encoding of column {}: ({}) to position {}", column_index, in_mem_seg.descriptor().field(column_index).name(), pos); if(column_data.num_blocks() > 0) { diff --git a/cpp/arcticdb/codec/encoded_field.hpp b/cpp/arcticdb/codec/encoded_field.hpp index d0f93cf1f4..a7263bfcf6 100644 --- a/cpp/arcticdb/codec/encoded_field.hpp +++ b/cpp/arcticdb/codec/encoded_field.hpp @@ -163,7 +163,8 @@ struct EncodedFieldImpl : public EncodedField { sizeof(values_count_) + sizeof(sparse_map_bytes_) + sizeof(items_count_) + - sizeof(format_); + sizeof(format_) + + sizeof(stats_); EncodedFieldImpl() = default; @@ -366,6 +367,14 @@ struct EncodedFieldImpl : public EncodedField { sparse_map_bytes_ = bytes; } + void set_statistics(FieldStats stats) { + stats_ = stats; + } + + FieldStats get_statistics() const { + return stats_; + } + EncodedBlock *add_values(EncodingVersion encoding_version) { const bool old_style = encoding_version == EncodingVersion::V1; size_t pos; diff --git a/cpp/arcticdb/codec/protobuf_mappings.cpp b/cpp/arcticdb/codec/protobuf_mappings.cpp index 4b21907f11..71217fda4b 100644 --- a/cpp/arcticdb/codec/protobuf_mappings.cpp +++ b/cpp/arcticdb/codec/protobuf_mappings.cpp @@ -10,6 +10,7 @@ #include "arcticdb/storage/memory_layout.hpp" #include #include +#include #include namespace arcticdb { @@ -78,6 +79,8 @@ void encoded_field_from_proto(const arcticdb::proto::encoding::EncodedField& inp auto* value_block = output_ndarray->add_values(EncodingVersion::V1); block_from_proto(input_ndarray.values(i), *value_block, false); } + + output.set_statistics(create_from_proto(input.stats())); } void copy_encoded_field_to_proto(const EncodedFieldImpl& input, arcticdb::proto::encoding::EncodedField& output) { @@ -97,6 +100,8 @@ void copy_encoded_field_to_proto(const EncodedFieldImpl& input, arcticdb::proto: auto* value_block = output_ndarray->add_values(); proto_from_block(input_ndarray.values(i), *value_block); } + + field_stats_to_proto(input.get_statistics(), *output.mutable_stats()); } size_t num_blocks(const arcticdb::proto::encoding::EncodedField& field) { diff --git a/cpp/arcticdb/codec/test/test_codec.cpp b/cpp/arcticdb/codec/test/test_codec.cpp index dc8694b854..3d40c5b826 100644 --- a/cpp/arcticdb/codec/test/test_codec.cpp +++ b/cpp/arcticdb/codec/test/test_codec.cpp @@ -512,6 +512,86 @@ TEST(Segment, RoundtripTimeseriesDescriptorWriteToBufferV2) { ASSERT_EQ(decoded, copy); } +TEST(Segment, RoundtripStatisticsV1) { + ScopedConfig reload_interval("Statistics.GenerateOnWrite", 1); + const auto stream_desc = stream_descriptor(StreamId{"thing"}, RowCountIndex{}, { + scalar_field(DataType::UINT8, "int8"), + scalar_field(DataType::FLOAT64, "doubles") + }); + + SegmentInMemory in_mem_seg{stream_desc.clone()}; + constexpr size_t num_rows = 10; + for(auto i = 0UL; i < num_rows; ++i) { + in_mem_seg.set_scalar(0, static_cast(i)); + in_mem_seg.set_scalar(1, static_cast(i * 2)); + in_mem_seg.end_row(); + } + in_mem_seg.calculate_statistics(); + auto copy = in_mem_seg.clone(); + auto seg = encode_v1(std::move(in_mem_seg), codec::default_lz4_codec()); + std::vector vec; + const auto bytes = seg.calculate_size(); + vec.resize(bytes); + seg.write_to(vec.data()); + auto unserialized = Segment::from_bytes(vec.data(), bytes); + SegmentInMemory decoded{stream_desc.clone()}; + decode_v1(unserialized, unserialized.header(), decoded, unserialized.descriptor()); + auto col1_stats = decoded.column(0).get_statistics(); + ASSERT_TRUE(col1_stats.has_max()); + ASSERT_EQ(col1_stats.get_max(), 9); + ASSERT_TRUE(col1_stats.has_max()); + ASSERT_EQ(col1_stats.get_min(), 0); + ASSERT_TRUE(col1_stats.has_unique()); + ASSERT_EQ(col1_stats.get_unique_count(), 10); + auto col2_stats = decoded.column(1).get_statistics(); + ASSERT_TRUE(col2_stats.has_max()); + ASSERT_EQ(col2_stats.get_max(), 18.0); + ASSERT_TRUE(col2_stats.has_max()); + ASSERT_EQ(col2_stats.get_min(), 0); + ASSERT_TRUE(col2_stats.has_unique()); + ASSERT_EQ(col2_stats.get_unique_count(), 10); +} + +TEST(Segment, RoundtripStatisticsV2) { + ScopedConfig reload_interval("Statistics.GenerateOnWrite", 1); + const auto stream_desc = stream_descriptor(StreamId{"thing"}, RowCountIndex{}, { + scalar_field(DataType::UINT8, "int8"), + scalar_field(DataType::FLOAT64, "doubles") + }); + + SegmentInMemory in_mem_seg{stream_desc.clone()}; + constexpr size_t num_rows = 10; + for(auto i = 0UL; i < num_rows; ++i) { + in_mem_seg.set_scalar(0, static_cast(i)); + in_mem_seg.set_scalar(1, static_cast(i * 2)); + in_mem_seg.end_row(); + } + in_mem_seg.calculate_statistics(); + auto copy = in_mem_seg.clone(); + auto seg = encode_v2(std::move(in_mem_seg), codec::default_lz4_codec()); + std::vector vec; + const auto bytes = seg.calculate_size(); + vec.resize(bytes); + seg.write_to(vec.data()); + auto unserialized = Segment::from_bytes(vec.data(), bytes); + SegmentInMemory decoded{stream_desc.clone()}; + decode_v2(unserialized, unserialized.header(), decoded, unserialized.descriptor()); + auto col1_stats = decoded.column(0).get_statistics(); + ASSERT_TRUE(col1_stats.has_max()); + ASSERT_EQ(col1_stats.get_max(), 9); + ASSERT_TRUE(col1_stats.has_max()); + ASSERT_EQ(col1_stats.get_min(), 0); + ASSERT_TRUE(col1_stats.has_unique()); + ASSERT_EQ(col1_stats.get_unique_count(), 10); + auto col2_stats = decoded.column(1).get_statistics(); + ASSERT_TRUE(col2_stats.has_max()); + ASSERT_EQ(col2_stats.get_max(), 18.0); + ASSERT_TRUE(col2_stats.has_max()); + ASSERT_EQ(col2_stats.get_min(), 0); + ASSERT_TRUE(col2_stats.has_unique()); + ASSERT_EQ(col2_stats.get_unique_count(), 10); +} + TEST(Segment, ColumnNamesProduceDifferentHashes) { const auto stream_desc_1 = stream_descriptor(StreamId{"thing"}, RowCountIndex{}, { scalar_field(DataType::UINT8, "ints1"), diff --git a/cpp/arcticdb/column_store/column.hpp b/cpp/arcticdb/column_store/column.hpp index b2460a36e3..1d89cb3107 100644 --- a/cpp/arcticdb/column_store/column.hpp +++ b/cpp/arcticdb/column_store/column.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -251,6 +252,18 @@ class Column { bool sparse_permitted() const; + void set_statistics(FieldStatsImpl stats) { + stats_ = stats; + } + + bool has_statistics() const { + return stats_.set_; + }; + + FieldStatsImpl get_statistics() const { + return stats_; + } + void backfill_sparse_map(ssize_t to_row) { ARCTICDB_TRACE(log::version(), "Backfilling sparse map to position {}", to_row); // Initialise the optional to an empty bitset if it has not been created yet @@ -936,6 +949,7 @@ class Column { Sparsity allow_sparse_ = Sparsity::NOT_PERMITTED; std::optional sparse_map_; + FieldStatsImpl stats_; util::MagicNum<'D', 'C', 'o', 'l'> magic_; }; diff --git a/cpp/arcticdb/column_store/memory_segment.hpp b/cpp/arcticdb/column_store/memory_segment.hpp index 4f982618c7..db1eaa4c5f 100644 --- a/cpp/arcticdb/column_store/memory_segment.hpp +++ b/cpp/arcticdb/column_store/memory_segment.hpp @@ -235,6 +235,10 @@ class SegmentInMemory { impl_->reset_timeseries_descriptor(); } + void calculate_statistics() { + impl_->calculate_statistics(); + } + [[nodiscard]] size_t num_columns() const { return impl_->num_columns(); } [[nodiscard]] size_t row_count() const { return impl_->row_count(); } diff --git a/cpp/arcticdb/column_store/memory_segment_impl.cpp b/cpp/arcticdb/column_store/memory_segment_impl.cpp index 36d6f17e20..23ad093eff 100644 --- a/cpp/arcticdb/column_store/memory_segment_impl.cpp +++ b/cpp/arcticdb/column_store/memory_segment_impl.cpp @@ -700,6 +700,20 @@ void SegmentInMemoryImpl::reset_timeseries_descriptor() { tsd_.reset(); } +void SegmentInMemoryImpl::calculate_statistics() { + for(auto& column : columns_) { + if(column->type().dimension() == Dimension::Dim0) { + const auto type = column->type(); + if(is_numeric_type(type.data_type()) || is_sequence_type(type.data_type())) { + type.visit_tag([&column] (auto tdt) { + using TagType = std::decay_t; + column->set_statistics(generate_column_statistics(column->data())); + }); + } + } + } +} + void SegmentInMemoryImpl::reset_metadata() { metadata_.reset(); } diff --git a/cpp/arcticdb/column_store/memory_segment_impl.hpp b/cpp/arcticdb/column_store/memory_segment_impl.hpp index e39c6de69d..994a2f26c2 100644 --- a/cpp/arcticdb/column_store/memory_segment_impl.hpp +++ b/cpp/arcticdb/column_store/memory_segment_impl.hpp @@ -763,6 +763,8 @@ class SegmentInMemoryImpl { void reset_timeseries_descriptor(); + void calculate_statistics(); + bool has_user_metadata() { return tsd_.has_value() && !tsd_->proto_is_null() && tsd_->proto().has_user_meta(); } diff --git a/cpp/arcticdb/column_store/statistics.hpp b/cpp/arcticdb/column_store/statistics.hpp new file mode 100644 index 0000000000..f58f4adfeb --- /dev/null +++ b/cpp/arcticdb/column_store/statistics.hpp @@ -0,0 +1,211 @@ +#pragma once + +#include +#include + +#include + +#include +#include + +namespace arcticdb { + +template +void set_value(T value, uint64_t& target) { + memcpy(&target, &value, sizeof(T)); +} + +template +void get_value(uint64_t value, T& target) { + memcpy(&target, &value, sizeof(T)); +} + +enum class FieldStatsValue : uint8_t { + MIN = 1, + MAX = 1 << 1, + UNIQUE = 1 << 2 + }; + +struct FieldStatsImpl : public FieldStats { + FieldStatsImpl() = default; + + ARCTICDB_MOVE_COPY_DEFAULT(FieldStatsImpl) + + template + void set_max(T value) { + set_value(value, max_); + set_ |= static_cast(FieldStatsValue::MAX); + } + + template + void set_min(T value) { + set_value(value, min_); + set_ |= static_cast(FieldStatsValue::MIN); + } + + void set_unique( + uint32_t unique_count, + UniqueCountType unique_count_precision) { + unique_count_ = unique_count; + unique_count_precision_ = unique_count_precision; + set_ |= static_cast(FieldStatsValue::UNIQUE); + } + + [[nodiscard]] bool has_max() const { + return set_ & static_cast(FieldStatsValue::MAX); + } + + [[nodiscard]] bool has_min() const { + return set_ & static_cast(FieldStatsValue::MIN); + } + + [[nodiscard]] bool has_unique() const { + return set_ & static_cast(FieldStatsValue::UNIQUE); + } + + [[nodiscard]] bool unique_count_is_precise() const { + return unique_count_precision_ == UniqueCountType::PRECISE; + }; + + template + T get_max() { + T value; + get_value(max_, value); + return value; + } + + template + T get_min() { + T value; + get_value(min_, value); + return value; + } + + size_t get_unique_count() const { + return unique_count_; + } + + FieldStatsImpl(FieldStats base) { + min_ = base.min_; + max_ = base.max_; + unique_count_ = base.unique_count_; + unique_count_precision_ = base.unique_count_precision_; + set_ = base.set_; + } + + template + FieldStatsImpl( + T min, + T max, + uint32_t unique_count, + UniqueCountType unique_count_precision) { + set_min(min); + set_max(max); + set_unique(unique_count, unique_count_precision); + } + + FieldStatsImpl( + uint32_t unique_count, + UniqueCountType unique_count_precision) { + set_unique(unique_count, unique_count_precision); + } + + + template + void compose(const FieldStatsImpl& other) { + if (other.has_min()) { + if (!has_min()) { + min_ = other.min_; + set_ |= static_cast(FieldStatsValue::MIN); + } else { + T this_min, other_min; + get_value(min_, this_min); + get_value(other.min_, other_min); + T result_min = std::min(this_min, other_min); + set_value(result_min, min_); + } + } + + if (other.has_max()) { + if (!has_max()) { + max_ = other.max_; + set_ |= static_cast(FieldStatsValue::MAX); + } else { + T this_max, other_max; + get_value(max_, this_max); + get_value(other.max_, other_max); + T result_max = std::max(this_max, other_max); + set_value(result_max, max_); + } + } + + if (other.has_unique()) { + if (!has_unique()) { + unique_count_ = other.unique_count_; + unique_count_precision_ = other.unique_count_precision_; + set_ |= static_cast(FieldStatsValue::UNIQUE); + } else { + util::check(unique_count_precision_ == other.unique_count_precision_, + "Mismatching unique count precision, {} != {}", + uint8_t(unique_count_precision_), uint8_t(other.unique_count_precision_)); + + unique_count_ += other.unique_count_; + } + } + } +}; + +template +FieldStatsImpl generate_numeric_statistics(std::span data) { + if(data.empty()) + return FieldStatsImpl{}; + + auto [col_min, col_max] = std::minmax_element(std::begin(data), std::end(data)); + ankerl::unordered_dense::set unique; + for(auto val : data) { + unique.emplace(val); + } + FieldStatsImpl field_stats(*col_min, *col_max, unique.size(), UniqueCountType::PRECISE); + return field_stats; +} + +inline FieldStatsImpl generate_string_statistics(std::span data) { + ankerl::unordered_dense::set unique; + for(auto val : data) { + unique.emplace(val); + } + FieldStatsImpl field_stats(unique.size(), UniqueCountType::PRECISE); + return field_stats; +} + +template +FieldStatsImpl generate_column_statistics(ColumnData column_data) { + using RawType = typename TagType::DataTypeTag::raw_type; + if(column_data.num_blocks() == 1) { + auto block = column_data.next(); + const RawType* ptr = block->data(); + const size_t count = block->row_count(); + if constexpr (is_numeric_type(TagType::DataTypeTag::data_type)) { + return generate_numeric_statistics(std::span{ptr, count}); + } else if constexpr (is_dynamic_string_type(TagType::DataTypeTag::data_type)) { + return generate_string_statistics(std::span{ptr, count}); + } else { + util::raise_rte("Cannot generate statistics for data type"); + } + } else { + FieldStatsImpl stats; + while (auto block = column_data.next()) { + const RawType* ptr = block->data(); + const size_t count = block->row_count(); + if constexpr (is_numeric_type(TagType::DataTypeTag::data_type)) { + auto local_stats = generate_numeric_statistics(std::span{ptr, count}); + stats.compose(local_stats); + } else if constexpr (is_dynamic_string_type(TagType::DataTypeTag::data_type)) { + auto local_stats = generate_string_statistics(std::span{ptr, count}); + stats.compose(local_stats); + } + } + return stats; + } +} +} // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/column_store/test/test_column.cpp b/cpp/arcticdb/column_store/test/test_column.cpp index f3b6008c1a..daeb2d9cc5 100644 --- a/cpp/arcticdb/column_store/test/test_column.cpp +++ b/cpp/arcticdb/column_store/test/test_column.cpp @@ -283,4 +283,117 @@ TEST(ColumnData, LowerBound) { auto it = std::lower_bound(column.begin(), column.end(), 5); ASSERT_EQ(*it, 6); ASSERT_EQ(std::distance(column.begin(), it), 3); +} + +FieldStatsImpl generate_stats_from_column(const Column& column) { + return column.type().visit_tag([&column](auto tdt) { + using TagType = std::decay_t; + return generate_column_statistics(column.data()); + }); +} + +TEST(ColumnStats, IntegerColumn) { + Column int_col(make_scalar_type(DataType::INT64)); + int_col.set_scalar(0, 10); + int_col.set_scalar(1, 5); + int_col.set_scalar(2, 20); + int_col.set_scalar(3, 5); + int_col.set_scalar(4, 15); + + FieldStatsImpl stats = generate_stats_from_column(int_col); + + EXPECT_TRUE(stats.has_min()); + EXPECT_TRUE(stats.has_max()); + EXPECT_TRUE(stats.has_unique()); + + EXPECT_EQ(stats.get_min(), 5); + EXPECT_EQ(stats.get_max(), 20); + EXPECT_EQ(stats.unique_count_, 4); + EXPECT_EQ(stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(ColumnStats, FloatColumn) { + Column float_col(make_scalar_type(DataType::FLOAT32)); + float_col.set_scalar(0, 10.5f); + float_col.set_scalar(1, 5.5f); + float_col.set_scalar(2, 20.5f); + float_col.set_scalar(3, 5.5f); + float_col.set_scalar(4, 15.5f); + + FieldStatsImpl stats = generate_stats_from_column(float_col); + + EXPECT_TRUE(stats.has_min()); + EXPECT_TRUE(stats.has_max()); + EXPECT_TRUE(stats.has_unique()); + + EXPECT_FLOAT_EQ(stats.get_min(), 5.5f); + EXPECT_FLOAT_EQ(stats.get_max(), 20.5f); + EXPECT_EQ(stats.unique_count_, 4); + EXPECT_EQ(stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(ColumnStats, EmptyColumn) { + Column empty_col(make_scalar_type(DataType::FLOAT32)); + FieldStatsImpl stats = generate_stats_from_column(empty_col); + + EXPECT_FALSE(stats.has_min()); + EXPECT_FALSE(stats.has_max()); + EXPECT_FALSE(stats.has_unique()); + EXPECT_EQ(stats.unique_count_, 0); + EXPECT_EQ(stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(ColumnStats, SingleValueColumn) { + Column single_col(make_scalar_type(DataType::INT32)); + single_col.set_scalar(0, 42); + + FieldStatsImpl stats = generate_stats_from_column(single_col); + + EXPECT_TRUE(stats.has_min()); + EXPECT_TRUE(stats.has_max()); + EXPECT_TRUE(stats.has_unique()); + + EXPECT_EQ(stats.get_min(), 42); + EXPECT_EQ(stats.get_max(), 42); + EXPECT_EQ(stats.unique_count_, 1); + EXPECT_EQ(stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(ColumnStats, NegativeNumbers) { + Column neg_col(make_scalar_type(DataType::INT64)); + neg_col.set_scalar(0, -10); + neg_col.set_scalar(1, -5); + neg_col.set_scalar(2, -20); + neg_col.set_scalar(3, -15); + + FieldStatsImpl stats = generate_stats_from_column(neg_col); + + EXPECT_TRUE(stats.has_min()); + EXPECT_TRUE(stats.has_max()); + EXPECT_TRUE(stats.has_unique()); + + EXPECT_EQ(stats.get_min(), -20); + EXPECT_EQ(stats.get_max(), -5); + EXPECT_EQ(stats.unique_count_, 4); + EXPECT_EQ(stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(ColumnStats, DoubleColumn) { + Column double_col(make_scalar_type(DataType::FLOAT64)); + double_col.set_scalar(0, 10.5); + double_col.set_scalar(1, 5.5); + double_col.set_scalar(2, 20.5); + double_col.set_scalar(3, 5.5); + double_col.set_scalar(4, 15.5); + + FieldStatsImpl stats = generate_stats_from_column(double_col); + + EXPECT_TRUE(stats.has_min()); + EXPECT_TRUE(stats.has_max()); + EXPECT_TRUE(stats.has_unique()); + + EXPECT_DOUBLE_EQ(stats.get_min(), 5.5); + EXPECT_DOUBLE_EQ(stats.get_max(), 20.5); + EXPECT_EQ(stats.unique_count_, 4); + EXPECT_EQ(stats.unique_count_precision_, UniqueCountType::PRECISE); } \ No newline at end of file diff --git a/cpp/arcticdb/column_store/test/test_statistics.cpp b/cpp/arcticdb/column_store/test/test_statistics.cpp new file mode 100644 index 0000000000..320d39e0f5 --- /dev/null +++ b/cpp/arcticdb/column_store/test/test_statistics.cpp @@ -0,0 +1,222 @@ +/* Copyright 2025 Man Group Operations Limited + * + * Use of this software is governed by the Business Source License 1.1 included in the file licenses/BSL.txt. + * + * As of the Change Date specified in that file, in accordance with the Business Source License, use of this software will be governed by the Apache License, version 2.0. + */ + +#include +#include +#include +#include + +TEST(FieldStatsTest, IntegralStatisticsBasic) { + using namespace arcticdb; + + std::vector data{1, 2, 3, 2, 1, 4, 5, 3}; + auto field_stats = generate_numeric_statistics(std::span(data)); + + EXPECT_TRUE(field_stats.has_min()); + EXPECT_TRUE(field_stats.has_max()); + EXPECT_TRUE(field_stats.has_unique()); + + EXPECT_EQ(field_stats.get_min(), 1); + EXPECT_EQ(field_stats.get_max(), 5); + EXPECT_EQ(field_stats.unique_count_, 5); + EXPECT_EQ(field_stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(FieldStatsTest, IntegralStatisticsSingleValue) { + using namespace arcticdb; + std::vector data{42, 42, 42, 42}; + auto field_stats = generate_numeric_statistics(std::span(data)); + + EXPECT_TRUE(field_stats.has_min()); + EXPECT_TRUE(field_stats.has_max()); + EXPECT_TRUE(field_stats.has_unique()); + + EXPECT_EQ(field_stats.get_min(), 42); + EXPECT_EQ(field_stats.get_max(), 42); + EXPECT_EQ(field_stats.unique_count_, 1); + EXPECT_EQ(field_stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(FieldStatsTest, StringStatisticsBasic) { + using namespace arcticdb; + std::vector data{0x123, 0x456, 0x123, 0x789}; + auto field_stats = generate_string_statistics(std::span(data)); + + EXPECT_FALSE(field_stats.has_min()); + EXPECT_FALSE(field_stats.has_max()); + EXPECT_TRUE(field_stats.has_unique()); + + EXPECT_EQ(field_stats.unique_count_, 3); + EXPECT_EQ(field_stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(FieldStatsTest, FieldStatsImplConstruction) { + using namespace arcticdb; + FieldStatsImpl stats(100u, UniqueCountType::PRECISE); + + EXPECT_FALSE(stats.has_min()); + EXPECT_FALSE(stats.has_max()); + EXPECT_TRUE(stats.has_unique()); + EXPECT_TRUE(stats.unique_count_is_precise()); + EXPECT_EQ(stats.unique_count_, 100u); +} + +TEST(FieldStatsTest, FieldStatsImplFullConstruction) { + using namespace arcticdb; + FieldStatsImpl stats( + static_cast(1), + static_cast(100), + 50u, + UniqueCountType::PRECISE + ); + + EXPECT_TRUE(stats.has_min()); + EXPECT_TRUE(stats.has_max()); + EXPECT_TRUE(stats.has_unique()); + EXPECT_TRUE(stats.unique_count_is_precise()); + + EXPECT_EQ(stats.get_max(), 100); + EXPECT_EQ(stats.get_min(), 1); + EXPECT_EQ(stats.unique_count_, 50u); +} + +TEST(FieldStatsTest, EmptyStringStatistics) { + using namespace arcticdb; + std::vector data; + auto field_stats = generate_string_statistics(std::span(data)); + + EXPECT_FALSE(field_stats.has_min()); + EXPECT_FALSE(field_stats.has_max()); + EXPECT_TRUE(field_stats.has_unique()); + + EXPECT_EQ(field_stats.unique_count_, 0); + EXPECT_EQ(field_stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(FieldStatsTest, EmptyIntegralStatistics) { + using namespace arcticdb; + std::vector data; + auto field_stats = generate_numeric_statistics(std::span(data)); + + EXPECT_FALSE(field_stats.has_min()); + EXPECT_FALSE(field_stats.has_max()); + EXPECT_FALSE(field_stats.has_unique()); + + EXPECT_EQ(field_stats.unique_count_, 0); + EXPECT_EQ(field_stats.unique_count_precision_, UniqueCountType::PRECISE); +} + +TEST(FieldStatsTest, ComposeIntegerStats) { + using namespace arcticdb; + FieldStatsImpl stats1; + stats1.set_max(100); + stats1.set_min(10); + stats1.set_unique(5, UniqueCountType::PRECISE); + + FieldStatsImpl stats2; + stats2.set_max(50); + stats2.set_min(20); + stats2.set_unique(3, UniqueCountType::PRECISE); + + stats1.compose(stats2); + + EXPECT_TRUE(stats1.has_max()); + EXPECT_TRUE(stats1.has_min()); + EXPECT_TRUE(stats1.has_unique()); + EXPECT_EQ(stats1.get_max(), 100); + EXPECT_EQ(stats1.get_min(), 10); + EXPECT_EQ(stats1.unique_count_, 8); +} + +TEST(FieldStatsTest, ComposeFloatStats) { + using namespace arcticdb; + FieldStatsImpl stats1; + stats1.set_max(100.5f); + stats1.set_min(10.5f); + stats1.set_unique(5, UniqueCountType::PRECISE); + + FieldStatsImpl stats2; + stats2.set_max(50.5f); + stats2.set_min(5.5f); + stats2.set_unique(3, UniqueCountType::PRECISE); + + stats1.compose(stats2); + + EXPECT_TRUE(stats1.has_max()); + EXPECT_TRUE(stats1.has_min()); + EXPECT_TRUE(stats1.has_unique()); + EXPECT_FLOAT_EQ(stats1.get_max(), 100.5f); + EXPECT_FLOAT_EQ(stats1.get_min(), 5.5f); + EXPECT_EQ(stats1.unique_count_, 8); +} + +TEST(FieldStatsTest, ComposePartialTemplatedStats) { + using namespace arcticdb; + FieldStatsImpl stats1; + stats1.set_max(100.5); + + FieldStatsImpl stats2; + stats2.set_min(5.5); + + stats1.compose(stats2); + + EXPECT_TRUE(stats1.has_max()); + EXPECT_TRUE(stats1.has_min()); + EXPECT_DOUBLE_EQ(stats1.get_max(), 100.5); + EXPECT_DOUBLE_EQ(stats1.get_min(), 5.5); +} + +TEST(FieldStatsTest, ComposeEmptyTemplatedStats) { + using namespace arcticdb; + FieldStatsImpl stats1; + stats1.set_max(100); + stats1.set_min(10); + stats1.set_unique(5, UniqueCountType::PRECISE); + + FieldStatsImpl empty_stats; + stats1.compose(empty_stats); + + EXPECT_TRUE(stats1.has_max()); + EXPECT_TRUE(stats1.has_min()); + EXPECT_TRUE(stats1.has_unique()); + EXPECT_EQ(stats1.get_max(), 100); + EXPECT_EQ(stats1.get_min(), 10); + EXPECT_EQ(stats1.unique_count_, 5); +} + +TEST(FieldStatsTest, ComposeNegativeNumbers) { + using namespace arcticdb; + FieldStatsImpl stats1; + stats1.set_max(-10); + stats1.set_min(-100); + + FieldStatsImpl stats2; + stats2.set_max(-20); + stats2.set_min(-50); + + stats1.compose(stats2); + + EXPECT_TRUE(stats1.has_max()); + EXPECT_TRUE(stats1.has_min()); + EXPECT_EQ(stats1.get_max(), -10); + EXPECT_EQ(stats1.get_min(), -100); +} + +TEST(FieldStatsTest, ComposeMismatchingPrecisionTemplated) { + using namespace arcticdb; + FieldStatsImpl stats1; + stats1.set_max(100); + stats1.set_min(10); + stats1.set_unique(5, UniqueCountType::PRECISE); + + FieldStatsImpl stats2; + stats2.set_max(200); + stats2.set_min(20); + stats2.set_unique(3, UniqueCountType::HYPERLOGLOG); + + EXPECT_THROW(stats1.compose(stats2), std::exception); +} \ No newline at end of file diff --git a/cpp/arcticdb/entity/protobuf_mappings.cpp b/cpp/arcticdb/entity/protobuf_mappings.cpp index 3b48af598f..751231457e 100644 --- a/cpp/arcticdb/entity/protobuf_mappings.cpp +++ b/cpp/arcticdb/entity/protobuf_mappings.cpp @@ -29,18 +29,6 @@ inline arcticdb::proto::descriptors::SortedValue sorted_value_to_proto(SortedVal } } -inline SortedValue sorted_value_from_proto(arcticdb::proto::descriptors::SortedValue sorted_proto) { - switch (sorted_proto) { - case arcticdb::proto::descriptors::SortedValue::UNSORTED: - return SortedValue::UNSORTED; - case arcticdb::proto::descriptors::SortedValue::DESCENDING: - return SortedValue::DESCENDING; - case arcticdb::proto::descriptors::SortedValue::ASCENDING: - return SortedValue::ASCENDING; - default: - return SortedValue::UNKNOWN; - } -} // The type enum needs to be kept in sync with the protobuf one, which should not be changed [[nodiscard]] arcticdb::proto::descriptors::IndexDescriptor index_descriptor_to_proto(const IndexDescriptorImpl& index_descriptor) { @@ -99,7 +87,7 @@ AtomKey key_from_proto(const arcticdb::proto::descriptors::AtomKey& input) { void copy_stream_descriptor_to_proto(const StreamDescriptor& desc, arcticdb::proto::descriptors::StreamDescriptor& proto) { proto.set_in_bytes(desc.uncompressed_bytes()); proto.set_out_bytes(desc.compressed_bytes()); - proto.set_sorted(arcticdb::proto::descriptors::SortedValue(desc.sorted())); + proto.set_sorted(sorted_value_to_proto(desc.sorted())); // The index descriptor enum must be kept in sync with the protobuf *proto.mutable_index() = index_descriptor_to_proto(desc.index()); util::variant_match(desc.id(), diff --git a/cpp/arcticdb/entity/types_proto.cpp b/cpp/arcticdb/entity/types_proto.cpp index afd5228a16..b1a8a4d064 100644 --- a/cpp/arcticdb/entity/types_proto.cpp +++ b/cpp/arcticdb/entity/types_proto.cpp @@ -57,13 +57,6 @@ void set_data_type(DataType data_type, arcticdb::proto::descriptors::TypeDescrip return output; } -DataType get_data_type(const arcticdb::proto::descriptors::TypeDescriptor& type_desc) { - return combine_data_type( - static_cast(static_cast(type_desc.value_type())), - static_cast(static_cast(type_desc.size_bits())) - ); -} - TypeDescriptor type_desc_from_proto(const arcticdb::proto::descriptors::TypeDescriptor& type_desc) { return { combine_data_type( @@ -74,10 +67,6 @@ TypeDescriptor type_desc_from_proto(const arcticdb::proto::descriptors::TypeDesc }; } -DataType data_type_from_proto(const arcticdb::proto::descriptors::TypeDescriptor& type_desc) { - return type_desc_from_proto(type_desc).data_type(); -} - arcticdb::proto::descriptors::StreamDescriptor_FieldDescriptor field_proto(DataType dt, Dimension dim, std::string_view name) { arcticdb::proto::descriptors::StreamDescriptor_FieldDescriptor output; if (!name.empty()) diff --git a/cpp/arcticdb/entity/types_proto.hpp b/cpp/arcticdb/entity/types_proto.hpp index 9729f255ea..5da8ab9faa 100644 --- a/cpp/arcticdb/entity/types_proto.hpp +++ b/cpp/arcticdb/entity/types_proto.hpp @@ -21,18 +21,12 @@ using FieldProto = arcticdb::proto::descriptors::StreamDescriptor_FieldDescripto bool operator==(const FieldProto &left, const FieldProto &right); bool operator<(const FieldProto &left, const FieldProto &right); -arcticdb::proto::descriptors::SortedValue sorted_value_to_proto(SortedValue sorted); - -SortedValue sorted_value_from_proto(arcticdb::proto::descriptors::SortedValue sorted_proto); - void set_data_type(DataType data_type, arcticdb::proto::descriptors::TypeDescriptor &type_desc); DataType get_data_type(const arcticdb::proto::descriptors::TypeDescriptor &type_desc); TypeDescriptor type_desc_from_proto(const arcticdb::proto::descriptors::TypeDescriptor &type_desc); -DataType data_type_from_proto(const arcticdb::proto::descriptors::TypeDescriptor &type_desc); - [[nodiscard]] arcticdb::proto::descriptors::TypeDescriptor type_descriptor_to_proto(const TypeDescriptor& desc); inline arcticdb::proto::descriptors::TypeDescriptor::SizeBits size_bits_proto_from_data_type(DataType data_type) { @@ -50,10 +44,6 @@ arcticdb::proto::descriptors::StreamDescriptor_FieldDescriptor field_proto( Dimension dim, std::string_view name); - DataType get_data_type(const arcticdb::proto::descriptors::TypeDescriptor& type_desc); - - DataType data_type_from_proto(const arcticdb::proto::descriptors::TypeDescriptor& type_desc); - arcticdb::proto::descriptors::StreamDescriptor_FieldDescriptor field_proto(DataType dt, Dimension dim, std::string_view name); const char* index_type_to_str(IndexDescriptor::Type type); diff --git a/cpp/arcticdb/pipeline/frame_utils.hpp b/cpp/arcticdb/pipeline/frame_utils.hpp index 125156dab0..90f0c50a6f 100644 --- a/cpp/arcticdb/pipeline/frame_utils.hpp +++ b/cpp/arcticdb/pipeline/frame_utils.hpp @@ -92,197 +92,256 @@ RawType* flatten_tensor( return reinterpret_cast(flattened_buffer->data()); } - -template -std::optional aggregator_set_data( - const TypeDescriptor& type_desc, - const entity::NativeTensor& tensor, - Aggregator& agg, - size_t col, - size_t rows_to_write, - size_t row, - size_t slice_num, - size_t regular_slice_size, - bool sparsify_floats -) { - return type_desc.visit_tag([&](auto tag) { - using RawType = typename std::decay_t::DataTypeTag::raw_type; - constexpr auto dt = std::decay_t::DataTypeTag::data_type; - - util::check(type_desc.data_type() == tensor.data_type(), "Type desc {} != {} tensor type", type_desc.data_type(), - tensor.data_type()); - util::check(type_desc.data_type() == dt, "Type desc {} != {} static type", type_desc.data_type(), dt); - const auto c_style = util::is_cstyle_array(tensor); - std::optional flattened_buffer; - if constexpr (is_sequence_type(dt)) { - normalization::check(tag.dimension() == Dimension::Dim0, - "Multidimensional string types are not supported."); - ARCTICDB_SUBSAMPLE_AGG(SetDataString) - if (is_fixed_string_type(dt)) { - // deduplicate the strings - auto str_stride = tensor.strides(0); - auto data = const_cast(tensor.data()); - auto char_data = reinterpret_cast(data) + row * str_stride; - auto str_len = tensor.elsize(); - - for (size_t s = 0; s < rows_to_write; ++s, char_data += str_stride) { - agg.set_string_at(col, s, char_data, str_len); - } +template +std::optional set_sequence_type( + AggregatorType& agg, + const entity::NativeTensor& tensor, + size_t col, + size_t rows_to_write, + size_t row, + size_t slice_num, + size_t regular_slice_size) { + constexpr auto dt = TagType::DataTypeTag::data_type; + const auto c_style = util::is_cstyle_array(tensor); + std::optional flattened_buffer; + + ARCTICDB_SAMPLE_DEFAULT(SetDataString) + if (is_fixed_string_type(dt)) { + // deduplicate the strings + auto str_stride = tensor.strides(0); + auto data = const_cast(tensor.data()); + auto char_data = reinterpret_cast(data) + row * str_stride; + auto str_len = tensor.elsize(); + + for (size_t s = 0; s < rows_to_write; ++s, char_data += str_stride) { + agg.set_string_at(col, s, char_data, str_len); + } + } else { + auto data = const_cast(tensor.data()); + auto ptr_data = reinterpret_cast(data); + ptr_data += row; + if (!c_style) + ptr_data = flatten_tensor(flattened_buffer, rows_to_write, tensor, slice_num, regular_slice_size); + + auto none = py::none{}; + std::variant wrapper_or_error; + // GIL will be acquired if there is a string that is not pure ASCII/UTF-8 + // In this case a PyObject will be allocated by convert::py_unicode_to_buffer + // If such a string is encountered in a column, then the GIL will be held until that whole column has + // been processed, on the assumption that if a column has one such string it will probably have many. + std::optional scoped_gil_lock; + auto& column = agg.segment().column(col); + column.allocate_data(rows_to_write * sizeof(entity::position_t)); + auto out_ptr = reinterpret_cast(column.buffer().data()); + auto& string_pool = agg.segment().string_pool(); + for (size_t s = 0; s < rows_to_write; ++s, ++ptr_data) { + if (*ptr_data == none.ptr()) { + *out_ptr++ = not_a_string(); + } else if(is_py_nan(*ptr_data)){ + *out_ptr++ = nan_placeholder(); } else { - auto data = const_cast(tensor.data()); - auto ptr_data = reinterpret_cast(data); - ptr_data += row; - if (!c_style) - ptr_data = flatten_tensor(flattened_buffer, rows_to_write, tensor, slice_num, regular_slice_size); - - auto none = py::none{}; - std::variant wrapper_or_error; - // GIL will be acquired if there is a string that is not pure ASCII/UTF-8 - // In this case a PyObject will be allocated by convert::py_unicode_to_buffer - // If such a string is encountered in a column, then the GIL will be held until that whole column has - // been processed, on the assumption that if a column has one such string it will probably have many. - std::optional scoped_gil_lock; - auto& column = agg.segment().column(col); - column.allocate_data(rows_to_write * sizeof(entity::position_t)); - auto out_ptr = reinterpret_cast(column.buffer().data()); - auto& string_pool = agg.segment().string_pool(); - for (size_t s = 0; s < rows_to_write; ++s, ++ptr_data) { - if (*ptr_data == none.ptr()) { - *out_ptr++ = not_a_string(); - } else if(is_py_nan(*ptr_data)){ - *out_ptr++ = nan_placeholder(); - } else { - if constexpr (is_utf_type(slice_value_type(dt))) { - wrapper_or_error = convert::py_unicode_to_buffer(*ptr_data, scoped_gil_lock); - } else { - wrapper_or_error = convert::pystring_to_buffer(*ptr_data, false); - } - // Cannot use util::variant_match as only one of the branches would have a return type - if (std::holds_alternative(wrapper_or_error)) { - convert::PyStringWrapper wrapper(std::move(std::get(wrapper_or_error))); - const auto offset = string_pool.get(wrapper.buffer_, wrapper.length_); - *out_ptr++ = offset.offset(); - } else if (std::holds_alternative(wrapper_or_error)) { - auto error = std::get(wrapper_or_error); - error.row_index_in_slice_ = s; - return std::optional(error); - } else { - internal::raise("Unexpected variant alternative"); - } - } - } - } - } else if constexpr ((is_numeric_type(dt) || is_bool_type(dt)) && tag.dimension() == Dimension::Dim0) { - auto ptr = tensor.template ptr_cast(row); - if (sparsify_floats) { - if constexpr (is_floating_point_type(dt)) { - agg.set_sparse_block(col, ptr, rows_to_write); + if constexpr (is_utf_type(slice_value_type(dt))) { + wrapper_or_error = convert::py_unicode_to_buffer(*ptr_data, scoped_gil_lock); } else { - util::raise_rte("sparse currently supported for floating point columns only."); + wrapper_or_error = convert::pystring_to_buffer(*ptr_data, false); } - } else { - if (c_style) { - ARCTICDB_SUBSAMPLE_AGG(SetDataZeroCopy) - agg.set_external_block(col, ptr, rows_to_write); + // Cannot use util::variant_match as only one of the branches would have a return type + if (std::holds_alternative(wrapper_or_error)) { + convert::PyStringWrapper wrapper(std::move(std::get(wrapper_or_error))); + const auto offset = string_pool.get(wrapper.buffer_, wrapper.length_); + *out_ptr++ = offset.offset(); + } else if (std::holds_alternative(wrapper_or_error)) { + auto error = std::get(wrapper_or_error); + error.row_index_in_slice_ = s; + return std::optional(error); } else { - ARCTICDB_SUBSAMPLE_AGG(SetDataFlatten) - ARCTICDB_DEBUG(log::version(), - "Data contains non-contiguous columns, writing will be inefficient, consider coercing to c_style ndarray (shape={}, data_size={})", - tensor.strides(0), - sizeof(RawType)); - - TypedTensor t(tensor, slice_num, regular_slice_size, rows_to_write); - agg.set_array(col, t); + internal::raise("Unexpected variant alternative"); } } - } else if constexpr(is_bool_object_type(dt)) { - normalization::check(tag.dimension() == Dimension::Dim0, - "Multidimensional nullable booleans are not supported"); - auto data = const_cast(tensor.data()); - auto ptr_data = reinterpret_cast(data); - ptr_data += row; - - if (!c_style) - ptr_data = flatten_tensor(flattened_buffer, rows_to_write, tensor, slice_num, regular_slice_size); - - util::BitSet bitset = util::scan_object_type_to_sparse(ptr_data, rows_to_write); - - const auto num_values = bitset.count(); - auto bool_buffer = ChunkedBuffer::presized(num_values * sizeof(uint8_t), entity::AllocationType::PRESIZED); - auto bool_ptr = bool_buffer.ptr_cast(0u, num_values); - for (auto it = bitset.first(); it < bitset.end(); ++it) { - *bool_ptr = static_cast(PyObject_IsTrue(ptr_data[*it])); - ++bool_ptr; - } - if(bitset.count() > 0) - agg.set_sparse_block(col, std::move(bool_buffer), std::move(bitset)); + } + } + return std::optional{}; +} - } else if constexpr(is_array_type(TypeDescriptor(tag))) { - auto data = const_cast(tensor.data()); - const auto ptr_data = reinterpret_cast(data) + row; +template +void set_integral_scalar_type( + AggregatorType& agg, + const entity::NativeTensor& tensor, + size_t col, + size_t rows_to_write, + size_t row, + size_t slice_num, + size_t regular_slice_size, + bool sparsify_floats) { + constexpr auto dt = TagType::DataTypeTag::data_type; + auto ptr = tensor.template ptr_cast(row); + if (sparsify_floats) { + if constexpr (is_floating_point_type(dt)) { + agg.set_sparse_block(col, ptr, rows_to_write); + } else { + util::raise_rte("sparse currently supported for floating point columns only."); + } + } else { + const auto c_style = util::is_cstyle_array(tensor); + if (c_style) { + ARCTICDB_SAMPLE_DEFAULT(SetDataZeroCopy) + agg.set_external_block(col, ptr, rows_to_write); + } else { + ARCTICDB_SAMPLE_DEFAULT(SetDataFlatten) + ARCTICDB_DEBUG(log::version(), + "Data contains non-contiguous columns, writing will be inefficient, consider coercing to c_style ndarray (shape={}, data_size={})", + tensor.strides(0), + sizeof(RawType)); + + TypedTensor t(tensor, slice_num, regular_slice_size, rows_to_write); + agg.set_array(col, t); + } + } +} + +template +void set_bool_object_type( + AggregatorType& agg, + const entity::NativeTensor& tensor, + size_t col, + size_t rows_to_write, + size_t row, + size_t slice_num, + size_t regular_slice_size) { + const auto c_style = util::is_cstyle_array(tensor); + std::optional flattened_buffer; - util::BitSet values_bitset = util::scan_object_type_to_sparse(ptr_data, rows_to_write); - util::check(!values_bitset.empty(), + auto data = const_cast(tensor.data()); + auto ptr_data = reinterpret_cast(data); + ptr_data += row; + + if (!c_style) + ptr_data = flatten_tensor(flattened_buffer, rows_to_write, tensor, slice_num, regular_slice_size); + + util::BitSet bitset = util::scan_object_type_to_sparse(ptr_data, rows_to_write); + + const auto num_values = bitset.count(); + auto bool_buffer = ChunkedBuffer::presized(num_values * sizeof(uint8_t), entity::AllocationType::PRESIZED); + auto bool_ptr = bool_buffer.ptr_cast(0u, num_values); + for (auto it = bitset.first(); it < bitset.end(); ++it) { + *bool_ptr = static_cast(PyObject_IsTrue(ptr_data[*it])); + ++bool_ptr; + } + if(bitset.count() > 0) + agg.set_sparse_block(col, std::move(bool_buffer), std::move(bitset)); + +} + +template +std::optional set_array_type( + const TypeDescriptor& type_desc, + AggregatorType& agg, + const entity::NativeTensor& tensor, + size_t col, + size_t rows_to_write, + size_t row) { + constexpr auto dt = TagType::DataTypeTag::data_type; + auto data = const_cast(tensor.data()); + const auto ptr_data = reinterpret_cast(data) + row; + + util::BitSet values_bitset = util::scan_object_type_to_sparse(ptr_data, rows_to_write); + util::check(!values_bitset.empty(), "Empty bit set means empty colum and should be processed by the empty column code path."); - if constexpr (is_empty_type(dt)) { - // If we have a column of type {EMPTYVAL, Dim1} and all values of the bitset are set to 1 this means - // that we have a column full of empty arrays. In this case there is no need to proceed further and - // store anything on disc. Empty arrays can be reconstructed given the type descriptor. However, if - // there is at least one "missing" value this means that we're mixing empty arrays and None values. - // In that case we need to save the bitset so that we can distinguish empty array from None during the - // read. - if(values_bitset.size() == values_bitset.count()) { - Column arr_col{TypeDescriptor{DataType::EMPTYVAL, Dimension::Dim2}, Sparsity::PERMITTED}; - agg.set_sparse_block(col, arr_col.release_buffer(), arr_col.release_shapes(), std::move(values_bitset)); - return std::optional(); + if constexpr (is_empty_type(dt)) { + // If we have a column of type {EMPTYVAL, Dim1} and all values of the bitset are set to 1 this means + // that we have a column full of empty arrays. In this case there is no need to proceed further and + // store anything on disc. Empty arrays can be reconstructed given the type descriptor. However, if + // there is at least one "missing" value this means that we're mixing empty arrays and None values. + // In that case we need to save the bitset so that we can distinguish empty array from None during the + // read. + if(values_bitset.size() == values_bitset.count()) { + Column arr_col{TypeDescriptor{DataType::EMPTYVAL, Dimension::Dim2}, Sparsity::PERMITTED}; + agg.set_sparse_block(col, arr_col.release_buffer(), arr_col.release_shapes(), std::move(values_bitset)); + return std::optional(); + } + } + + ssize_t last_logical_row{0}; + const auto column_type_descriptor = TypeDescriptor{tensor.data_type(), Dimension::Dim2}; + TypeDescriptor secondary_type = type_desc; + Column arr_col{column_type_descriptor, Sparsity::PERMITTED}; + for (auto en = values_bitset.first(); en < values_bitset.end(); ++en) { + const auto arr_pos = *en; + const auto row_tensor = convert::obj_to_tensor(ptr_data[arr_pos], false); + const auto row_type_descriptor = TypeDescriptor{row_tensor.data_type(), Dimension::Dim1}; + const std::optional& common_type = has_valid_common_type(row_type_descriptor, secondary_type); + normalization::check( + common_type.has_value(), + "Numpy arrays in the same column must be of compatible types {} {}", + datatype_to_str(secondary_type.data_type()), + datatype_to_str(row_type_descriptor.data_type())); + secondary_type = *common_type; + // TODO: If the input array contains unexpected elements such as None, NaN, string the type + // descriptor will have data_type == BYTES_DYNAMIC64. TypeDescriptor::visit_tag does not have a + // case for it and it will throw exception which is not meaningful. Adding BYTES_DYNAMIC64 in + // TypeDescriptor::visit_tag leads to a bunch of compilation errors spread all over the code. + normalization::check( + is_numeric_type(row_type_descriptor.data_type()) || is_empty_type(row_type_descriptor.data_type()), + "Numpy array type {} is not implemented. Only dense int and float arrays are supported.", + datatype_to_str(row_type_descriptor.data_type()) + ); + row_type_descriptor.visit_tag([&arr_col, &row_tensor, &last_logical_row] (auto tdt) { + using ArrayDataTypeTag = typename decltype(tdt)::DataTypeTag; + using ArrayType = typename ArrayDataTypeTag::raw_type; + if constexpr(is_empty_type(ArrayDataTypeTag::data_type)) { + arr_col.set_empty_array(last_logical_row, row_tensor.ndim()); + } else if constexpr(is_numeric_type(ArrayDataTypeTag::data_type)) { + if(row_tensor.nbytes()) { + TypedTensor typed_tensor{row_tensor}; + arr_col.set_array(last_logical_row, typed_tensor); + } else { + arr_col.set_empty_array(last_logical_row, row_tensor.ndim()); } + } else { + normalization::raise( + "Numpy array type is not implemented. Only dense int and float arrays are supported."); } + }); + last_logical_row++; + } + arr_col.set_type(TypeDescriptor{secondary_type.data_type(), column_type_descriptor.dimension()}); + agg.set_sparse_block(col, arr_col.release_buffer(), arr_col.release_shapes(), std::move(values_bitset)); + return std::optional{}; +} - ssize_t last_logical_row{0}; - const auto column_type_descriptor = TypeDescriptor{tensor.data_type(), Dimension::Dim2}; - TypeDescriptor secondary_type = type_desc; - Column arr_col{column_type_descriptor, Sparsity::PERMITTED}; - for (auto en = values_bitset.first(); en < values_bitset.end(); ++en) { - const auto arr_pos = *en; - const auto row_tensor = convert::obj_to_tensor(ptr_data[arr_pos], false); - const auto row_type_descriptor = TypeDescriptor{row_tensor.data_type(), Dimension::Dim1}; - const std::optional& common_type = has_valid_common_type(row_type_descriptor, secondary_type); - normalization::check( - common_type.has_value(), - "Numpy arrays in the same column must be of compatible types {} {}", - datatype_to_str(secondary_type.data_type()), - datatype_to_str(row_type_descriptor.data_type())); - secondary_type = *common_type; - // TODO: If the input array contains unexpected elements such as None, NaN, string the type - // descriptor will have data_type == BYTES_DYNAMIC64. TypeDescriptor::visit_tag does not have a - // case for it and it will throw exception which is not meaningful. Adding BYTES_DYNAMIC64 in - // TypeDescriptor::visit_tag leads to a bunch of compilation errors spread all over the code. - normalization::check( - is_numeric_type(row_type_descriptor.data_type()) || is_empty_type(row_type_descriptor.data_type()), - "Numpy array type {} is not implemented. Only dense int and float arrays are supported.", - datatype_to_str(row_type_descriptor.data_type()) - ); - row_type_descriptor.visit_tag([&arr_col, &row_tensor, &last_logical_row] (auto tdt) { - using ArrayDataTypeTag = typename decltype(tdt)::DataTypeTag; - using ArrayType = typename ArrayDataTypeTag::raw_type; - if constexpr(is_empty_type(ArrayDataTypeTag::data_type)) { - arr_col.set_empty_array(last_logical_row, row_tensor.ndim()); - } else if constexpr(is_numeric_type(ArrayDataTypeTag::data_type)) { - if(row_tensor.nbytes()) { - TypedTensor typed_tensor{row_tensor}; - arr_col.set_array(last_logical_row, typed_tensor); - } else { - arr_col.set_empty_array(last_logical_row, row_tensor.ndim()); - } - } else { - normalization::raise( - "Numpy array type is not implemented. Only dense int and float arrays are supported."); - } - }); - last_logical_row++; - } - arr_col.set_type(TypeDescriptor{secondary_type.data_type(), column_type_descriptor.dimension()}); - agg.set_sparse_block(col, arr_col.release_buffer(), arr_col.release_shapes(), std::move(values_bitset)); +template +std::optional aggregator_set_data( + const TypeDescriptor& type_desc, + const entity::NativeTensor& tensor, + Aggregator& agg, + size_t col, + size_t rows_to_write, + size_t row, + size_t slice_num, + size_t regular_slice_size, + bool sparsify_floats) { + return type_desc.visit_tag([&](auto tag) { + using TagType = std::decay_t; + using RawType = typename TagType::DataTypeTag::raw_type; + constexpr auto dt = std::decay_t::DataTypeTag::data_type; + + util::check(type_desc.data_type() == tensor.data_type(), "Type desc {} != {} tensor type", type_desc.data_type(),tensor.data_type()); + util::check(type_desc.data_type() == dt, "Type desc {} != {} static type", type_desc.data_type(), dt); + + if constexpr (is_sequence_type(dt)) { + normalization::check(tag.dimension() == Dimension::Dim0, "Multidimensional string types are not supported."); + auto maybe_error = set_sequence_type(agg, tensor, col, rows_to_write, row, slice_num, regular_slice_size); + if(maybe_error) + return maybe_error; + } else if constexpr ((is_numeric_type(dt) || is_bool_type(dt)) && tag.dimension() == Dimension::Dim0) { + set_integral_scalar_type(agg, tensor, col, rows_to_write, row, slice_num, regular_slice_size, sparsify_floats); + } else if constexpr(is_bool_object_type(dt)) { + normalization::check(tag.dimension() == Dimension::Dim0, "Multidimensional nullable booleans are not supported"); + set_bool_object_type(agg, tensor, col, rows_to_write, row, slice_num, regular_slice_size); + } else if constexpr(is_array_type(TypeDescriptor(tag))) { + auto maybe_error = set_array_type(type_desc, agg, tensor, col, rows_to_write, row); + if(maybe_error) + return maybe_error; } else if constexpr(tag.dimension() == Dimension::Dim2) { normalization::raise( "Trying to add matrix of base type {}. Matrix types are not supported.", diff --git a/cpp/arcticdb/pipeline/write_frame.cpp b/cpp/arcticdb/pipeline/write_frame.cpp index 29205880d1..3379f3c3ea 100644 --- a/cpp/arcticdb/pipeline/write_frame.cpp +++ b/cpp/arcticdb/pipeline/write_frame.cpp @@ -104,6 +104,10 @@ std::tuple WriteToS } agg.end_block_write(rows_to_write); + + if(ConfigsMap().instance()->get_int("Statistics.GenerateOnWrite", 0) == 1) + agg.segment().calculate_statistics(); + agg.finalize(); return output; }); diff --git a/cpp/arcticdb/storage/memory_layout.hpp b/cpp/arcticdb/storage/memory_layout.hpp index 05b6431549..5a3cceb274 100644 --- a/cpp/arcticdb/storage/memory_layout.hpp +++ b/cpp/arcticdb/storage/memory_layout.hpp @@ -15,6 +15,13 @@ namespace arcticdb { #pragma pack(push) #pragma pack(1) +enum class SortedValue : uint8_t { + UNKNOWN = 0, + UNSORTED = 1, + ASCENDING = 2, + DESCENDING = 3, +}; + constexpr size_t encoding_size = 6; // And extendable list of codecs supported by ArcticDB // N.B. this list is likely to change @@ -95,6 +102,26 @@ enum class BitmapFormat : uint8_t { BITMAGIC }; +enum class UniqueCountType : uint8_t { + PRECISE, + HYPERLOGLOG, +}; + +struct FieldStats { + + FieldStats() = default; + + uint64_t min_ = 0UL; + uint64_t max_ = 0UL; + uint32_t unique_count_ = 0UL; + UniqueCountType unique_count_precision_ = UniqueCountType::PRECISE; + SortedValue sorted_ = SortedValue::UNKNOWN; + uint8_t set_ = 0U; + bool unused_ = false; +}; + +static_assert(sizeof(FieldStats) == 24); + // Each encoded field will have zero or one shapes blocks, // a potentially large number of values (data) blocks, and // an optional sparse bitmap. The block array serves as a @@ -106,10 +133,11 @@ struct EncodedField { uint32_t sparse_map_bytes_ = 0u; uint32_t items_count_ = 0u; BitmapFormat format_ = BitmapFormat::UNKNOWN; + FieldStats stats_; std::array blocks_; }; -static_assert(sizeof(EncodedField) == 64); +static_assert(sizeof(EncodedField) == 88); enum class EncodingVersion : uint16_t { V1 = 0, @@ -157,13 +185,6 @@ struct HeaderData { ; FieldBuffer field_buffer_; }; -// Indicates the sortedness of this segment -enum class SortedValue : uint8_t { - UNKNOWN = 0, - UNSORTED = 1, - ASCENDING = 2, - DESCENDING = 3, -}; // Dynamic schema frames can change their schema over time, // adding and removing columns and changing types. A dynamic diff --git a/cpp/arcticdb/storage/storages.hpp b/cpp/arcticdb/storage/storages.hpp index d3dd7e3cc8..116ac36c5e 100644 --- a/cpp/arcticdb/storage/storages.hpp +++ b/cpp/arcticdb/storage/storages.hpp @@ -293,4 +293,4 @@ inline std::shared_ptr create_storages(const LibraryPath& library_path return std::make_shared(std::move(storages), mode); } -} //namespace arcticdb::storage +} //namespace arcticdb::storage \ No newline at end of file diff --git a/cpp/arcticdb/stream/protobuf_mappings.cpp b/cpp/arcticdb/stream/protobuf_mappings.cpp index be9e69322b..7a0c5a4e28 100644 --- a/cpp/arcticdb/stream/protobuf_mappings.cpp +++ b/cpp/arcticdb/stream/protobuf_mappings.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -67,13 +68,25 @@ FrameDescriptorImpl frame_descriptor_from_proto(arcticdb::proto::descriptors::Ti return output; } +inline SortedValue sorted_value_from_proto(arcticdb::proto::descriptors::SortedValue sorted_proto) { + switch (sorted_proto) { + case arcticdb::proto::descriptors::SortedValue::UNSORTED: + return SortedValue::UNSORTED; + case arcticdb::proto::descriptors::SortedValue::DESCENDING: + return SortedValue::DESCENDING; + case arcticdb::proto::descriptors::SortedValue::ASCENDING: + return SortedValue::ASCENDING; + default: + return SortedValue::UNKNOWN; + } +} SegmentDescriptorImpl segment_descriptor_from_proto(const arcticdb::proto::descriptors::StreamDescriptor& desc) { SegmentDescriptorImpl output; - output.sorted_ = SortedValue(desc.sorted()); output.compressed_bytes_ = desc.out_bytes(); output.uncompressed_bytes_ = desc.in_bytes(); output.row_count_ = desc.row_count(); output.index_ = IndexDescriptor(IndexDescriptor::Type(desc.index().kind()), desc.index().field_count()); + output.sorted_ = sorted_value_from_proto(desc.sorted()); return output; } @@ -81,4 +94,46 @@ StreamId stream_id_from_proto(const arcticdb::proto::descriptors::StreamDescript return desc.id_case() == desc.kNumId ? StreamId(desc.num_id()) : StreamId(desc.str_id()); } + +void field_stats_to_proto(const FieldStatsImpl& stats, arcticdb::proto::encoding::FieldStats& msg) { + msg.set_min(stats.min_); + msg.set_max(stats.max_); + msg.set_unique_count(stats.unique_count_); + msg.set_set(stats.set_); + + switch(stats.unique_count_precision_) { + case UniqueCountType::PRECISE: + msg.set_unique_count_precision(arcticdb::proto::encoding::FieldStats::PRECISE); + break; + case UniqueCountType::HYPERLOGLOG: + msg.set_unique_count_precision(arcticdb::proto::encoding::FieldStats::HYPERLOGLOG); + break; + default: + util::raise_rte("Unknown value in field stats unique count precision"); + } +} + +void field_stats_from_proto(const arcticdb::proto::encoding::FieldStats& msg, FieldStatsImpl& stats) { + stats.min_ = msg.min(); + stats.max_ = msg.max(); + stats.unique_count_ = msg.unique_count(); + stats.set_ = static_cast(msg.set()); + + switch(msg.unique_count_precision()) { + case arcticdb::proto::encoding::FieldStats::PRECISE: + stats.unique_count_precision_ = UniqueCountType::PRECISE; + break; + case arcticdb::proto::encoding::FieldStats::HYPERLOGLOG: + stats.unique_count_precision_ = UniqueCountType::HYPERLOGLOG; + break; + default: + util::raise_rte("Unknown unique count in field stats"); + } +} + +FieldStatsImpl create_from_proto(const arcticdb::proto::encoding::FieldStats& msg) { + FieldStatsImpl stats; + field_stats_from_proto(msg, stats); + return stats; +} } //namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/stream/protobuf_mappings.hpp b/cpp/arcticdb/stream/protobuf_mappings.hpp index da5f4473dd..b4cd19fea1 100644 --- a/cpp/arcticdb/stream/protobuf_mappings.hpp +++ b/cpp/arcticdb/stream/protobuf_mappings.hpp @@ -9,6 +9,7 @@ #include #include +#include #include @@ -38,4 +39,10 @@ StreamId stream_id_from_proto(const arcticdb::proto::descriptors::StreamDescript size_t num_blocks(const arcticdb::proto::encoding::EncodedField& field); +void field_stats_to_proto(const FieldStatsImpl& stats, arcticdb::proto::encoding::FieldStats& msg); + +void field_stats_from_proto(const arcticdb::proto::encoding::FieldStats& msg, FieldStatsImpl& stats); + +FieldStatsImpl create_from_proto(const arcticdb::proto::encoding::FieldStats& msg); + } //namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/stream/test/test_protobuf_mappings.cpp b/cpp/arcticdb/stream/test/test_protobuf_mappings.cpp new file mode 100644 index 0000000000..367d9518ea --- /dev/null +++ b/cpp/arcticdb/stream/test/test_protobuf_mappings.cpp @@ -0,0 +1,69 @@ +#include +#include + +TEST(FieldStatsTest, ProtoConversion) { + using namespace arcticdb; + // Create a FieldStatsImpl with some values + FieldStatsImpl field_stats; + field_stats.set_max(100); + field_stats.set_min(1); + field_stats.set_unique(50, UniqueCountType::PRECISE); + + // Convert to proto + arcticdb::proto::encoding::FieldStats msg; + field_stats_to_proto(field_stats, msg); + + // Verify proto values + EXPECT_EQ(msg.max(), 100); + EXPECT_EQ(msg.min(), 1); + EXPECT_EQ(msg.unique_count(), 50); + EXPECT_EQ(msg.unique_count_precision(), arcticdb::proto::encoding::FieldStats::PRECISE); + EXPECT_EQ(msg.set(), field_stats.set_); + + // Convert back to FieldStatsImpl + FieldStatsImpl roundtrip; + field_stats_from_proto(msg, roundtrip); + + // Verify roundtrip values + EXPECT_EQ(roundtrip.max_, field_stats.max_); + EXPECT_EQ(roundtrip.min_, field_stats.min_); + EXPECT_EQ(roundtrip.unique_count_, field_stats.unique_count_); + EXPECT_EQ(roundtrip.unique_count_precision_, field_stats.unique_count_precision_); + EXPECT_EQ(roundtrip.set_, field_stats.set_); +} + +TEST(FieldStatsTest, ProtoConversionHyperLogLog) { + using namespace arcticdb; + FieldStatsImpl field_stats; + field_stats.set_unique(1000, UniqueCountType::HYPERLOGLOG); + + arcticdb::proto::encoding::FieldStats msg; + field_stats_to_proto(field_stats, msg); + + EXPECT_EQ(msg.unique_count_precision(), arcticdb::proto::encoding::FieldStats::HYPERLOGLOG); + + FieldStatsImpl roundtrip; + field_stats_from_proto(msg, roundtrip); + + EXPECT_EQ(roundtrip.unique_count_precision_, UniqueCountType::HYPERLOGLOG); + EXPECT_EQ(roundtrip.unique_count_, 1000); +} + +TEST(FieldStatsTest, CreateFromProto) { + using namespace arcticdb; + + arcticdb::proto::encoding::FieldStats msg; + msg.set_max(100); + msg.set_min(1); + msg.set_unique_count(50); + msg.set_unique_count_precision(arcticdb::proto::encoding::FieldStats::PRECISE); + msg.set_set(7); // Example value with multiple flags set + + FieldStatsImpl stats = create_from_proto(msg); + + EXPECT_EQ(stats.max_, 100); + EXPECT_EQ(stats.min_, 1); + EXPECT_EQ(stats.unique_count_, 50); + EXPECT_EQ(stats.unique_count_precision_, UniqueCountType::PRECISE); + EXPECT_EQ(stats.set_, 7); +} diff --git a/cpp/arcticdb/version/test/test_version_store.cpp b/cpp/arcticdb/version/test/test_version_store.cpp index 9f51ff98da..c5bc5abf87 100644 --- a/cpp/arcticdb/version/test/test_version_store.cpp +++ b/cpp/arcticdb/version/test/test_version_store.cpp @@ -852,4 +852,4 @@ TEST(VersionStore, TestWriteAppendMapHead) { auto [next_key, total_rows] = read_head(version_store._test_get_store(), symbol); ASSERT_EQ(next_key, key); ASSERT_EQ(total_rows, num_rows); -} +} \ No newline at end of file diff --git a/cpp/arcticdb/version/version_store_api.hpp b/cpp/arcticdb/version/version_store_api.hpp index 0ace64bf73..58f1dea0a0 100644 --- a/cpp/arcticdb/version/version_store_api.hpp +++ b/cpp/arcticdb/version/version_store_api.hpp @@ -42,9 +42,8 @@ using namespace arcticdb::entity; namespace as = arcticdb::stream; /** - * PythonVersionStore contains all the Python cruft that isn't portable, as well as non-essential features that are - * part of the backwards-compatibility with Arctic Python but that we think are random/a bad idea and aren't part of - * the main product. + * The purpose of this class is to perform python-specific translations into either native C++ or protobuf objects + * so that the LocalVersionedEngine contains only partable C++ code. */ class PythonVersionStore : public LocalVersionedEngine { @@ -73,7 +72,7 @@ class PythonVersionStore : public LocalVersionedEngine { VersionedItem write_versioned_composite_data( const StreamId& stream_id, const py::object &metastruct, - const std::vector &sub_keys, // TODO: make this optional? + const std::vector &sub_keys, const std::vector &items, const std::vector &norm_metas, const py::object &user_meta, diff --git a/cpp/proto/arcticc/pb2/encoding.proto b/cpp/proto/arcticc/pb2/encoding.proto index f2b7c262c4..d2a63d0a0c 100644 --- a/cpp/proto/arcticc/pb2/encoding.proto +++ b/cpp/proto/arcticc/pb2/encoding.proto @@ -34,6 +34,21 @@ message SegmentHeader { uint32 encoding_version = 13; } +message FieldStats { + uint64 min = 1; + uint64 max = 2; + uint32 unique_count = 3; + bool sorted = 4; + uint32 set = 5; + + enum UniqueCountPrecision { + PRECISE = 0; + HYPERLOGLOG = 1; + } + + UniqueCountPrecision unique_count_precision = 6; +}; + message EncodedField { /* Captures the variant field encoding */ oneof encoding { @@ -42,6 +57,7 @@ message EncodedField { } uint32 offset = 4; uint32 num_elements = 5; + FieldStats stats = 6; } message VariantCodec { From aea49d223d55b0a34e81a94701465f4a81a2c0f0 Mon Sep 17 00:00:00 2001 From: William Dealtry Date: Tue, 14 Jan 2025 10:01:12 +0000 Subject: [PATCH 2/5] Vectorized min and max --- cpp/arcticdb/CMakeLists.txt | 2 +- cpp/arcticdb/util/min_max.hpp | 115 ++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 cpp/arcticdb/util/min_max.hpp diff --git a/cpp/arcticdb/CMakeLists.txt b/cpp/arcticdb/CMakeLists.txt index f236731c33..7a9dca56f8 100644 --- a/cpp/arcticdb/CMakeLists.txt +++ b/cpp/arcticdb/CMakeLists.txt @@ -523,7 +523,7 @@ set(arcticdb_srcs version/version_core.cpp version/version_store_api.cpp version/version_utils.cpp - version/version_map_batch_methods.cpp) + version/version_map_batch_methods.cpp util/min_max.hpp) add_library(arcticdb_core_object OBJECT ${arcticdb_srcs}) diff --git a/cpp/arcticdb/util/min_max.hpp b/cpp/arcticdb/util/min_max.hpp new file mode 100644 index 0000000000..ae8330769a --- /dev/null +++ b/cpp/arcticdb/util/min_max.hpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include + +template +using vector_type = T __attribute__((vector_size(64))); + +template +struct MinMax { + T min; + T max; +}; + +template +struct is_supported_type : std::false_type {}; + +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; + +template +class MinMaxFinder { + static_assert(is_supported_type::value, "Unsupported type"); + +public: + static MinMax find(const T* data, size_t n) { + using VectorType = vector_type; + + VectorType vec_min; + VectorType vec_max; + T min_val; + T max_val; + + if constexpr(std::is_floating_point_v) { + vec_min = VectorType{std::numeric_limits::infinity()}; + vec_max = VectorType{-std::numeric_limits::infinity()}; + min_val = std::numeric_limits::infinity(); + max_val = -std::numeric_limits::infinity(); + } + else if constexpr(std::is_signed_v) { + vec_min = VectorType{std::numeric_limits::max()}; + vec_max = VectorType{std::numeric_limits::min()}; + min_val = std::numeric_limits::max(); + max_val = std::numeric_limits::min(); + } else { + vec_min = VectorType{std::numeric_limits::max()}; + vec_max = VectorType{0}; + min_val = std::numeric_limits::max(); + max_val = 0; + } + + const VectorType* vdata = reinterpret_cast(data); + const size_t elements_per_vector = sizeof(VectorType) / sizeof(T); + const size_t vlen = n / elements_per_vector; + + for(size_t i = 0; i < vlen; i++) { + VectorType v = vdata[i]; + if constexpr(std::is_floating_point_v) { + VectorType mask = v == v; + v = __builtin_choose_expr(mask, v, vec_min); + vec_min = __builtin_min(vec_min, v); + vec_max = __builtin_max(vec_max, v); + } else { + vec_min = __builtin_min(vec_min, v); + vec_max = __builtin_max(vec_max, v); + } + } + + const T* min_arr = reinterpret_cast(&vec_min); + const T* max_arr = reinterpret_cast(&vec_max); + + for(size_t i = 0; i < elements_per_vector; i++) { + if constexpr(std::is_floating_point_v) { + // Skip NaN values in reduction + if (min_arr[i] == min_arr[i]) { // Not NaN + min_val = std::min(min_val, min_arr[i]); + } + if (max_arr[i] == max_arr[i]) { // Not NaN + max_val = std::max(max_val, max_arr[i]); + } + } else { + min_val = std::min(min_val, min_arr[i]); + max_val = std::max(max_val, max_arr[i]); + } + } + + const T* remain = data + (vlen * elements_per_vector); + for(size_t i = 0; i < n % elements_per_vector; i++) { + if constexpr(std::is_floating_point_v) { + if (remain[i] == remain[i]) { // !NaN + min_val = std::min(min_val, remain[i]); + max_val = std::max(max_val, remain[i]); + } + } else { + min_val = std::min(min_val, remain[i]); + max_val = std::max(max_val, remain[i]); + } + } + + return {min_val, max_val}; + } +}; + +template +MinMax find_min_max(const T* data, size_t n) { + return MinMaxFinder::find(data, n); +} From 316182c573e5d813b5a1dede2da3f91b1a0fc1d0 Mon Sep 17 00:00:00 2001 From: William Dealtry Date: Tue, 14 Jan 2025 14:43:48 +0000 Subject: [PATCH 3/5] More aggregation code --- cpp/arcticdb/CMakeLists.txt | 10 +- cpp/arcticdb/util/mean.hpp | 102 +++++++ cpp/arcticdb/util/min_max.hpp | 115 -------- cpp/arcticdb/util/min_max_float.hpp | 124 ++++++++ cpp/arcticdb/util/min_max_integer.hpp | 201 +++++++++++++ cpp/arcticdb/util/sum.hpp | 73 +++++ cpp/arcticdb/util/test/test_min_max_float.cpp | 91 ++++++ .../util/test/test_min_max_integer.cpp | 262 +++++++++++++++++ cpp/arcticdb/util/test/test_sum.cpp | 264 ++++++++++++++++++ 9 files changed, 1122 insertions(+), 120 deletions(-) create mode 100644 cpp/arcticdb/util/mean.hpp delete mode 100644 cpp/arcticdb/util/min_max.hpp create mode 100644 cpp/arcticdb/util/min_max_float.hpp create mode 100644 cpp/arcticdb/util/min_max_integer.hpp create mode 100644 cpp/arcticdb/util/sum.hpp create mode 100644 cpp/arcticdb/util/test/test_min_max_float.cpp create mode 100644 cpp/arcticdb/util/test/test_min_max_integer.cpp create mode 100644 cpp/arcticdb/util/test/test_sum.cpp diff --git a/cpp/arcticdb/CMakeLists.txt b/cpp/arcticdb/CMakeLists.txt index 7a9dca56f8..bff3d284cf 100644 --- a/cpp/arcticdb/CMakeLists.txt +++ b/cpp/arcticdb/CMakeLists.txt @@ -523,7 +523,7 @@ set(arcticdb_srcs version/version_core.cpp version/version_store_api.cpp version/version_utils.cpp - version/version_map_batch_methods.cpp util/min_max.hpp) + version/version_map_batch_methods.cpp util/min_max_integer.hpp util/mean.hpp util/min_max_float.hpp util/sum.hpp) add_library(arcticdb_core_object OBJECT ${arcticdb_srcs}) @@ -750,8 +750,8 @@ if (SSL_LINK) find_package(OpenSSL REQUIRED) list(APPEND arcticdb_core_libraries OpenSSL::SSL) if (NOT WIN32) - list(APPEND arcticdb_core_libraries ${KERBEROS_LIBRARY}) - list(APPEND arcticdb_core_includes ${KERBEROS_INCLUDE_DIR}) + #list(APPEND arcticdb_core_libraries ${KERBEROS_LIBRARY}) + #list(APPEND arcticdb_core_includes ${KERBEROS_INCLUDE_DIR}) endif() endif () target_link_libraries(arcticdb_core_object PUBLIC ${arcticdb_core_libraries}) @@ -980,7 +980,7 @@ if(${TEST}) version/test/test_version_map_batch.cpp version/test/test_version_store.cpp version/test/version_map_model.hpp - python/python_handlers.cpp) + python/python_handlers.cpp util/test/test_min_max_float.cpp util/test/test_sum.cpp) set(EXECUTABLE_PERMS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) # 755 @@ -1084,7 +1084,7 @@ if(${TEST}) util/test/rapidcheck_string_pool.cpp util/test/rapidcheck_main.cpp util/test/rapidcheck_lru_cache.cpp - version/test/rapidcheck_version_map.cpp) + version/test/rapidcheck_version_map.cpp util/test/test_min_max_integer.cpp) add_executable(arcticdb_rapidcheck_tests ${rapidcheck_srcs}) install(TARGETS arcticdb_rapidcheck_tests RUNTIME diff --git a/cpp/arcticdb/util/mean.hpp b/cpp/arcticdb/util/mean.hpp new file mode 100644 index 0000000000..fd8f72de73 --- /dev/null +++ b/cpp/arcticdb/util/mean.hpp @@ -0,0 +1,102 @@ +#include +#include +#include +#include + + +template +struct is_supported_type : std::false_type {}; + +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; + +template +struct MeanResult { + T mean; + size_t count; // Useful for floating point to know how many non-NaN values +}; + +template +class MeanFinder { + static_assert(is_supported_type::value, "Unsupported type"); + + using VectorType = T __attribute__((vector_size(64))); + +public: + + static MeanResult find(const T* data, size_t n) { + + // Use double for accumulation to avoid overflow and precision loss + using AccumVectorType = double __attribute__((vector_size(64))); + + const size_t elements_per_vector = sizeof(VectorType) / sizeof(T); + const size_t vlen = n / elements_per_vector; + + // Initialize accumulators with zero + AccumVectorType sum_vec = {0}; + AccumVectorType count_vec = {0}; + double total_sum = 0; + size_t valid_count = 0; + + for(size_t i = 0; i < vlen; i++) { + VectorType v = reinterpret_cast(data)[i]; + + if constexpr(std::is_floating_point_v) { + VectorType mask = v == v; // !NaN + VectorType valid = v & mask; + VectorType replaced = VectorType{0} & ~mask; + v = valid | replaced; + + AccumVectorType count_mask; + for(size_t j = 0; j < elements_per_vector; j++) { + count_mask[j] = reinterpret_cast(&mask)[j] != 0 ? 1.0 : 0.0; + } + count_vec += count_mask; + } else { + count_vec += AccumVectorType{1}; + } + + AccumVectorType v_double; + for(size_t j = 0; j < elements_per_vector; j++) { + v_double[j] = static_cast(reinterpret_cast(&v)[j]); + } + sum_vec += v_double; + } + + const double* sum_arr = reinterpret_cast(&sum_vec); + const double* count_arr = reinterpret_cast(&count_vec); + for(size_t i = 0; i < elements_per_vector; i++) { + total_sum += sum_arr[i]; + valid_count += static_cast(count_arr[i]); + } + + const T* remain = data + (vlen * elements_per_vector); + for(size_t i = 0; i < n % elements_per_vector; i++) { + if constexpr(std::is_floating_point_v) { + if (remain[i] == remain[i]) { // Not NaN + total_sum += static_cast(remain[i]); + valid_count++; + } + } else { + total_sum += static_cast(remain[i]); + valid_count++; + } + } + + double mean = valid_count > 0 ? total_sum / valid_count : 0.0; + return {mean, valid_count}; + } +}; + +template +MeanResult find_mean(const T* data, size_t n) { + return MeanFinder::find(data, n); +} diff --git a/cpp/arcticdb/util/min_max.hpp b/cpp/arcticdb/util/min_max.hpp deleted file mode 100644 index ae8330769a..0000000000 --- a/cpp/arcticdb/util/min_max.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#include -#include -#include -#include - -template -using vector_type = T __attribute__((vector_size(64))); - -template -struct MinMax { - T min; - T max; -}; - -template -struct is_supported_type : std::false_type {}; - -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; - -template -class MinMaxFinder { - static_assert(is_supported_type::value, "Unsupported type"); - -public: - static MinMax find(const T* data, size_t n) { - using VectorType = vector_type; - - VectorType vec_min; - VectorType vec_max; - T min_val; - T max_val; - - if constexpr(std::is_floating_point_v) { - vec_min = VectorType{std::numeric_limits::infinity()}; - vec_max = VectorType{-std::numeric_limits::infinity()}; - min_val = std::numeric_limits::infinity(); - max_val = -std::numeric_limits::infinity(); - } - else if constexpr(std::is_signed_v) { - vec_min = VectorType{std::numeric_limits::max()}; - vec_max = VectorType{std::numeric_limits::min()}; - min_val = std::numeric_limits::max(); - max_val = std::numeric_limits::min(); - } else { - vec_min = VectorType{std::numeric_limits::max()}; - vec_max = VectorType{0}; - min_val = std::numeric_limits::max(); - max_val = 0; - } - - const VectorType* vdata = reinterpret_cast(data); - const size_t elements_per_vector = sizeof(VectorType) / sizeof(T); - const size_t vlen = n / elements_per_vector; - - for(size_t i = 0; i < vlen; i++) { - VectorType v = vdata[i]; - if constexpr(std::is_floating_point_v) { - VectorType mask = v == v; - v = __builtin_choose_expr(mask, v, vec_min); - vec_min = __builtin_min(vec_min, v); - vec_max = __builtin_max(vec_max, v); - } else { - vec_min = __builtin_min(vec_min, v); - vec_max = __builtin_max(vec_max, v); - } - } - - const T* min_arr = reinterpret_cast(&vec_min); - const T* max_arr = reinterpret_cast(&vec_max); - - for(size_t i = 0; i < elements_per_vector; i++) { - if constexpr(std::is_floating_point_v) { - // Skip NaN values in reduction - if (min_arr[i] == min_arr[i]) { // Not NaN - min_val = std::min(min_val, min_arr[i]); - } - if (max_arr[i] == max_arr[i]) { // Not NaN - max_val = std::max(max_val, max_arr[i]); - } - } else { - min_val = std::min(min_val, min_arr[i]); - max_val = std::max(max_val, max_arr[i]); - } - } - - const T* remain = data + (vlen * elements_per_vector); - for(size_t i = 0; i < n % elements_per_vector; i++) { - if constexpr(std::is_floating_point_v) { - if (remain[i] == remain[i]) { // !NaN - min_val = std::min(min_val, remain[i]); - max_val = std::max(max_val, remain[i]); - } - } else { - min_val = std::min(min_val, remain[i]); - max_val = std::max(max_val, remain[i]); - } - } - - return {min_val, max_val}; - } -}; - -template -MinMax find_min_max(const T* data, size_t n) { - return MinMaxFinder::find(data, n); -} diff --git a/cpp/arcticdb/util/min_max_float.hpp b/cpp/arcticdb/util/min_max_float.hpp new file mode 100644 index 0000000000..d9c3dd7236 --- /dev/null +++ b/cpp/arcticdb/util/min_max_float.hpp @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include + +namespace arcticdb { + +template +struct is_supported_float : std::false_type {}; + +template +using vector_type __attribute__((vector_size(64))) = T; + +template<> struct is_supported_float : std::true_type {}; +template<> struct is_supported_float : std::true_type {}; + +template +class FloatMinFinder { + static_assert(is_supported_float::value, "Type must be float or double"); + static_assert(std::is_floating_point_v, "Type must be floating point"); + +public: + static T find(const T *data, size_t n) { + using vec_t __attribute__((vector_size(64))) = T; + + vec_t vmin; + for (size_t i = 0; i < sizeof(vec_t) / sizeof(T); i++) { + reinterpret_cast(&vmin)[i] = std::numeric_limits::infinity(); + } + + const vec_t *vdata = reinterpret_cast(data); + const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); + const size_t vlen = n / elements_per_vector; + + for (size_t i = 0; i < vlen; i++) { + vec_t v = vdata[i]; + vec_t mask = v == v; // !NaN + vec_t valid = v & mask; + vec_t replaced = vmin & ~mask; + v = valid | replaced; + vmin = (v < vmin) ? v : vmin; + } + + T min_val = std::numeric_limits::infinity(); + const T *min_arr = reinterpret_cast(&vmin); + for (size_t i = 0; i < elements_per_vector; i++) { + if (min_arr[i] == min_arr[i]) { // Not NaN + min_val = std::min(min_val, min_arr[i]); + } + } + + const T *remain = data + (vlen * elements_per_vector); + for (size_t i = 0; i < n % elements_per_vector; i++) { + if (remain[i] == remain[i]) { // Not NaN + min_val = std::min(min_val, remain[i]); + } + } + + return min_val; + } +}; + +template +class FloatMaxFinder { + static_assert(is_supported_float::value, "Type must be float or double"); + static_assert(std::is_floating_point_v, "Type must be floating point"); + +public: + static T find(const T *data, size_t n) { + using vec_t = vector_type; + + // Initialize max vector with negative infinity + vec_t vmax; + for (size_t i = 0; i < sizeof(vec_t) / sizeof(T); i++) { + reinterpret_cast(&vmax)[i] = -std::numeric_limits::infinity(); + } + + const vec_t *vdata = reinterpret_cast(data); + const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); + const size_t vlen = n / elements_per_vector; + + // Main SIMD loop + for (size_t i = 0; i < vlen; i++) { + vec_t v = vdata[i]; + // Create mask for non-NaN values + vec_t mask = v == v; // false for NaN + vec_t valid = v & mask; + vec_t replaced = vmax & ~mask; + v = valid | replaced; + // Vector max + vmax = (v > vmax) ? v : vmax; + } + + T max_val = -std::numeric_limits::infinity(); + const T *max_arr = reinterpret_cast(&vmax); + for (size_t i = 0; i < elements_per_vector; i++) { + if (max_arr[i] == max_arr[i]) { // Not NaN + max_val = std::max(max_val, max_arr[i]); + } + } + + const T *remain = data + (vlen * elements_per_vector); + for (size_t i = 0; i < n % elements_per_vector; i++) { + if (remain[i] == remain[i]) { // Not NaN + max_val = std::max(max_val, remain[i]); + } + } + + return max_val; + } +}; + +template +T find_float_min(const T *data, size_t n) { + return FloatMinFinder::find(data, n); +} + +template +T find_float_max(const T *data, size_t n) { + return FloatMaxFinder::find(data, n); +} + +} // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/min_max_integer.hpp b/cpp/arcticdb/util/min_max_integer.hpp new file mode 100644 index 0000000000..f178b60a32 --- /dev/null +++ b/cpp/arcticdb/util/min_max_integer.hpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include + +namespace arcticdb { + +#include +#include +#include + +// Check compiler support for vector extensions +#if defined(__GNUC__) || defined(__clang__) +#define HAS_VECTOR_EXTENSIONS 1 +#else +#define HAS_VECTOR_EXTENSIONS 0 +#endif + +#include +#include +#include + +template +using vector_type = T __attribute__((vector_size(64))); + +template +struct is_supported_int : std::false_type {}; + +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; + +template +struct MinMax { + T min; + T max; +}; + +template +class MinMaxFinder { + static_assert(is_supported_int::value, "Type must be integer"); + static_assert(std::is_integral_v, "Type must be integral"); + +public: + static MinMax find(const T* data, size_t n) { + using vec_t = vector_type; + + vec_t vmin, vmax; + T min_val, max_val; + + if constexpr(std::is_signed_v) { + min_val = std::numeric_limits::max(); + max_val = std::numeric_limits::min(); + } else { + min_val = std::numeric_limits::max(); + max_val = 0; + } + + for(size_t i = 0; i < sizeof(vec_t)/sizeof(T); i++) { + reinterpret_cast(&vmin)[i] = min_val; + reinterpret_cast(&vmax)[i] = max_val; + } + + const vec_t* vdata = reinterpret_cast(data); + const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); + const size_t vlen = n / elements_per_vector; + + for(size_t i = 0; i < vlen; i++) { + vec_t v = vdata[i]; + vmin = (v < vmin) ? v : vmin; + vmax = (v > vmax) ? v : vmax; + } + + const T* min_arr = reinterpret_cast(&vmin); + const T* max_arr = reinterpret_cast(&vmax); + + min_val = min_arr[0]; + max_val = max_arr[0]; + for(size_t i = 1; i < elements_per_vector; i++) { + min_val = std::min(min_val, min_arr[i]); + max_val = std::max(max_val, max_arr[i]); + } + + const T* remain = data + (vlen * elements_per_vector); + for(size_t i = 0; i < n % elements_per_vector; i++) { + min_val = std::min(min_val, remain[i]); + max_val = std::max(max_val, remain[i]); + } + + return {min_val, max_val}; + } +}; + +template +class MinFinder { + static_assert(is_supported_int::value, "Type must be integer"); + static_assert(std::is_integral_v, "Type must be integral"); + +public: + static T find(const T* data, size_t n) { + using vec_t = vector_type; + + vec_t vmin; + T min_val = std::numeric_limits::max(); + + for(size_t i = 0; i < sizeof(vec_t)/sizeof(T); i++) { + reinterpret_cast(&vmin)[i] = min_val; + } + + const vec_t* vdata = reinterpret_cast(data); + const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); + const size_t vlen = n / elements_per_vector; + + for(size_t i = 0; i < vlen; i++) { + vec_t v = vdata[i]; + vmin = (v < vmin) ? v : vmin; + } + + const T* min_arr = reinterpret_cast(&vmin); + min_val = min_arr[0]; + for(size_t i = 1; i < elements_per_vector; i++) { + min_val = std::min(min_val, min_arr[i]); + } + + const T* remain = data + (vlen * elements_per_vector); + for(size_t i = 0; i < n % elements_per_vector; i++) { + min_val = std::min(min_val, remain[i]); + } + + return min_val; + } +}; + +template +class MaxFinder { + static_assert(is_supported_int::value, "Type must be integer"); + static_assert(std::is_integral_v, "Type must be integral"); + +public: + static T find(const T* data, size_t n) { + using vec_t = vector_type; + + vec_t vmax; + T max_val; + + if constexpr(std::is_signed_v) { + max_val = std::numeric_limits::min(); + } else { + max_val = 0; + } + + for(size_t i = 0; i < sizeof(vec_t)/sizeof(T); i++) { + reinterpret_cast(&vmax)[i] = max_val; + } + + const vec_t* vdata = reinterpret_cast(data); + const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); + const size_t vlen = n / elements_per_vector; + + for(size_t i = 0; i < vlen; i++) { + vec_t v = vdata[i]; + vmax = (v > vmax) ? v : vmax; + } + + const T* max_arr = reinterpret_cast(&vmax); + max_val = max_arr[0]; + for(size_t i = 1; i < elements_per_vector; i++) { + max_val = std::max(max_val, max_arr[i]); + } + + const T* remain = data + (vlen * elements_per_vector); + for(size_t i = 0; i < n % elements_per_vector; i++) { + max_val = std::max(max_val, remain[i]); + } + + return max_val; + } +}; + +template +MinMax find_min_max(const T* data, size_t n) { + return MinMaxFinder::find(data, n); +} + +template +T find_min(const T* data, size_t n) { + return MinFinder::find(data, n); +} + +template +T find_max(const T* data, size_t n) { + return MaxFinder::find(data, n); +} + + +} // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/sum.hpp b/cpp/arcticdb/util/sum.hpp new file mode 100644 index 0000000000..64ec33f907 --- /dev/null +++ b/cpp/arcticdb/util/sum.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace arcticdb { + +template +struct is_supported_type : std::false_type {}; + +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; +template<> struct is_supported_type : std::true_type {}; + +template +class SumFinder { + static_assert(is_supported_type::value, "Unsupported type"); + static_assert(std::is_arithmetic_v, "Type must be numeric"); + +public: + static double find(const T *data, size_t n) { + using vec_t __attribute__((vector_size(64))) = T; + using acc_vec_t __attribute((vector_size(64))) = double; + + acc_vec_t vsum = {0.0}; + const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); + const size_t doubles_per_vector = sizeof(acc_vec_t) / sizeof(double); + + const vec_t *vdata = reinterpret_cast(data); + const size_t vlen = n / elements_per_vector; + + for (size_t i = 0; i < vlen; i++) { + vec_t v = vdata[i]; + + const T *v_arr = reinterpret_cast(&v); + for (size_t j = 0; j < elements_per_vector; j++) { + size_t acc_idx = j % doubles_per_vector; + reinterpret_cast(&vsum)[acc_idx] += + static_cast(v_arr[j]); + } + } + + double total = 0.0; + const double *sum_arr = reinterpret_cast(&vsum); + for (size_t i = 0; i < doubles_per_vector; i++) { + total += sum_arr[i]; + } + + const T *remain = data + (vlen * elements_per_vector); + for (size_t i = 0; i < n % elements_per_vector; i++) { + total += static_cast(remain[i]); + } + + return total; + } +}; + +template +double find_sum(const T *data, size_t n) { + return SumFinder::find(data, n); +} + +} // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/test/test_min_max_float.cpp b/cpp/arcticdb/util/test/test_min_max_float.cpp new file mode 100644 index 0000000000..4d6d10f2c6 --- /dev/null +++ b/cpp/arcticdb/util/test/test_min_max_float.cpp @@ -0,0 +1,91 @@ +#include +#include +#include + +#include + +namespace arcticdb { + +#if defined (__clang__) + +class FloatFinderTest : public ::testing::Test { +protected: + std::mt19937 rng{std::random_device{}()}; + + // Helper to create aligned data + template + std::vector create_aligned_data(size_t n) { + std::vector data(n + 16); // Extra space for alignment + size_t offset = (64 - (reinterpret_cast(data.data()) % 64)) / sizeof(T); + return std::vector(data.data() + offset, data.data() + offset + n); + } +}; + +TEST_F(FloatFinderTest, VectorAlignedFloat) { + auto data = create_aligned_data(64); // One full vector + std::iota(data.begin(), data.end(), 0.0f); + + EXPECT_FLOAT_EQ(find_float_min(data.data(), data.size()), 0.0f); + EXPECT_FLOAT_EQ(find_float_max(data.data(), data.size()), 63.0f); +} + +TEST_F(FloatFinderTest, VectorAlignedDouble) { + auto data = create_aligned_data(32); // One full vector + std::iota(data.begin(), data.end(), 0.0); + + EXPECT_DOUBLE_EQ(find_float_min(data.data(), data.size()), 0.0); + EXPECT_DOUBLE_EQ(find_float_max(data.data(), data.size()), 31.0); +} + +TEST_F(FloatFinderTest, VectorUnalignedSize) { + std::vector data(67); // Non-multiple of vector size + std::iota(data.begin(), data.end(), 0.0f); + + EXPECT_FLOAT_EQ(find_float_min(data.data(), data.size()), 0.0f); + EXPECT_FLOAT_EQ(find_float_max(data.data(), data.size()), 66.0f); +} + +TEST_F(FloatFinderTest, VectorWithNaNs) { + auto data = create_aligned_data(64); + for (size_t i = 0; i < data.size(); i++) { + data[i] = (i % 2 == 0) ? static_cast(i) : + std::numeric_limits::quiet_NaN(); + } + + EXPECT_FLOAT_EQ(find_float_min(data.data(), data.size()), 0.0f); + EXPECT_FLOAT_EQ(find_float_max(data.data(), data.size()), 62.0f); +} + +// Performance test (disabled by default) +TEST_F(FloatFinderTest, DISABLED_LargeArrayPerformance) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::uniform_real_distribution dist(-1000.0f, 1000.0f); + + for (auto &x : data) { + x = dist(rng); + } + + auto start = std::chrono::high_resolution_clock::now(); + auto min_result = find_float_min(data.data(), data.size()); + auto max_result = find_float_max(data.data(), data.size()); + auto end = std::chrono::high_resolution_clock::now(); + + auto duration = std::chrono::duration_cast(end - start); + std::cout << "SIMD time: " << duration.count() << "ms\n"; + + // Compare with std::minmax_element + start = std::chrono::high_resolution_clock::now(); + auto std_result = std::minmax_element(data.begin(), data.end()); + end = std::chrono::high_resolution_clock::now(); + + auto std_duration = std::chrono::duration_cast(end - start); + std::cout << "std::minmax_element time: " << std_duration.count() << "ms\n"; + + EXPECT_FLOAT_EQ(min_result, *std_result.first); + EXPECT_FLOAT_EQ(max_result, *std_result.second); +} + +#endif + +} // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/test/test_min_max_integer.cpp b/cpp/arcticdb/util/test/test_min_max_integer.cpp new file mode 100644 index 0000000000..554905e0a6 --- /dev/null +++ b/cpp/arcticdb/util/test/test_min_max_integer.cpp @@ -0,0 +1,262 @@ +#include +#include +#include +#include "arcticdb/util/min_max_integer.hpp" + +namespace arcticdb { + +TEST(MinMaxFinder, Int8Basic) { + int8_t data[] = {0, 1, 2, 3, 4}; + auto result = find_min_max(data, 5); + EXPECT_EQ(result.min, 0); + EXPECT_EQ(result.max, 4); +} + +TEST(MinMaxFinder, Int8Extremes) { + int8_t data[] = {INT8_MIN, -1, 0, 1, INT8_MAX}; + auto result = find_min_max(data, 5); + EXPECT_EQ(result.min, INT8_MIN); + EXPECT_EQ(result.max, INT8_MAX); +} + +TEST(MinMaxFinder, UInt8Extremes) { + uint8_t data[] = {0, 1, UINT8_MAX - 1, UINT8_MAX}; + auto result = find_min_max(data, 4); + EXPECT_EQ(result.min, 0); + EXPECT_EQ(result.max, UINT8_MAX); +} + +TEST(MinMaxFinder, Int64Basic) { + int64_t data[] = {0, 1, 2, 3, 4}; + auto result = find_min_max(data, 5); + EXPECT_EQ(result.min, 0); + EXPECT_EQ(result.max, 4); +} + +TEST(MinMaxFinder, Int64Extremes) { + int64_t data[] = {INT64_MIN, -1, 0, 1, INT64_MAX}; + auto result = find_min_max(data, 5); + EXPECT_EQ(result.min, INT64_MIN); + EXPECT_EQ(result.max, INT64_MAX); +} +TEST(MinMaxFinder, EmptyArray) { + std::vector data; + auto result = find_min_max(data.data(), 0); + EXPECT_EQ(result.min, std::numeric_limits::max()); + EXPECT_EQ(result.max, std::numeric_limits::min()); +} + +TEST(MinMaxFinder, SingleElement) { + int data[] = {42}; + auto result = find_min_max(data, 1); + EXPECT_EQ(result.min, 42); + EXPECT_EQ(result.max, 42); +} + +// Alignment Tests +TEST(MinMaxFinder, UnalignedSize) { + std::vector data(67); // Non-multiple of SIMD width + std::iota(data.begin(), data.end(), 0); // Fill with 0..66 + auto result = find_min_max(data.data(), data.size()); + EXPECT_EQ(result.min, 0); + EXPECT_EQ(result.max, 66); +} + +TEST(MinMaxFinder, RandomInt32) { + std::mt19937 rng{std::random_device{}()}; + std::vector data(1000); + std::uniform_int_distribution dist(INT32_MIN, INT32_MAX); + + for (auto &x : data) { + x = dist(rng); + } + + auto result = find_min_max(data.data(), data.size()); + auto std_result = std::minmax_element(data.begin(), data.end()); + + EXPECT_EQ(result.min, *std_result.first); + EXPECT_EQ(result.max, *std_result.second); +} + +TEST(MinMaxFinder, MinSignedBasic) { + int32_t data[] = {1, -2, 3, -4, 5}; + EXPECT_EQ(find_min(data, 5), -4); +} + +TEST(MinMaxFinder, MinUnsignedBasic) { + uint32_t data[] = {1, 2, 3, 4, 5}; + EXPECT_EQ(find_min(data, 5), 1); +} + +TEST(MinMaxFinder, MinSignedExtremes) { + int32_t data[] = { + std::numeric_limits::max(), + std::numeric_limits::min(), + 0, -1, 1 + }; + EXPECT_EQ(find_min(data, 5), std::numeric_limits::min()); +} + +TEST(MinMaxFinder, MinUnsignedExtremes) { + uint32_t data[] = { + std::numeric_limits::max(), + 0, 1, + std::numeric_limits::max() - 1 + }; + EXPECT_EQ(find_min(data, 4), 0); +} + +TEST(MinMaxFinder, MinEmptyArray) { + std::vector data; + EXPECT_EQ(find_min(data.data(), 0), std::numeric_limits::max()); +} + +TEST(MinMaxFinder, MinSingleElement) { + int32_t data[] = {42}; + EXPECT_EQ(find_min(data, 1), 42); +} + +// Max Finder Tests +TEST(MinMaxFinder, MaxSignedBasic) { + int32_t data[] = {1, -2, 3, -4, 5}; + EXPECT_EQ(find_max(data, 5), 5); +} + +TEST(MinMaxFinder, MaxUnsignedBasic) { + uint32_t data[] = {1, 2, 3, 4, 5}; + EXPECT_EQ(find_max(data, 5), 5); +} + +TEST(MinMaxFinder, MaxSignedExtremes) { + int32_t data[] = { + std::numeric_limits::max(), + std::numeric_limits::min(), + 0, -1, 1 + }; + EXPECT_EQ(find_max(data, 5), std::numeric_limits::max()); +} + +TEST(MinMaxFinder, MaxUnsignedExtremes) { + uint32_t data[] = { + std::numeric_limits::max(), + 0, 1, + std::numeric_limits::max() - 1 + }; + EXPECT_EQ(find_max(data, 4), std::numeric_limits::max()); +} + +TEST(MinMaxFinder, MaxEmptyArray) { + std::vector data; + EXPECT_EQ(find_max(data.data(), 0), std::numeric_limits::min()); +} + +TEST(MinMaxFinder, MaxSingleElement) { + int32_t data[] = {42}; + EXPECT_EQ(find_max(data, 1), 42); +} + +// Different Integer Types Tests +TEST(MinMaxFinder, Int8Types) { + int8_t data[] = { + std::numeric_limits::min(), + 0, + std::numeric_limits::max() + }; + EXPECT_EQ(find_min(data, 3), std::numeric_limits::min()); + EXPECT_EQ(find_max(data, 3), std::numeric_limits::max()); +} + +TEST(MinMaxFinder, UInt8Types) { + uint8_t data[] = { + 0, + std::numeric_limits::max() / 2, + std::numeric_limits::max() + }; + EXPECT_EQ(find_min(data, 3), 0); + EXPECT_EQ(find_max(data, 3), std::numeric_limits::max()); +} + +TEST(MinMaxFinder, Int64Types) { + int64_t data[] = { + std::numeric_limits::min(), + 0, + std::numeric_limits::max() + }; + EXPECT_EQ(find_min(data, 3), std::numeric_limits::min()); + EXPECT_EQ(find_max(data, 3), std::numeric_limits::max()); +} + +TEST(MinMaxFinder, RandomInt32Separate) { + std::mt19937 rng{std::random_device{}()}; + std::vector data(1000); + std::uniform_int_distribution dist( + std::numeric_limits::min(), + std::numeric_limits::max() + ); + + for(auto& x : data) { + x = dist(rng); + } + + auto min_result = find_min(data.data(), data.size()); + auto max_result = find_max(data.data(), data.size()); + + auto std_min = *std::min_element(data.begin(), data.end()); + auto std_max = *std::max_element(data.begin(), data.end()); + + EXPECT_EQ(min_result, std_min); + EXPECT_EQ(max_result, std_max); +} + +TEST(MinMaxFinder, RandomUInt32) { + std::mt19937 rng{std::random_device{}()}; + std::vector data(1000); + std::uniform_int_distribution dist( + std::numeric_limits::min(), + std::numeric_limits::max() + ); + + for(auto& x : data) { + x = dist(rng); + } + + auto min_result = find_min(data.data(), data.size()); + auto max_result = find_max(data.data(), data.size()); + + auto std_min = *std::min_element(data.begin(), data.end()); + auto std_max = *std::max_element(data.begin(), data.end()); + + EXPECT_EQ(min_result, std_min); + EXPECT_EQ(max_result, std_max); +} + +TEST(MinMaxFinder, Stress) { + std::mt19937 rng{std::random_device{}()}; + constexpr size_t size = 100'000'000; + std::vector data(size); + std::uniform_int_distribution dist(INT32_MIN, INT32_MAX); + + for (auto &x : data) { + x = dist(rng); + } + + auto start = std::chrono::high_resolution_clock::now(); + auto result = find_min_max(data.data(), data.size()); + auto end = std::chrono::high_resolution_clock::now(); + + auto duration = std::chrono::duration_cast(end - start); + std::cout << "Time taken: " << duration.count() << "ms\n"; + + // Compare with std::minmax_element + start = std::chrono::high_resolution_clock::now(); + auto std_result = std::minmax_element(data.begin(), data.end()); + end = std::chrono::high_resolution_clock::now(); + + auto std_duration = std::chrono::duration_cast(end - start); + std::cout << "std::minmax_element time: " << std_duration.count() << "ms\n"; + + EXPECT_EQ(result.min, *std_result.first); + EXPECT_EQ(result.max, *std_result.second); +} + +} //namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/test/test_sum.cpp b/cpp/arcticdb/util/test/test_sum.cpp new file mode 100644 index 0000000000..69a19da9f6 --- /dev/null +++ b/cpp/arcticdb/util/test/test_sum.cpp @@ -0,0 +1,264 @@ +#include +#include +#include +#include + +#include + +namespace arcticdb { + +class SumFinderTest : public ::testing::Test { +protected: + std::mt19937 rng{std::random_device{}()}; + + template + std::vector create_aligned_data(size_t n) { + std::vector data(n + 16); + size_t offset = (64 - (reinterpret_cast(data.data()) % 64)) / sizeof(T); + return std::vector(data.data() + offset, data.data() + offset + n); + } +}; + +TEST_F(SumFinderTest, LargeInt32Sum) { + std::vector data(1000, 1000000); // Would overflow int32 + double expected = 1000.0 * 1000000.0; + EXPECT_DOUBLE_EQ(find_sum(data.data(), data.size()), expected); +} + +TEST_F(SumFinderTest, SmallValuesLargeCount) { + std::vector data(10000, 1); // Many small values + double expected = 10000.0; + EXPECT_DOUBLE_EQ(find_sum(data.data(), data.size()), expected); +} + +TEST_F(SumFinderTest, MixedSizedIntegers) { + { + std::vector data = {100, 100, 100}; + EXPECT_DOUBLE_EQ(find_sum(data.data(), data.size()), 300.0); + } + { + std::vector data = {10000, 10000, 10000}; + EXPECT_DOUBLE_EQ(find_sum(data.data(), data.size()), 30000.0); + } + { + std::vector data = {1000000, 1000000, 1000000}; + EXPECT_DOUBLE_EQ(find_sum(data.data(), data.size()), 3000000.0); + } + { + std::vector data = {1000000000000LL, 1000000000000LL}; + EXPECT_DOUBLE_EQ(find_sum(data.data(), data.size()), 2000000000000.0); + } +} + +TEST_F(SumFinderTest, PrecisionTestSmallAndLarge) { + std::vector data = {1e15, 1.0, -1e15, 1.0}; + EXPECT_DOUBLE_EQ(find_sum(data.data(), data.size()), 2.0); +} + +TEST_F(SumFinderTest, LargeArrayOverflow) { + // Would overflow int64_t, but works with double + auto data = create_aligned_data(1000000); + std::fill(data.begin(), data.end(), 1000000); + + double expected = 1000000.0 * 1000000.0; + EXPECT_DOUBLE_EQ(find_sum(data.data(), data.size()), expected); +} + +TEST_F(SumFinderTest, CompareWithStdAccumulate) { + auto data = create_aligned_data(1000); + std::uniform_int_distribution dist(-1000, 1000); + + for (auto &x : data) + x = dist(rng); + + double simd_sum = find_sum(data.data(), data.size()); + double std_sum = std::accumulate(data.begin(), data.end(), 0.0); + + EXPECT_DOUBLE_EQ(simd_sum, std_sum); +} + +TEST_F(SumFinderTest, UnsignedOverflow) { + std::vector data(1000, UINT32_MAX); + double expected = 1000.0 * static_cast(UINT32_MAX); + EXPECT_DOUBLE_EQ(find_sum(data.data(), data.size()), expected); +} + +class SumStressTest : public ::testing::Test { +protected: + std::mt19937_64 rng{std::random_device{}()}; + + template + std::vector create_aligned_data(size_t n) { + std::vector data(n + 16); + size_t offset = (64 - (reinterpret_cast(data.data()) % 64)) / sizeof(T); + return std::vector(data.data() + offset, data.data() + offset + n); + } + + double naive_sum(const double* data, size_t n) { + double sum = 0.0; + for(size_t i = 0; i < n; i++) { + sum += data[i]; + } + return sum; + } + + double kahan_sum(const double* data, size_t n) { + double sum = 0.0; + double c = 0.0; + + for(size_t i = 0; i < n; i++) { + double y = data[i] - c; + double t = sum + y; + c = (t - sum) - y; + sum = t; + } + return sum; + } + + void run_benchmark(const std::vector& data, const std::string& test_name) { + constexpr int num_runs = 10; + + auto simd_time = std::chrono::microseconds(0); + auto naive_time = std::chrono::microseconds(0); + auto kahan_time = std::chrono::microseconds(0); + + // Result variables + double simd_sum = 0.0; + double naive_sum_result = 0.0; + double kahan_sum_result = 0.0; + + // Run multiple times to get average performance + for(int i = 0; i < num_runs; i++) { + { + auto start = std::chrono::high_resolution_clock::now(); + simd_sum = find_sum(data.data(), data.size()); + auto end = std::chrono::high_resolution_clock::now(); + simd_time += std::chrono::duration_cast(end - start); + } + + { + auto start = std::chrono::high_resolution_clock::now(); + naive_sum_result = naive_sum(data.data(), data.size()); + auto end = std::chrono::high_resolution_clock::now(); + naive_time += std::chrono::duration_cast(end - start); + } + + { + auto start = std::chrono::high_resolution_clock::now(); + kahan_sum_result = kahan_sum(data.data(), data.size()); + auto end = std::chrono::high_resolution_clock::now(); + kahan_time += std::chrono::duration_cast(end - start); + } + } + + // Calculate average times + double simd_avg = simd_time.count() / static_cast(num_runs); + double naive_avg = naive_time.count() / static_cast(num_runs); + double kahan_avg = kahan_time.count() / static_cast(num_runs); + + // Calculate relative errors using Kahan as reference + double simd_error = std::abs((simd_sum - kahan_sum_result) / kahan_sum_result); + double naive_error = std::abs((naive_sum_result - kahan_sum_result) / kahan_sum_result); + + std::cout << "\n" << test_name << " Results:\n" + << std::scientific << std::setprecision(6) + << "SIMD Sum: " << simd_sum << "\n" + << "Naive Sum: " << naive_sum_result << "\n" + << "Kahan Sum: " << kahan_sum_result << "\n" + << "SIMD Error: " << simd_error << "\n" + << "Naive Error: " << naive_error << "\n" + << std::fixed << std::setprecision(2) + << "SIMD Time: " << simd_avg << " µs\n" + << "Naive Time: " << naive_avg << " µs\n" + << "Kahan Time: " << kahan_avg << " µs\n" + << "SIMD Speedup vs Naive: " << naive_avg/simd_avg << "x\n"; + } +}; + +TEST_F(SumStressTest, LargeUniformValues) { + size_t n = 10'000'000; + auto data = create_aligned_data(n); + std::uniform_real_distribution dist(1.0, 2.0); + + for(auto& x : data) { + x = dist(rng); + } + + run_benchmark(data, "Large Uniform Values"); +} + +TEST_F(SumStressTest, WideRangeValues) { + size_t n = 10'000'000; + auto data = create_aligned_data(n); + std::uniform_real_distribution dist(-1e200, 1e200); + + for(auto& x : data) { + x = dist(rng); + } + + run_benchmark(data, "Wide Range Values"); +} + +TEST_F(SumStressTest, AlternatingLargeSmall) { + size_t n = 10'000'000; + auto data = create_aligned_data(n); + + for(size_t i = 0; i < n; i++) { + if(i % 2 == 0) { + data[i] = 1e100; + } else { + data[i] = 1e-100; + } + } + + run_benchmark(data, "Alternating Large/Small Values"); +} + +TEST_F(SumStressTest, KahanChallenging) { + size_t n = 10'000'000; + auto data = create_aligned_data(n); + + // Create a sequence that's challenging for naive summation + data[0] = 1.0; + for(size_t i = 1; i < n; i++) { + data[i] = 1.0e-16; + } + + run_benchmark(data, "Kahan Challenging Sequence"); +} + +TEST_F(SumStressTest, RandomWalk) { + size_t n = 10'000'000; + auto data = create_aligned_data(n); + std::normal_distribution dist(0.0, 1.0); + + data[0] = 0.0; + for(size_t i = 1; i < n; i++) { + data[i] = data[i-1] + dist(rng); + } + + run_benchmark(data, "Random Walk"); +} + +TEST_F(SumStressTest, ExtremeValues) { + size_t n = 10'000'000; + auto data = create_aligned_data(n); + + // Mix of extreme values + for(size_t i = 0; i < n; i++) { + switch(i % 4) { + case 0: data[i] = std::numeric_limits::max() / (n * 2.0); break; + case 1: data[i] = std::numeric_limits::min(); break; + case 2: data[i] = -std::numeric_limits::max() / (n * 2.0); break; + case 3: data[i] = -std::numeric_limits::min(); break; + } + } + + run_benchmark(data, "Extreme Values"); +} + + + + + +} // namespace arcticdb \ No newline at end of file From 291275f79098d2518406a824ce2bd60a5a0ecdeb Mon Sep 17 00:00:00 2001 From: William Dealtry Date: Tue, 14 Jan 2025 21:22:22 +0000 Subject: [PATCH 4/5] Vector fun --- cpp/arcticdb/CMakeLists.txt | 2 +- cpp/arcticdb/util/mean.hpp | 8 +- cpp/arcticdb/util/test/test_mean.cpp | 161 +++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 cpp/arcticdb/util/test/test_mean.cpp diff --git a/cpp/arcticdb/CMakeLists.txt b/cpp/arcticdb/CMakeLists.txt index bff3d284cf..1e5112f1f8 100644 --- a/cpp/arcticdb/CMakeLists.txt +++ b/cpp/arcticdb/CMakeLists.txt @@ -980,7 +980,7 @@ if(${TEST}) version/test/test_version_map_batch.cpp version/test/test_version_store.cpp version/test/version_map_model.hpp - python/python_handlers.cpp util/test/test_min_max_float.cpp util/test/test_sum.cpp) + python/python_handlers.cpp util/test/test_min_max_float.cpp util/test/test_sum.cpp util/test/test_mean.cpp) set(EXECUTABLE_PERMS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) # 755 diff --git a/cpp/arcticdb/util/mean.hpp b/cpp/arcticdb/util/mean.hpp index fd8f72de73..d10311fd86 100644 --- a/cpp/arcticdb/util/mean.hpp +++ b/cpp/arcticdb/util/mean.hpp @@ -32,15 +32,13 @@ class MeanFinder { public: - static MeanResult find(const T* data, size_t n) { + static double find(const T* data, size_t n) { - // Use double for accumulation to avoid overflow and precision loss using AccumVectorType = double __attribute__((vector_size(64))); const size_t elements_per_vector = sizeof(VectorType) / sizeof(T); const size_t vlen = n / elements_per_vector; - // Initialize accumulators with zero AccumVectorType sum_vec = {0}; AccumVectorType count_vec = {0}; double total_sum = 0; @@ -92,11 +90,11 @@ class MeanFinder { } double mean = valid_count > 0 ? total_sum / valid_count : 0.0; - return {mean, valid_count}; + return mean; } }; template -MeanResult find_mean(const T* data, size_t n) { +double find_mean(const T* data, size_t n) { return MeanFinder::find(data, n); } diff --git a/cpp/arcticdb/util/test/test_mean.cpp b/cpp/arcticdb/util/test/test_mean.cpp new file mode 100644 index 0000000000..9659b82b2d --- /dev/null +++ b/cpp/arcticdb/util/test/test_mean.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include + +#include + +namespace arcticdb { + +class MeanFinderTest : public ::testing::Test { +protected: + std::mt19937 rng{std::random_device{}()}; + + template + std::vector create_aligned_data(size_t n) { + std::vector data(n + 16); + size_t offset = (64 - (reinterpret_cast(data.data()) % 64)) / sizeof(T); + return std::vector(data.data() + offset, data.data() + offset + n); + } +}; + +// Basic Tests +TEST_F(MeanFinderTest, BasicInt32) { + std::vector data = {1, 2, 3, 4, 5}; + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 3.0); +} + +TEST_F(MeanFinderTest, BasicUInt32) { + std::vector data = {1, 2, 3, 4, 5}; + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 3.0); +} + +TEST_F(MeanFinderTest, SingleElement) { + std::vector data = {42}; + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 42.0); +} + +TEST_F(MeanFinderTest, EmptyArray) { + std::vector data; + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 0.0); +} + +// Vector Size Tests +TEST_F(MeanFinderTest, VectorSizedArray) { + auto data = create_aligned_data(64); // One full vector + std::iota(data.begin(), data.end(), 0); // Fill with 0..63 + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 31.5); +} + +TEST_F(MeanFinderTest, NonVectorSizedArray) { + std::vector data(67); // Non-multiple of vector size + std::iota(data.begin(), data.end(), 0); // Fill with 0..66 + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 33.0); +} + +// Different Integer Types +TEST_F(MeanFinderTest, Int8Type) { + std::vector data = {-128, 0, 127}; + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), -0.3333333333333333); +} + +TEST_F(MeanFinderTest, UInt8Type) { + std::vector data = {0, 128, 255}; + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 127.66666666666667); +} + +TEST_F(MeanFinderTest, Int64Type) { + std::vector data = { + std::numeric_limits::min(), + 0, + std::numeric_limits::max() + }; + EXPECT_FALSE(std::isnan(find_mean(data.data(), data.size()))); +} + +// Large Numbers +TEST_F(MeanFinderTest, LargeIntegers) { + std::vector data = {1000000, 2000000, 3000000}; + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 2000000.0); +} + +TEST_F(MeanFinderTest, MaxIntegers) { + std::vector data = { + std::numeric_limits::max(), + std::numeric_limits::max() + }; + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), + static_cast(std::numeric_limits::max())); +} + +// Random Data Tests +TEST_F(MeanFinderTest, RandomInt32) { + auto data = create_aligned_data(1000); + std::uniform_int_distribution dist(-1000, 1000); + + for (auto &x : data) { + x = dist(rng); + } + + double expected = std::accumulate(data.begin(), data.end(), 0.0) / data.size(); + EXPECT_NEAR(find_mean(data.data(), data.size()), expected, 1e-10); +} + +TEST_F(MeanFinderTest, RandomUInt32) { + auto data = create_aligned_data(1000); + std::uniform_int_distribution dist(0, 1000); + + for (auto &x : data) { + x = dist(rng); + } + + double expected = std::accumulate(data.begin(), data.end(), 0.0) / data.size(); + EXPECT_NEAR(find_mean(data.data(), data.size()), expected, 1e-10); +} + +// Edge Cases +TEST_F(MeanFinderTest, AllZeros) { + std::vector data(1000, 0); + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 0.0); +} + +TEST_F(MeanFinderTest, AlternatingValues) { + std::vector data(1000); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = (i % 2 == 0) ? 1000 : -1000; + } + EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 0.0); +} + +// Performance Test (disabled by default) +TEST_F(MeanFinderTest, DISABLED_LargeArrayPerformance) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::uniform_int_distribution dist(-1000, 1000); + + for (auto &x : data) { + x = dist(rng); + } + + auto start = std::chrono::high_resolution_clock::now(); + double simd_mean = find_mean(data.data(), data.size()); + auto end = std::chrono::high_resolution_clock::now(); + + auto simd_duration = std::chrono::duration_cast(end - start); + + // Compare with std::accumulate + start = std::chrono::high_resolution_clock::now(); + double std_mean = std::accumulate(data.begin(), data.end(), 0.0) / data.size(); + end = std::chrono::high_resolution_clock::now(); + + auto std_duration = std::chrono::duration_cast(end - start); + + std::cout << "SIMD mean time: " << simd_duration.count() << "ms\n"; + std::cout << "Standard mean time: " << std_duration.count() << "ms\n"; + std::cout << "Speedup: " << static_cast(std_duration.count()) / + static_cast(simd_duration.count()) << "x\n"; + + EXPECT_NEAR(simd_mean, std_mean, 1e-10); +} + +} //namespace arcticdb \ No newline at end of file From df5ab5dcdd49044f3777126b96a0b142594fe59e Mon Sep 17 00:00:00 2001 From: William Dealtry Date: Fri, 17 Jan 2025 12:11:47 +0000 Subject: [PATCH 5/5] More stress tests --- cpp/arcticdb/CMakeLists.txt | 16 +- cpp/arcticdb/column_store/block.hpp | 21 +- cpp/arcticdb/column_store/chunked_buffer.cpp | 4 +- cpp/arcticdb/column_store/chunked_buffer.hpp | 1 - cpp/arcticdb/util/mean.hpp | 89 +++---- cpp/arcticdb/util/min_max_float.hpp | 64 +++-- cpp/arcticdb/util/min_max_integer.hpp | 111 ++++----- cpp/arcticdb/util/sum.hpp | 14 +- cpp/arcticdb/util/test/test_mean.cpp | 33 +-- cpp/arcticdb/util/test/test_min_max_float.cpp | 18 +- .../util/test/test_min_max_integer.cpp | 227 ++++++++++++++++-- cpp/arcticdb/util/test/test_sum.cpp | 9 - cpp/arcticdb/util/vector_common.hpp | 37 +++ 13 files changed, 408 insertions(+), 236 deletions(-) create mode 100644 cpp/arcticdb/util/vector_common.hpp diff --git a/cpp/arcticdb/CMakeLists.txt b/cpp/arcticdb/CMakeLists.txt index 1e5112f1f8..7e3a1b5b29 100644 --- a/cpp/arcticdb/CMakeLists.txt +++ b/cpp/arcticdb/CMakeLists.txt @@ -381,6 +381,11 @@ set(arcticdb_srcs util/lazy.hpp util/type_traits.hpp util/variant.hpp + util/min_max_integer.hpp + util/mean.hpp + util/min_max_float.hpp + util/sum.hpp + util/vector_common.hpp version/de_dup_map.hpp version/op_log.hpp version/schema_checks.hpp @@ -523,7 +528,7 @@ set(arcticdb_srcs version/version_core.cpp version/version_store_api.cpp version/version_utils.cpp - version/version_map_batch_methods.cpp util/min_max_integer.hpp util/mean.hpp util/min_max_float.hpp util/sum.hpp) + version/version_map_batch_methods.cpp ) add_library(arcticdb_core_object OBJECT ${arcticdb_srcs}) @@ -750,8 +755,8 @@ if (SSL_LINK) find_package(OpenSSL REQUIRED) list(APPEND arcticdb_core_libraries OpenSSL::SSL) if (NOT WIN32) - #list(APPEND arcticdb_core_libraries ${KERBEROS_LIBRARY}) - #list(APPEND arcticdb_core_includes ${KERBEROS_INCLUDE_DIR}) + list(APPEND arcticdb_core_libraries ${KERBEROS_LIBRARY}) + list(APPEND arcticdb_core_includes ${KERBEROS_INCLUDE_DIR}) endif() endif () target_link_libraries(arcticdb_core_object PUBLIC ${arcticdb_core_libraries}) @@ -968,6 +973,9 @@ if(${TEST}) util/test/test_storage_lock.cpp util/test/test_string_pool.cpp util/test/test_string_utils.cpp + util/test/test_min_max_float.cpp + util/test/test_sum.cpp + util/test/test_mean.cpp util/test/test_tracing_allocator.cpp version/test/test_append.cpp version/test/test_key_block.cpp @@ -980,7 +988,7 @@ if(${TEST}) version/test/test_version_map_batch.cpp version/test/test_version_store.cpp version/test/version_map_model.hpp - python/python_handlers.cpp util/test/test_min_max_float.cpp util/test/test_sum.cpp util/test/test_mean.cpp) + python/python_handlers.cpp) set(EXECUTABLE_PERMS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) # 755 diff --git a/cpp/arcticdb/column_store/block.hpp b/cpp/arcticdb/column_store/block.hpp index 80a4e65ab1..dcedecf45b 100644 --- a/cpp/arcticdb/column_store/block.hpp +++ b/cpp/arcticdb/column_store/block.hpp @@ -15,7 +15,6 @@ namespace arcticdb { struct MemBlock { - static const size_t Align = 128; static const size_t MinSize = 64; using magic_t = arcticdb::util::MagicNum<'M', 'e', 'm', 'b'>; magic_t magic_; @@ -136,17 +135,21 @@ struct MemBlock { bool owns_external_data_ = false; static const size_t HeaderDataSize = - sizeof(magic_) + // 8 bytes - sizeof(bytes_) + // 8 bytes - sizeof(capacity_) + // 8 bytes + sizeof(magic_) + + sizeof(bytes_) + + sizeof(capacity_) + sizeof(external_data_) + sizeof(offset_) + - sizeof(timestamp_) + + sizeof(timestamp_) + sizeof(owns_external_data_); - uint8_t pad[Align - HeaderDataSize]; - static const size_t HeaderSize = HeaderDataSize + sizeof(pad); - static_assert(HeaderSize == Align); - uint8_t data_[MinSize]; + static const size_t DataAlignment = 64; + static const size_t PadSize = (DataAlignment - (HeaderDataSize % DataAlignment)) % DataAlignment; + + uint8_t pad[PadSize]; + static const size_t HeaderSize = HeaderDataSize + PadSize; + static_assert(HeaderSize % DataAlignment == 0, "Header size must be aligned to 64 bytes"); + + alignas(DataAlignment) uint8_t data_[MinSize]; }; } diff --git a/cpp/arcticdb/column_store/chunked_buffer.cpp b/cpp/arcticdb/column_store/chunked_buffer.cpp index 69c9bdd701..f034323510 100644 --- a/cpp/arcticdb/column_store/chunked_buffer.cpp +++ b/cpp/arcticdb/column_store/chunked_buffer.cpp @@ -68,7 +68,7 @@ std::vector> split(const ChunkedBufferImpl> split(const ChunkedBufferImpl<64>& input, size_t nbytes); -template std::vector> split(const ChunkedBufferImpl<3968>& input, size_t nbytes); +template std::vector> split(const ChunkedBufferImpl<4032ul>& input, size_t nbytes); // Inclusive of start_byte, exclusive of end_byte template @@ -112,6 +112,6 @@ ChunkedBufferImpl truncate(const ChunkedBufferImpl& input, } template ChunkedBufferImpl<64> truncate(const ChunkedBufferImpl<64>& input, size_t start_byte, size_t end_byte); -template ChunkedBufferImpl<3968> truncate(const ChunkedBufferImpl<3968>& input, size_t start_byte, size_t end_byte); +template ChunkedBufferImpl<4032ul> truncate(const ChunkedBufferImpl<4032ul>& input, size_t start_byte, size_t end_byte); } //namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/column_store/chunked_buffer.hpp b/cpp/arcticdb/column_store/chunked_buffer.hpp index 38695ed61a..3074f70d10 100644 --- a/cpp/arcticdb/column_store/chunked_buffer.hpp +++ b/cpp/arcticdb/column_store/chunked_buffer.hpp @@ -39,7 +39,6 @@ class ChunkedBufferImpl { using BlockType = MemBlock; - static_assert(sizeof(BlockType) == BlockType::Align + BlockType::MinSize); static_assert(DefaultBlockSize >= BlockType::MinSize); public: diff --git a/cpp/arcticdb/util/mean.hpp b/cpp/arcticdb/util/mean.hpp index d10311fd86..f381fcc2d9 100644 --- a/cpp/arcticdb/util/mean.hpp +++ b/cpp/arcticdb/util/mean.hpp @@ -3,98 +3,79 @@ #include #include +#include -template -struct is_supported_type : std::false_type {}; - -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; -template<> struct is_supported_type : std::true_type {}; - -template -struct MeanResult { - T mean; - size_t count; // Useful for floating point to know how many non-NaN values -}; +namespace arcticdb { template class MeanFinder { - static_assert(is_supported_type::value, "Unsupported type"); - - using VectorType = T __attribute__((vector_size(64))); + static_assert(is_supported_int::value || is_supported_float::value, "Unsupported type"); public: - static double find(const T* data, size_t n) { + using VectorType = vector_type; + using AccumVectorType = vector_type; - using AccumVectorType = double __attribute__((vector_size(64))); - + AccumVectorType vsum = {0.0}; const size_t elements_per_vector = sizeof(VectorType) / sizeof(T); - const size_t vlen = n / elements_per_vector; + const size_t doubles_per_vector = sizeof(AccumVectorType) / sizeof(double); + const size_t vectors_per_acc = elements_per_vector / doubles_per_vector; - AccumVectorType sum_vec = {0}; - AccumVectorType count_vec = {0}; - double total_sum = 0; size_t valid_count = 0; - for(size_t i = 0; i < vlen; i++) { - VectorType v = reinterpret_cast(data)[i]; + const auto* vdata = reinterpret_cast(data); + const size_t vector_len = n / elements_per_vector; + + for(size_t i = 0; i < vector_len; i++) { + VectorType v = vdata[i]; if constexpr(std::is_floating_point_v) { - VectorType mask = v == v; // !NaN - VectorType valid = v & mask; - VectorType replaced = VectorType{0} & ~mask; - v = valid | replaced; + VectorType mask = v == v; + v = v & mask; - AccumVectorType count_mask; + const T* mask_arr = reinterpret_cast(&mask); for(size_t j = 0; j < elements_per_vector; j++) { - count_mask[j] = reinterpret_cast(&mask)[j] != 0 ? 1.0 : 0.0; + if(mask_arr[j] != 0) valid_count++; } - count_vec += count_mask; } else { - count_vec += AccumVectorType{1}; + valid_count += elements_per_vector; } - AccumVectorType v_double; - for(size_t j = 0; j < elements_per_vector; j++) { - v_double[j] = static_cast(reinterpret_cast(&v)[j]); + const T* v_arr = reinterpret_cast(&v); + for(size_t chunk = 0; chunk < vectors_per_acc; chunk++) { + for(size_t j = 0; j < doubles_per_vector; j++) { + size_t idx = chunk * doubles_per_vector + j; + reinterpret_cast(&vsum)[j] += static_cast(v_arr[idx]); + } } - sum_vec += v_double; } - const double* sum_arr = reinterpret_cast(&sum_vec); - const double* count_arr = reinterpret_cast(&count_vec); - for(size_t i = 0; i < elements_per_vector; i++) { - total_sum += sum_arr[i]; - valid_count += static_cast(count_arr[i]); + double total = 0.0; + const auto* sum_arr = reinterpret_cast(&vsum); + for(size_t i = 0; i < doubles_per_vector; i++) { + total += sum_arr[i]; } - const T* remain = data + (vlen * elements_per_vector); + const T* remain = data + (vector_len * elements_per_vector); for(size_t i = 0; i < n % elements_per_vector; i++) { if constexpr(std::is_floating_point_v) { if (remain[i] == remain[i]) { // Not NaN - total_sum += static_cast(remain[i]); + total += static_cast(remain[i]); valid_count++; } } else { - total_sum += static_cast(remain[i]); + total += static_cast(remain[i]); valid_count++; } } - double mean = valid_count > 0 ? total_sum / valid_count : 0.0; - return mean; + return valid_count > 0 ? total / static_cast(valid_count) : 0.0; } }; template -double find_mean(const T* data, size_t n) { +double find_mean(const T *data, size_t n) { return MeanFinder::find(data, n); } + +} // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/min_max_float.hpp b/cpp/arcticdb/util/min_max_float.hpp index d9c3dd7236..0afa00c9b4 100644 --- a/cpp/arcticdb/util/min_max_float.hpp +++ b/cpp/arcticdb/util/min_max_float.hpp @@ -4,54 +4,51 @@ #include #include -namespace arcticdb { +#include -template -struct is_supported_float : std::false_type {}; +namespace arcticdb { template using vector_type __attribute__((vector_size(64))) = T; -template<> struct is_supported_float : std::true_type {}; -template<> struct is_supported_float : std::true_type {}; - template class FloatMinFinder { static_assert(is_supported_float::value, "Type must be float or double"); static_assert(std::is_floating_point_v, "Type must be floating point"); public: - static T find(const T *data, size_t n) { - using vec_t __attribute__((vector_size(64))) = T; + static T find(const T* data, size_t n) { + using vec_t = vector_type; + // Initialize min vector with infinity vec_t vmin; - for (size_t i = 0; i < sizeof(vec_t) / sizeof(T); i++) { - reinterpret_cast(&vmin)[i] = std::numeric_limits::infinity(); + for(size_t i = 0; i < sizeof(vec_t)/sizeof(T); i++) { + reinterpret_cast(&vmin)[i] = std::numeric_limits::infinity(); } - const vec_t *vdata = reinterpret_cast(data); + // Process full vectors + const vec_t* vdata = reinterpret_cast(data); const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); const size_t vlen = n / elements_per_vector; - for (size_t i = 0; i < vlen; i++) { + // Main SIMD loop + for(size_t i = 0; i < vlen; i++) { vec_t v = vdata[i]; - vec_t mask = v == v; // !NaN - vec_t valid = v & mask; - vec_t replaced = vmin & ~mask; - v = valid | replaced; vmin = (v < vmin) ? v : vmin; } + // Reduce vector to scalar T min_val = std::numeric_limits::infinity(); - const T *min_arr = reinterpret_cast(&vmin); - for (size_t i = 0; i < elements_per_vector; i++) { + const T* min_arr = reinterpret_cast(&vmin); + for(size_t i = 0; i < elements_per_vector; i++) { if (min_arr[i] == min_arr[i]) { // Not NaN min_val = std::min(min_val, min_arr[i]); } } - const T *remain = data + (vlen * elements_per_vector); - for (size_t i = 0; i < n % elements_per_vector; i++) { + // Handle remainder + const T* remain = data + (vlen * elements_per_vector); + for(size_t i = 0; i < n % elements_per_vector; i++) { if (remain[i] == remain[i]) { // Not NaN min_val = std::min(min_val, remain[i]); } @@ -67,41 +64,38 @@ class FloatMaxFinder { static_assert(std::is_floating_point_v, "Type must be floating point"); public: - static T find(const T *data, size_t n) { + static T find(const T* data, size_t n) { using vec_t = vector_type; // Initialize max vector with negative infinity vec_t vmax; - for (size_t i = 0; i < sizeof(vec_t) / sizeof(T); i++) { - reinterpret_cast(&vmax)[i] = -std::numeric_limits::infinity(); + for(size_t i = 0; i < sizeof(vec_t)/sizeof(T); i++) { + reinterpret_cast(&vmax)[i] = -std::numeric_limits::infinity(); } - const vec_t *vdata = reinterpret_cast(data); + // Process full vectors + const vec_t* vdata = reinterpret_cast(data); const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); const size_t vlen = n / elements_per_vector; // Main SIMD loop - for (size_t i = 0; i < vlen; i++) { + for(size_t i = 0; i < vlen; i++) { vec_t v = vdata[i]; - // Create mask for non-NaN values - vec_t mask = v == v; // false for NaN - vec_t valid = v & mask; - vec_t replaced = vmax & ~mask; - v = valid | replaced; - // Vector max vmax = (v > vmax) ? v : vmax; } + // Reduce vector to scalar T max_val = -std::numeric_limits::infinity(); - const T *max_arr = reinterpret_cast(&vmax); - for (size_t i = 0; i < elements_per_vector; i++) { + const T* max_arr = reinterpret_cast(&vmax); + for(size_t i = 0; i < elements_per_vector; i++) { if (max_arr[i] == max_arr[i]) { // Not NaN max_val = std::max(max_val, max_arr[i]); } } - const T *remain = data + (vlen * elements_per_vector); - for (size_t i = 0; i < n % elements_per_vector; i++) { + // Handle remainder + const T* remain = data + (vlen * elements_per_vector); + for(size_t i = 0; i < n % elements_per_vector; i++) { if (remain[i] == remain[i]) { // Not NaN max_val = std::max(max_val, remain[i]); } diff --git a/cpp/arcticdb/util/min_max_integer.hpp b/cpp/arcticdb/util/min_max_integer.hpp index f178b60a32..e5c69b65ca 100644 --- a/cpp/arcticdb/util/min_max_integer.hpp +++ b/cpp/arcticdb/util/min_max_integer.hpp @@ -3,37 +3,13 @@ #include #include -namespace arcticdb { - #include #include #include -// Check compiler support for vector extensions -#if defined(__GNUC__) || defined(__clang__) -#define HAS_VECTOR_EXTENSIONS 1 -#else -#define HAS_VECTOR_EXTENSIONS 0 -#endif +#include -#include -#include -#include - -template -using vector_type = T __attribute__((vector_size(64))); - -template -struct is_supported_int : std::false_type {}; - -template<> struct is_supported_int : std::true_type {}; -template<> struct is_supported_int : std::true_type {}; -template<> struct is_supported_int : std::true_type {}; -template<> struct is_supported_int : std::true_type {}; -template<> struct is_supported_int : std::true_type {}; -template<> struct is_supported_int : std::true_type {}; -template<> struct is_supported_int : std::true_type {}; -template<> struct is_supported_int : std::true_type {}; +namespace arcticdb { template struct MinMax { @@ -48,10 +24,12 @@ class MinMaxFinder { public: static MinMax find(const T* data, size_t n) { - using vec_t = vector_type; + using VectorType = vector_type; - vec_t vmin, vmax; - T min_val, max_val; + VectorType vector_min; + VectorType vector_max; + T min_val; + T max_val; if constexpr(std::is_signed_v) { min_val = std::numeric_limits::max(); @@ -61,23 +39,23 @@ class MinMaxFinder { max_val = 0; } - for(size_t i = 0; i < sizeof(vec_t)/sizeof(T); i++) { - reinterpret_cast(&vmin)[i] = min_val; - reinterpret_cast(&vmax)[i] = max_val; + for(size_t i = 0; i < sizeof(VectorType)/sizeof(T); i++) { + reinterpret_cast(&vector_min)[i] = min_val; + reinterpret_cast(&vector_max)[i] = max_val; } - const vec_t* vdata = reinterpret_cast(data); - const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); - const size_t vlen = n / elements_per_vector; + const auto* vdata = reinterpret_cast(data); + const size_t elements_per_vector = sizeof(VectorType) / sizeof(T); + const size_t vector_len = n / elements_per_vector; - for(size_t i = 0; i < vlen; i++) { - vec_t v = vdata[i]; - vmin = (v < vmin) ? v : vmin; - vmax = (v > vmax) ? v : vmax; + for(size_t i = 0; i < vector_len; i++) { + VectorType v = vdata[i]; + vector_min = (v < vector_min) ? v : vector_min; + vector_max = (v > vector_max) ? v : vector_max; } - const T* min_arr = reinterpret_cast(&vmin); - const T* max_arr = reinterpret_cast(&vmax); + const T* min_arr = reinterpret_cast(&vector_min); + const T* max_arr = reinterpret_cast(&vector_max); min_val = min_arr[0]; max_val = max_arr[0]; @@ -86,7 +64,7 @@ class MinMaxFinder { max_val = std::max(max_val, max_arr[i]); } - const T* remain = data + (vlen * elements_per_vector); + const T* remain = data + (vector_len * elements_per_vector); for(size_t i = 0; i < n % elements_per_vector; i++) { min_val = std::min(min_val, remain[i]); max_val = std::max(max_val, remain[i]); @@ -103,31 +81,31 @@ class MinFinder { public: static T find(const T* data, size_t n) { - using vec_t = vector_type; + using VectorType = vector_type; - vec_t vmin; + VectorType vector_min; T min_val = std::numeric_limits::max(); - for(size_t i = 0; i < sizeof(vec_t)/sizeof(T); i++) { - reinterpret_cast(&vmin)[i] = min_val; + for(size_t i = 0; i < sizeof(VectorType)/sizeof(T); i++) { + reinterpret_cast(&vector_min)[i] = min_val; } - const vec_t* vdata = reinterpret_cast(data); - const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); - const size_t vlen = n / elements_per_vector; + const auto* vdata = reinterpret_cast(data); + const size_t elements_per_vector = sizeof(VectorType) / sizeof(T); + const size_t vector_len = n / elements_per_vector; - for(size_t i = 0; i < vlen; i++) { - vec_t v = vdata[i]; - vmin = (v < vmin) ? v : vmin; + for(size_t i = 0; i < vector_len; i++) { + VectorType v = vdata[i]; + vector_min = (v < vector_min) ? v : vector_min; } - const T* min_arr = reinterpret_cast(&vmin); + const T* min_arr = reinterpret_cast(&vector_min); min_val = min_arr[0]; for(size_t i = 1; i < elements_per_vector; i++) { min_val = std::min(min_val, min_arr[i]); } - const T* remain = data + (vlen * elements_per_vector); + const T* remain = data + (vector_len * elements_per_vector); for(size_t i = 0; i < n % elements_per_vector; i++) { min_val = std::min(min_val, remain[i]); } @@ -143,9 +121,9 @@ class MaxFinder { public: static T find(const T* data, size_t n) { - using vec_t = vector_type; + using VectorType = vector_type; - vec_t vmax; + VectorType vector_max; T max_val; if constexpr(std::is_signed_v) { @@ -154,26 +132,26 @@ class MaxFinder { max_val = 0; } - for(size_t i = 0; i < sizeof(vec_t)/sizeof(T); i++) { - reinterpret_cast(&vmax)[i] = max_val; + for(size_t i = 0; i < sizeof(VectorType)/sizeof(T); i++) { + reinterpret_cast(&vector_max)[i] = max_val; } - const vec_t* vdata = reinterpret_cast(data); - const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); - const size_t vlen = n / elements_per_vector; + const auto* vdata = reinterpret_cast(data); + const size_t elements_per_vector = sizeof(VectorType) / sizeof(T); + const size_t vector_len = n / elements_per_vector; - for(size_t i = 0; i < vlen; i++) { - vec_t v = vdata[i]; - vmax = (v > vmax) ? v : vmax; + for(size_t i = 0; i < vector_len; i++) { + VectorType v = vdata[i]; + vector_max = (v > vector_max) ? v : vector_max; } - const T* max_arr = reinterpret_cast(&vmax); + const auto* max_arr = reinterpret_cast(&vector_max); max_val = max_arr[0]; for(size_t i = 1; i < elements_per_vector; i++) { max_val = std::max(max_val, max_arr[i]); } - const T* remain = data + (vlen * elements_per_vector); + const T* remain = data + (vector_len * elements_per_vector); for(size_t i = 0; i < n % elements_per_vector; i++) { max_val = std::max(max_val, remain[i]); } @@ -197,5 +175,4 @@ T find_max(const T* data, size_t n) { return MaxFinder::find(data, n); } - } // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/sum.hpp b/cpp/arcticdb/util/sum.hpp index 64ec33f907..7a04974490 100644 --- a/cpp/arcticdb/util/sum.hpp +++ b/cpp/arcticdb/util/sum.hpp @@ -29,18 +29,18 @@ class SumFinder { public: static double find(const T *data, size_t n) { - using vec_t __attribute__((vector_size(64))) = T; + using VectorType __attribute__((vector_size(64))) = T; using acc_vec_t __attribute((vector_size(64))) = double; acc_vec_t vsum = {0.0}; - const size_t elements_per_vector = sizeof(vec_t) / sizeof(T); + const size_t elements_per_vector = sizeof(VectorType) / sizeof(T); const size_t doubles_per_vector = sizeof(acc_vec_t) / sizeof(double); - const vec_t *vdata = reinterpret_cast(data); - const size_t vlen = n / elements_per_vector; + const VectorType *vdata = reinterpret_cast(data); + const size_t vector_len = n / elements_per_vector; - for (size_t i = 0; i < vlen; i++) { - vec_t v = vdata[i]; + for (size_t i = 0; i < vector_len; i++) { + VectorType v = vdata[i]; const T *v_arr = reinterpret_cast(&v); for (size_t j = 0; j < elements_per_vector; j++) { @@ -56,7 +56,7 @@ class SumFinder { total += sum_arr[i]; } - const T *remain = data + (vlen * elements_per_vector); + const T *remain = data + (vector_len * elements_per_vector); for (size_t i = 0; i < n % elements_per_vector; i++) { total += static_cast(remain[i]); } diff --git a/cpp/arcticdb/util/test/test_mean.cpp b/cpp/arcticdb/util/test/test_mean.cpp index 9659b82b2d..501668e229 100644 --- a/cpp/arcticdb/util/test/test_mean.cpp +++ b/cpp/arcticdb/util/test/test_mean.cpp @@ -19,7 +19,6 @@ class MeanFinderTest : public ::testing::Test { } }; -// Basic Tests TEST_F(MeanFinderTest, BasicInt32) { std::vector data = {1, 2, 3, 4, 5}; EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 3.0); @@ -42,18 +41,17 @@ TEST_F(MeanFinderTest, EmptyArray) { // Vector Size Tests TEST_F(MeanFinderTest, VectorSizedArray) { - auto data = create_aligned_data(64); // One full vector - std::iota(data.begin(), data.end(), 0); // Fill with 0..63 + auto data = create_aligned_data(64); + std::iota(data.begin(), data.end(), 0); EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 31.5); } TEST_F(MeanFinderTest, NonVectorSizedArray) { - std::vector data(67); // Non-multiple of vector size - std::iota(data.begin(), data.end(), 0); // Fill with 0..66 + std::vector data(67); + std::iota(data.begin(), data.end(), 0); EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 33.0); } -// Different Integer Types TEST_F(MeanFinderTest, Int8Type) { std::vector data = {-128, 0, 127}; EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), -0.3333333333333333); @@ -73,7 +71,6 @@ TEST_F(MeanFinderTest, Int64Type) { EXPECT_FALSE(std::isnan(find_mean(data.data(), data.size()))); } -// Large Numbers TEST_F(MeanFinderTest, LargeIntegers) { std::vector data = {1000000, 2000000, 3000000}; EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 2000000.0); @@ -88,7 +85,6 @@ TEST_F(MeanFinderTest, MaxIntegers) { static_cast(std::numeric_limits::max())); } -// Random Data Tests TEST_F(MeanFinderTest, RandomInt32) { auto data = create_aligned_data(1000); std::uniform_int_distribution dist(-1000, 1000); @@ -97,7 +93,7 @@ TEST_F(MeanFinderTest, RandomInt32) { x = dist(rng); } - double expected = std::accumulate(data.begin(), data.end(), 0.0) / data.size(); + double expected = std::accumulate(data.begin(), data.end(), 0.0) / static_cast(data.size()); EXPECT_NEAR(find_mean(data.data(), data.size()), expected, 1e-10); } @@ -105,15 +101,13 @@ TEST_F(MeanFinderTest, RandomUInt32) { auto data = create_aligned_data(1000); std::uniform_int_distribution dist(0, 1000); - for (auto &x : data) { + for (auto &x : data) x = dist(rng); - } - double expected = std::accumulate(data.begin(), data.end(), 0.0) / data.size(); + double expected = std::accumulate(data.begin(), data.end(), 0.0) / static_cast(data.size()); EXPECT_NEAR(find_mean(data.data(), data.size()), expected, 1e-10); } -// Edge Cases TEST_F(MeanFinderTest, AllZeros) { std::vector data(1000, 0); EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 0.0); @@ -127,15 +121,13 @@ TEST_F(MeanFinderTest, AlternatingValues) { EXPECT_DOUBLE_EQ(find_mean(data.data(), data.size()), 0.0); } -// Performance Test (disabled by default) -TEST_F(MeanFinderTest, DISABLED_LargeArrayPerformance) { - constexpr size_t size = 10'000'000; +TEST_F(MeanFinderTest, LargeArrayPerformance) { + constexpr size_t size = 100'000'000; auto data = create_aligned_data(size); std::uniform_int_distribution dist(-1000, 1000); - for (auto &x : data) { + for (auto &x : data) x = dist(rng); - } auto start = std::chrono::high_resolution_clock::now(); double simd_mean = find_mean(data.data(), data.size()); @@ -143,9 +135,8 @@ TEST_F(MeanFinderTest, DISABLED_LargeArrayPerformance) { auto simd_duration = std::chrono::duration_cast(end - start); - // Compare with std::accumulate start = std::chrono::high_resolution_clock::now(); - double std_mean = std::accumulate(data.begin(), data.end(), 0.0) / data.size(); + double std_mean = std::accumulate(data.begin(), data.end(), 0.0) / static_cast(data.size()); end = std::chrono::high_resolution_clock::now(); auto std_duration = std::chrono::duration_cast(end - start); @@ -158,4 +149,4 @@ TEST_F(MeanFinderTest, DISABLED_LargeArrayPerformance) { EXPECT_NEAR(simd_mean, std_mean, 1e-10); } -} //namespace arcticdb \ No newline at end of file +} // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/test/test_min_max_float.cpp b/cpp/arcticdb/util/test/test_min_max_float.cpp index 4d6d10f2c6..e6450da5a5 100644 --- a/cpp/arcticdb/util/test/test_min_max_float.cpp +++ b/cpp/arcticdb/util/test/test_min_max_float.cpp @@ -6,8 +6,6 @@ namespace arcticdb { -#if defined (__clang__) - class FloatFinderTest : public ::testing::Test { protected: std::mt19937 rng{std::random_device{}()}; @@ -56,15 +54,13 @@ TEST_F(FloatFinderTest, VectorWithNaNs) { EXPECT_FLOAT_EQ(find_float_max(data.data(), data.size()), 62.0f); } -// Performance test (disabled by default) -TEST_F(FloatFinderTest, DISABLED_LargeArrayPerformance) { - constexpr size_t size = 10'000'000; +TEST_F(FloatFinderTest, LargeArrayPerformance) { + constexpr size_t size = 100'000'000; auto data = create_aligned_data(size); std::uniform_real_distribution dist(-1000.0f, 1000.0f); - for (auto &x : data) { + for (auto &x : data) x = dist(rng); - } auto start = std::chrono::high_resolution_clock::now(); auto min_result = find_float_min(data.data(), data.size()); @@ -76,16 +72,16 @@ TEST_F(FloatFinderTest, DISABLED_LargeArrayPerformance) { // Compare with std::minmax_element start = std::chrono::high_resolution_clock::now(); - auto std_result = std::minmax_element(data.begin(), data.end()); + auto std_result_max = std::max_element(data.begin(), data.end()); + auto std_result_min = std::min_element(data.begin(), data.end()); end = std::chrono::high_resolution_clock::now(); auto std_duration = std::chrono::duration_cast(end - start); std::cout << "std::minmax_element time: " << std_duration.count() << "ms\n"; - EXPECT_FLOAT_EQ(min_result, *std_result.first); - EXPECT_FLOAT_EQ(max_result, *std_result.second); + EXPECT_FLOAT_EQ(min_result, *std_result_min); + EXPECT_FLOAT_EQ(max_result, *std_result_max); } -#endif } // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/test/test_min_max_integer.cpp b/cpp/arcticdb/util/test/test_min_max_integer.cpp index 554905e0a6..0e7ab976a9 100644 --- a/cpp/arcticdb/util/test/test_min_max_integer.cpp +++ b/cpp/arcticdb/util/test/test_min_max_integer.cpp @@ -1,7 +1,7 @@ #include #include #include -#include "arcticdb/util/min_max_integer.hpp" +#include namespace arcticdb { @@ -53,10 +53,9 @@ TEST(MinMaxFinder, SingleElement) { EXPECT_EQ(result.max, 42); } -// Alignment Tests TEST(MinMaxFinder, UnalignedSize) { - std::vector data(67); // Non-multiple of SIMD width - std::iota(data.begin(), data.end(), 0); // Fill with 0..66 + std::vector data(67); + std::iota(data.begin(), data.end(), 0); auto result = find_min_max(data.data(), data.size()); EXPECT_EQ(result.min, 0); EXPECT_EQ(result.max, 66); @@ -67,9 +66,8 @@ TEST(MinMaxFinder, RandomInt32) { std::vector data(1000); std::uniform_int_distribution dist(INT32_MIN, INT32_MAX); - for (auto &x : data) { + for (auto &x : data) x = dist(rng); - } auto result = find_min_max(data.data(), data.size()); auto std_result = std::minmax_element(data.begin(), data.end()); @@ -116,18 +114,17 @@ TEST(MinMaxFinder, MinSingleElement) { EXPECT_EQ(find_min(data, 1), 42); } -// Max Finder Tests -TEST(MinMaxFinder, MaxSignedBasic) { +TEST(MaxFinder, MaxSignedBasic) { int32_t data[] = {1, -2, 3, -4, 5}; EXPECT_EQ(find_max(data, 5), 5); } -TEST(MinMaxFinder, MaxUnsignedBasic) { +TEST(MaxFinder, MaxUnsignedBasic) { uint32_t data[] = {1, 2, 3, 4, 5}; EXPECT_EQ(find_max(data, 5), 5); } -TEST(MinMaxFinder, MaxSignedExtremes) { +TEST(MaxFinder, MaxSignedExtremes) { int32_t data[] = { std::numeric_limits::max(), std::numeric_limits::min(), @@ -136,7 +133,7 @@ TEST(MinMaxFinder, MaxSignedExtremes) { EXPECT_EQ(find_max(data, 5), std::numeric_limits::max()); } -TEST(MinMaxFinder, MaxUnsignedExtremes) { +TEST(MaxFinder, MaxUnsignedExtremes) { uint32_t data[] = { std::numeric_limits::max(), 0, 1, @@ -145,17 +142,16 @@ TEST(MinMaxFinder, MaxUnsignedExtremes) { EXPECT_EQ(find_max(data, 4), std::numeric_limits::max()); } -TEST(MinMaxFinder, MaxEmptyArray) { +TEST(MaxFinder, MaxEmptyArray) { std::vector data; EXPECT_EQ(find_max(data.data(), 0), std::numeric_limits::min()); } -TEST(MinMaxFinder, MaxSingleElement) { +TEST(MaxFinder, MaxSingleElement) { int32_t data[] = {42}; EXPECT_EQ(find_max(data, 1), 42); } -// Different Integer Types Tests TEST(MinMaxFinder, Int8Types) { int8_t data[] = { std::numeric_limits::min(), @@ -236,9 +232,8 @@ TEST(MinMaxFinder, Stress) { std::vector data(size); std::uniform_int_distribution dist(INT32_MIN, INT32_MAX); - for (auto &x : data) { + for (auto &x : data) x = dist(rng); - } auto start = std::chrono::high_resolution_clock::now(); auto result = find_min_max(data.data(), data.size()); @@ -259,4 +254,204 @@ TEST(MinMaxFinder, Stress) { EXPECT_EQ(result.max, *std_result.second); } +class MinMaxStressTest : public ::testing::Test { +protected: + std::mt19937_64 rng{std::random_device{}()}; + + template + std::vector create_aligned_data(size_t n) { + std::vector data(n + 16); + size_t offset = (64 - (reinterpret_cast(data.data()) % 64)) / sizeof(T); + return std::vector(data.data() + offset, data.data() + offset + n); + } + + template + void run_benchmark(const std::vector& data, const std::string& test_name) { + constexpr int num_runs = 10; + + auto simd_min_time = std::chrono::microseconds(0); + auto simd_max_time = std::chrono::microseconds(0); + auto std_min_time = std::chrono::microseconds(0); + auto std_max_time = std::chrono::microseconds(0); + + T simd_min; + T simd_max; + T std_min; + T std_max; + + // Run multiple times to get average performance + for(int i = 0; i < num_runs; i++) { + { + auto start = std::chrono::high_resolution_clock::now(); + simd_min = find_min(data.data(), data.size()); + auto end = std::chrono::high_resolution_clock::now(); + simd_min_time += std::chrono::duration_cast(end - start); + } + + { + auto start = std::chrono::high_resolution_clock::now(); + simd_max = find_max(data.data(), data.size()); + auto end = std::chrono::high_resolution_clock::now(); + simd_max_time += std::chrono::duration_cast(end - start); + } + + { + auto start = std::chrono::high_resolution_clock::now(); + std_min = *std::min_element(data.begin(), data.end()); + auto end = std::chrono::high_resolution_clock::now(); + std_min_time += std::chrono::duration_cast(end - start); + } + + { + auto start = std::chrono::high_resolution_clock::now(); + std_max = *std::max_element(data.begin(), data.end()); + auto end = std::chrono::high_resolution_clock::now(); + std_max_time += std::chrono::duration_cast(end - start); + } + + EXPECT_EQ(simd_min, std_min); + EXPECT_EQ(simd_max, std_max); + } + + // Calculate average times + double simd_min_avg = simd_min_time.count() / static_cast(num_runs); + double simd_max_avg = simd_max_time.count() / static_cast(num_runs); + double std_min_avg = std_min_time.count() / static_cast(num_runs); + double std_max_avg = std_max_time.count() / static_cast(num_runs); + + std::cout << "\n" << test_name << " Results:\n" + << "SIMD min time: " << std::fixed << std::setprecision(2) + << simd_min_avg << " µs\n" + << "std::min time: " << std_min_avg << " µs\n" + << "SIMD min speedup: " << std_min_avg/simd_min_avg << "x\n" + << "SIMD max time: " << simd_max_avg << " µs\n" + << "std::max time: " << std_max_avg << " µs\n" + << "SIMD max speedup: " << std_max_avg/simd_max_avg << "x\n"; + } +}; + +TEST_F(MinMaxStressTest, LargeRandomInt32) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::uniform_int_distribution dist( + std::numeric_limits::min(), + std::numeric_limits::max() + ); + + for(auto& x : data) { + x = dist(rng); + } + + run_benchmark(data, "Large Random Int32"); +} + +TEST_F(MinMaxStressTest, LargeRandomUInt32) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::uniform_int_distribution dist( + std::numeric_limits::min(), + std::numeric_limits::max() + ); + + for(auto& x : data) { + x = dist(rng); + } + + run_benchmark(data, "Large Random UInt32"); +} + +TEST_F(MinMaxStressTest, SmallIntegers) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::uniform_int_distribution dist( + std::numeric_limits::min(), + std::numeric_limits::max() + ); + + for(auto& x : data) { + x = static_cast(dist(rng)); + } + + run_benchmark(data, "Small Integers (int8_t)"); +} + +TEST_F(MinMaxStressTest, LargeIntegers) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::uniform_int_distribution dist( + std::numeric_limits::min(), + std::numeric_limits::max() + ); + + for(auto& x : data) { + x = dist(rng); + } + + run_benchmark(data, "Large Integers (int64_t)"); +} + +TEST_F(MinMaxStressTest, MonotonicIncreasing) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::iota(data.begin(), data.end(), 0); + + run_benchmark(data, "Monotonic Increasing"); +} + +TEST_F(MinMaxStressTest, MonotonicDecreasing) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::iota(data.rbegin(), data.rend(), 0); + + run_benchmark(data, "Monotonic Decreasing"); +} + +TEST_F(MinMaxStressTest, AllSameValue) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::fill(data.begin(), data.end(), 42); + + run_benchmark(data, "All Same Value"); +} + +TEST_F(MinMaxStressTest, Alternating) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + for(size_t i = 0; i < data.size(); ++i) { + data[i] = (i % 2 == 0) ? 1000 : -1000; + } + + run_benchmark(data, "Alternating Values"); +} + +TEST_F(MinMaxStressTest, MinMaxAtEnds) { + constexpr size_t size = 10'000'000; + auto data = create_aligned_data(size); + std::uniform_int_distribution dist(-1000, 1000); + + for(auto& x : data) { + x = dist(rng); + } + + data.front() = std::numeric_limits::min(); + data.back() = std::numeric_limits::max(); + + run_benchmark(data, "Min/Max at Ends"); +} + +TEST_F(MinMaxStressTest, UnalignedSize) { + constexpr size_t size = 10'000'001; // Prime number size + auto data = create_aligned_data(size); + std::uniform_int_distribution dist( + std::numeric_limits::min(), + std::numeric_limits::max() + ); + + for(auto& x : data) { + x = dist(rng); + } + + run_benchmark(data, "Unaligned Size"); +} + } //namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/test/test_sum.cpp b/cpp/arcticdb/util/test/test_sum.cpp index 69a19da9f6..48fc270d8f 100644 --- a/cpp/arcticdb/util/test/test_sum.cpp +++ b/cpp/arcticdb/util/test/test_sum.cpp @@ -122,12 +122,10 @@ class SumStressTest : public ::testing::Test { auto naive_time = std::chrono::microseconds(0); auto kahan_time = std::chrono::microseconds(0); - // Result variables double simd_sum = 0.0; double naive_sum_result = 0.0; double kahan_sum_result = 0.0; - // Run multiple times to get average performance for(int i = 0; i < num_runs; i++) { { auto start = std::chrono::high_resolution_clock::now(); @@ -151,12 +149,10 @@ class SumStressTest : public ::testing::Test { } } - // Calculate average times double simd_avg = simd_time.count() / static_cast(num_runs); double naive_avg = naive_time.count() / static_cast(num_runs); double kahan_avg = kahan_time.count() / static_cast(num_runs); - // Calculate relative errors using Kahan as reference double simd_error = std::abs((simd_sum - kahan_sum_result) / kahan_sum_result); double naive_error = std::abs((naive_sum_result - kahan_sum_result) / kahan_sum_result); @@ -218,7 +214,6 @@ TEST_F(SumStressTest, KahanChallenging) { size_t n = 10'000'000; auto data = create_aligned_data(n); - // Create a sequence that's challenging for naive summation data[0] = 1.0; for(size_t i = 1; i < n; i++) { data[i] = 1.0e-16; @@ -257,8 +252,4 @@ TEST_F(SumStressTest, ExtremeValues) { run_benchmark(data, "Extreme Values"); } - - - - } // namespace arcticdb \ No newline at end of file diff --git a/cpp/arcticdb/util/vector_common.hpp b/cpp/arcticdb/util/vector_common.hpp new file mode 100644 index 0000000000..862d19fa8a --- /dev/null +++ b/cpp/arcticdb/util/vector_common.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +// Check compiler support for vector extensions +#if defined(__GNUC__) || defined(__clang__) +#define HAS_VECTOR_EXTENSIONS 1 +#else +#define HAS_VECTOR_EXTENSIONS 0 +#endif + +namespace arcticdb { + +template +using vector_type __attribute__((vector_size(64))) = T; + +template +struct is_supported_int : std::false_type {}; + +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; +template<> struct is_supported_int : std::true_type {}; + +template +struct is_supported_float : std::false_type {}; + +template<> struct is_supported_float : std::true_type {}; +template<> struct is_supported_float : std::true_type {}; + +} // namespace arcticdb \ No newline at end of file