-
Notifications
You must be signed in to change notification settings - Fork 4.2k
ARROW-82: Initial IPC support for ListArray #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
cc7f851
01c50be
3895d34
20f984b
1374485
45e41c0
5f87aef
61b0481
a2e1e52
39c57ed
aa0602c
8e464b5
53d37bc
8ab5315
e71810b
2e6c477
3b219a1
10e6651
be04b3e
8982723
5e15815
6e57728
7789205
0af558b
0c5162d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ | |
| namespace arrow { | ||
|
|
||
| class Buffer; | ||
| class Status; | ||
|
|
||
| // Immutable data array with some logical type and some length. Any memory is | ||
| // owned by the respective Buffer instance (or its parents). | ||
|
|
@@ -39,7 +40,7 @@ class Array { | |
| Array(const std::shared_ptr<DataType>& type, int32_t length, int32_t null_count = 0, | ||
| const std::shared_ptr<Buffer>& null_bitmap = nullptr); | ||
|
|
||
| virtual ~Array() {} | ||
| virtual ~Array() = default; | ||
|
|
||
| // Determine if a slot is null. For inner loops. Does *not* boundscheck | ||
| bool IsNull(int i) const { | ||
|
|
@@ -58,6 +59,9 @@ class Array { | |
|
|
||
| bool EqualsExact(const Array& arr) const; | ||
| virtual bool Equals(const std::shared_ptr<Array>& arr) const = 0; | ||
| // Determines if the array is internally consistent. Defaults to always | ||
| // returning Status::OK. This can be an expensive check. | ||
| virtual Status Validate() const; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pardon my dumb question, my understanding of Status just seeing its usage in the code I thought it's signaling either an operation succeeded or failed, but validate seems to imply it's either in a valid state or invalid state (like a bool). Why not just use bool? Or does Status can also encode error information?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Status can encode error information.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was some work last year in Impala in which they analyzed the generated x86 instructions to see how to minimize the CPU cycles associated with verifying Status::OK: apache/impala@1afe728 |
||
|
|
||
| protected: | ||
| std::shared_ptr<DataType> type_; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,25 @@ | |
|
|
||
| namespace arrow { | ||
|
|
||
| Status ArrayBuilder::AppendToBitmap(bool is_null) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a matter of consistency, should this be
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense, I did it this way because it was closer to how the code previously worked. But I will change it. |
||
| if (length_ == capacity_) { | ||
| // If the capacity was not already a multiple of 2, do so here | ||
| // TODO(emkornfield) doubling isn't great default allocation practice | ||
| // see https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md | ||
| // fo discussion | ||
| RETURN_NOT_OK(Resize(util::next_power2(capacity_ + 1))); | ||
| } | ||
| UnsafeAppendToBitmap(is_null); | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| Status ArrayBuilder::AppendToBitmap(const uint8_t* valid_bytes, int32_t length) { | ||
| Reserve(length); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops. I wonder how hard it would be write a clang-tidy rule to always ensure methods with a Status field are checked.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kudu wrote an LLVM static analyzer for this https://github.com/apache/incubator-kudu/tree/master/build-support/tools/kudu-lint |
||
|
|
||
| UnsafeAppendToBitmap(valid_bytes, length); | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| Status ArrayBuilder::Init(int32_t capacity) { | ||
| capacity_ = capacity; | ||
| int32_t to_alloc = util::ceil_byte(capacity) / 8; | ||
|
|
@@ -36,6 +55,7 @@ Status ArrayBuilder::Init(int32_t capacity) { | |
| } | ||
|
|
||
| Status ArrayBuilder::Resize(int32_t new_bits) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By the way I was thinking it would be nice to make Resize and Init virtual methods, I think it would reduce code repetition and potential bugs for classes the don't have any reason to override Reserve. Thoughts?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good to me, go ahead |
||
| if (!null_bitmap_) { return Init(new_bits); } | ||
| int32_t new_bytes = util::ceil_byte(new_bits) / 8; | ||
| int32_t old_bytes = null_bitmap_->size(); | ||
| RETURN_NOT_OK(null_bitmap_->Resize(new_bytes)); | ||
|
|
@@ -56,10 +76,46 @@ Status ArrayBuilder::Advance(int32_t elements) { | |
|
|
||
| Status ArrayBuilder::Reserve(int32_t elements) { | ||
| if (length_ + elements > capacity_) { | ||
| // TODO(emkornfield) power of 2 growth is potentially suboptimal | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aside: should we do 1.5x growth everywhere (this is the folly approach IIRC -- is there more research on this subject?)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes this is what folly uses. Seems like 1.5 edges out 2 on the very light survey done here: https://en.wikipedia.org/wiki/Dynamic_array#Growth_factor Ideally, we would benchmark once we have some real data in place. In the absence of that 1.5 seems like a good default. |
||
| int32_t new_capacity = util::next_power2(length_ + elements); | ||
| return Resize(new_capacity); | ||
| } | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| Status ArrayBuilder::SetNotNull(int32_t length) { | ||
| RETURN_NOT_OK(Reserve(length)); | ||
| UnsafeSetNotNull(length); | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| void ArrayBuilder::UnsafeAppendToBitmap(bool is_null) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same question as above
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, I think so. I will make the change. |
||
| if (is_null) { | ||
| ++null_count_; | ||
| } else { | ||
| util::set_bit(null_bitmap_data_, length_); | ||
| } | ||
| ++length_; | ||
| } | ||
|
|
||
| void ArrayBuilder::UnsafeAppendToBitmap(const uint8_t* valid_bytes, int32_t length) { | ||
| if (valid_bytes == nullptr) { | ||
| UnsafeSetNotNull(length); | ||
| return; | ||
| } | ||
| for (int32_t i = 0; i < length; ++i) { | ||
| // TODO(emkornfield) Optimize for large values of length? | ||
| AppendToBitmap(valid_bytes[i] == 0); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be UnsafeAppendToBitmap? This presumes that the user has called
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. correct. I will change. |
||
| } | ||
| } | ||
|
|
||
| void ArrayBuilder::UnsafeSetNotNull(int32_t length) { | ||
| const int32_t new_length = length + length_; | ||
| // TODO(emkornfield) Optimize for large values of length? | ||
| for (int32_t i = length_; i < new_length; ++i) { | ||
| util::set_bit(null_bitmap_data_, i); | ||
| } | ||
| length_ = new_length; | ||
| } | ||
|
|
||
| } // namespace arrow | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
|
|
||
| #include <cstdint> | ||
| #include <cstring> | ||
| #include <sstream> | ||
| #include <vector> | ||
|
|
||
| #include "arrow/array.h" | ||
|
|
@@ -31,6 +32,7 @@ | |
| #include "arrow/type.h" | ||
| #include "arrow/types/construct.h" | ||
| #include "arrow/types/primitive.h" | ||
| #include "arrow/types/list.h" | ||
| #include "arrow/util/buffer.h" | ||
| #include "arrow/util/logging.h" | ||
| #include "arrow/util/status.h" | ||
|
|
@@ -63,32 +65,56 @@ static bool IsPrimitive(const DataType* type) { | |
| } | ||
| } | ||
|
|
||
| static bool IsListType(const DataType* type) { | ||
| DCHECK(type != nullptr); | ||
| switch (type->type) { | ||
| // TODO(emkornfield) grouping like this are used in a few places in the | ||
| // code consider using pattern like: | ||
| // http://stackoverflow.com/questions/26784685/c-macro-for-calling-function-based-on-enum-type | ||
| // | ||
| // TODO(emkornfield) Fix type systems so these are all considered lists and | ||
| // the types behave the same way? | ||
| // case Type::BINARY: | ||
| // case Type::CHAR: | ||
| case Type::LIST: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See also @jacques-n comment on the mailing list. Having logical types for String (UTF8) and Binary makes sense, but internally these are represented in memory using the same memory layout as List. So how we implement them in C++ should be whatever is the most convenient and high performance thing, probably |
||
| // see todo on common types | ||
| // case Type::STRING: | ||
| // case Type::VARCHAR: | ||
| return true; | ||
| default: | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // ---------------------------------------------------------------------- | ||
| // Row batch write path | ||
|
|
||
| Status VisitArray(const Array* arr, std::vector<flatbuf::FieldNode>* field_nodes, | ||
| std::vector<std::shared_ptr<Buffer>>* buffers) { | ||
| if (IsPrimitive(arr->type().get())) { | ||
| const PrimitiveArray* prim_arr = static_cast<const PrimitiveArray*>(arr); | ||
|
|
||
| field_nodes->push_back( | ||
| flatbuf::FieldNode(prim_arr->length(), prim_arr->null_count())); | ||
| DCHECK(arr); | ||
| DCHECK(field_nodes); | ||
| // push back all common elements | ||
| field_nodes->push_back(flatbuf::FieldNode(arr->length(), arr->null_count())); | ||
| if (arr->null_count() > 0) { | ||
| buffers->push_back(arr->null_bitmap()); | ||
| } else { | ||
| // Push a dummy zero-length buffer, not to be copied | ||
| buffers->push_back(std::make_shared<Buffer>(nullptr, 0)); | ||
| } | ||
|
|
||
| if (prim_arr->null_count() > 0) { | ||
| buffers->push_back(prim_arr->null_bitmap()); | ||
| } else { | ||
| // Push a dummy zero-length buffer, not to be copied | ||
| buffers->push_back(std::make_shared<Buffer>(nullptr, 0)); | ||
| } | ||
| const auto arr_type = arr->type().get(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declare this type |
||
| if (IsPrimitive(arr_type)) { | ||
| const PrimitiveArray* prim_arr = static_cast<const PrimitiveArray*>(arr); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. auto is appropriate here, as below |
||
| buffers->push_back(prim_arr->data()); | ||
| } else if (arr->type_enum() == Type::LIST) { | ||
| // TODO(wesm) | ||
| return Status::NotImplemented("List type"); | ||
| } else if (IsListType(arr_type)) { | ||
| const ListArray* list_arr = static_cast<const ListArray*>(arr); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. auto is OK here |
||
| buffers->push_back(list_arr->offset_buffer()); | ||
| // TODO(emkornfield) limit recursion depth | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Impala limited to 32K depth IIRC, but at minimum we should track the depth and have a static limit. |
||
| RETURN_NOT_OK(VisitArray(list_arr->values().get(), field_nodes, buffers)); | ||
| } else if (arr->type_enum() == Type::STRUCT) { | ||
| // TODO(wesm) | ||
| return Status::NotImplemented("Struct type"); | ||
| } | ||
|
|
||
| return Status::OK(); | ||
| } | ||
|
|
||
|
|
@@ -214,7 +240,7 @@ class RowBatchReader::Impl { | |
| // Traverse the flattened record batch metadata and reassemble the | ||
| // corresponding array containers | ||
| Status NextArray(const Field* field, std::shared_ptr<Array>* out) { | ||
| const std::shared_ptr<DataType>& type = field->type; | ||
| const TypePtr& type = field->type; | ||
|
|
||
| // pop off a field | ||
| if (field_index_ >= num_flattened_fields_) { | ||
|
|
@@ -226,15 +252,17 @@ class RowBatchReader::Impl { | |
| // we can skip that buffer without reading from shared memory | ||
| FieldMetadata field_meta = metadata_->field(field_index_++); | ||
|
|
||
| // extract null_bitmap which is common to all arrays | ||
| std::shared_ptr<Buffer> null_bitmap; | ||
| if (field_meta.null_count == 0) { | ||
| null_bitmap = nullptr; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this redundant?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so. The way the spec is currently worded it says it is optional on whether null_count == 0 implies a null pointer, and that the presence of it should be communicated in the metadata. I need to check Message.fbs, but I'm not sure it currently allows for communicating whether the bitmap is included. Here we make the decision to never include the null bitmap because the data-structure is redundant. I will add a comment explaining this.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah sorry, I meant that the nullary ctor for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes good point. |
||
| ++buffer_index_; | ||
| } else { | ||
| RETURN_NOT_OK(GetBuffer(buffer_index_++, &null_bitmap)); | ||
| } | ||
|
|
||
| if (IsPrimitive(type.get())) { | ||
| std::shared_ptr<Buffer> null_bitmap; | ||
| std::shared_ptr<Buffer> data; | ||
| if (field_meta.null_count == 0) { | ||
| null_bitmap = nullptr; | ||
| ++buffer_index_; | ||
| } else { | ||
| RETURN_NOT_OK(GetBuffer(buffer_index_++, &null_bitmap)); | ||
| } | ||
| if (field_meta.length > 0) { | ||
| RETURN_NOT_OK(GetBuffer(buffer_index_++, &data)); | ||
| } else { | ||
|
|
@@ -243,6 +271,26 @@ class RowBatchReader::Impl { | |
| return MakePrimitiveArray( | ||
| type, field_meta.length, data, field_meta.null_count, null_bitmap, out); | ||
| } | ||
| if (IsListType(type.get())) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. else if?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will check what clang-tidy says. In other cases it liked not having else if when the original "if" block always returned. If we prefer to allow either I can look into turning off that particular check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either else if, or I also think having more consistency in spacing will be better, so extra space after closing braces.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The prior block is executed if |
||
| std::shared_ptr<Buffer> offsets; | ||
| if (field_meta.length > 0) { | ||
| RETURN_NOT_OK(GetBuffer(buffer_index_++, &offsets)); | ||
| } else { | ||
| offsets.reset(new Buffer(nullptr, 0)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting -- I suppose with length-0
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I wasn't sure, its not mentioned in the spec. I'll send another e-mail out to the ML about this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After further consideration I think we should always serialize the offsets. I don't think it is worth consuming code have to special case for 0 length lists. |
||
| } | ||
| const int num_children = type->num_children(); | ||
| if (num_children != 1) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could skip the local variable creation (to avoid an extra |
||
| std::stringstream ss; | ||
| ss << "Field: " << field->ToString() | ||
| << " has wrong number of children:" << num_children; | ||
| return Status::Invalid(ss.str()); | ||
| } | ||
| std::shared_ptr<Array> values_array; | ||
| // TODO(emkornfield): limit recursion depth? | ||
| RETURN_NOT_OK(NextArray(type->child(0).get(), &values_array)); | ||
| return MakeListArray(type, field_meta.length, offsets, values_array, | ||
| field_meta.null_count, null_bitmap, out); | ||
| } | ||
| return Status::NotImplemented("Non-primitive types not complete yet"); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aside: I tried to find definitive documentation on when to use
= defaultvs= 0in C++11, if you have any pointersThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is default == {}, and destructor = 0, should be used rarely. From: http://en.cppreference.com/w/cpp/language/destructor "A destructor may be declared pure virtual, for example in a base class which needs to be made abstract, but has no other suitable functions that could be declared pure virtual. Such destructor must have a definition, since all base class destructors are always called when the derived class is destroyed" Although the second sentence seems to contradict the first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed to default purely for style (I likely should have done this as a separate PR and discussed), for some reason it reads more elegantly to me.