Skip to content

Commit b6cc238

Browse files
felipecrvliujiacheng777
authored andcommitted
apacheGH-34893: [C++] Fix run-end encoded array iterator issues that manifest on backwards iteration (apache#34896)
### Rationale for this change The value of `RunEndEncodedArraySpan::Iterator::physical_pos_` from the instance returned by `RunEndEncodedArraySpan::end()` is smaller (by 1) than the results of successive applications of `operator++()` until `logical_pos_` reaches the logical length of the run-end encoded array. To support backwards iteration, the `Iterator` returned by `RunEndEncodedArraySpan::end()` should be well-formed. There was no `operator--()` in `RunEndEncodedArraySpan::Iterator` before this PR, so one can't go backwards from `end()` and this latent bug doesn't manifest. ### What changes are included in this PR? - Addition of `RunEndEncodedArraySpan::Iterator::operator--()` - Addition of `MergedRunsIterator::operator--()` - Rename `isEnd()` to `is_end()` (it was a code-style violation) - More test cases ### Are these changes tested? By existing and new unit tests. ### Are there any user-facing changes? A public function -- `isEnd` -- was renamed, but this code hasn't been released yet, so it's OK. * Closes: apache#34893 Authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com> Signed-off-by: Matt Topol <zotthewizard@gmail.com>
1 parent a869f96 commit b6cc238

5 files changed

Lines changed: 270 additions & 39 deletions

File tree

cpp/src/arrow/array/builder_run_end.cc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,7 @@ Status RunEndEncodedBuilder::DoAppendArray(const ArraySpan& to_append) {
232232
RETURN_NOT_OK(ReservePhysical(physical_length));
233233

234234
// Append all the run ends from to_append
235-
const auto end = ree_span.end();
236-
for (auto it = ree_span.iterator(0, physical_offset); it != end; ++it) {
235+
for (auto it = ree_span.iterator(0, physical_offset); !it.is_end(ree_span); ++it) {
237236
const int64_t run_end = committed_logical_length_ + it.run_length();
238237
RETURN_NOT_OK(DoAppendRunEnd<RunEndCType>(run_end));
239238
UpdateDimensions(run_end, 0);

cpp/src/arrow/compare.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ class RangeDataEqualsImpl {
486486
const auto& right_values = *right_.child_data[1];
487487

488488
auto it = ree_util::MergedRunsIterator(left, right);
489-
for (; !it.isEnd(); ++it) {
489+
for (; !it.is_end(); ++it) {
490490
RangeDataEqualsImpl impl(options_, floating_approximate_, left_values, right_values,
491491
it.index_into_left_array(), it.index_into_right_array(),
492492
/*range_length=*/1);

cpp/src/arrow/compute/kernels/vector_run_end_encode.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ class RunEndDecodingLoop {
698698
const ree_util::RunEndEncodedArraySpan<RunEndCType> ree_array_span(input_array_);
699699
int64_t write_offset = 0;
700700
int64_t output_valid_count = 0;
701-
for (auto it = ree_array_span.begin(); it != ree_array_span.end(); ++it) {
701+
for (auto it = ree_array_span.begin(); !it.is_end(ree_array_span); ++it) {
702702
const int64_t read_offset = values_offset_ + it.index_into_array();
703703
const int64_t run_length = it.run_length();
704704
ValueRepr value;

cpp/src/arrow/util/ree_util.h

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,13 @@ class RunEndEncodedArraySpan {
157157
/// The values array can be addressed with this index to get the value
158158
/// that makes up the run.
159159
///
160-
/// NOTE: if this Iterator was produced by RunEndEncodedArraySpan::end(),
160+
/// NOTE: if this Iterator is equal to RunEndEncodedArraySpan::end(),
161161
/// the value returned is undefined.
162162
int64_t index_into_array() const { return physical_pos_; }
163163

164164
/// \brief Return the initial logical position of the run
165165
///
166-
/// If this Iterator was produced by RunEndEncodedArraySpan::end(), this is
166+
/// If this Iterator is equal to RunEndEncodedArraySpan::end(), this is
167167
/// the same as RunEndEncodedArraySpan::length().
168168
int64_t logical_position() const { return logical_pos_; }
169169

@@ -177,6 +177,16 @@ class RunEndEncodedArraySpan {
177177
/// Pre-condition: *this != RunEndEncodedArraySpan::end()
178178
int64_t run_length() const { return run_end() - logical_pos_; }
179179

180+
/// \brief Check if the iterator is at the end of the array.
181+
///
182+
/// This can be used to avoid paying the cost of a call to
183+
/// RunEndEncodedArraySpan::end().
184+
///
185+
/// \return true if the iterator is at the end of the array
186+
bool is_end(const RunEndEncodedArraySpan& span) const {
187+
return logical_pos_ >= span.length();
188+
}
189+
180190
Iterator& operator++() {
181191
logical_pos_ = span.run_end(physical_pos_);
182192
physical_pos_ += 1;
@@ -189,6 +199,18 @@ class RunEndEncodedArraySpan {
189199
return prev;
190200
}
191201

202+
Iterator& operator--() {
203+
physical_pos_ -= 1;
204+
logical_pos_ = (physical_pos_ > 0) ? span.run_end(physical_pos_ - 1) : 0;
205+
return *this;
206+
}
207+
208+
Iterator operator--(int) {
209+
const Iterator prev = *this;
210+
--(*this);
211+
return prev;
212+
}
213+
192214
bool operator==(const Iterator& other) const {
193215
return logical_pos_ == other.logical_pos_;
194216
}
@@ -234,31 +256,52 @@ class RunEndEncodedArraySpan {
234256
///
235257
/// \param logical_pos is an index in the [0, length()] range
236258
Iterator iterator(int64_t logical_pos) const {
237-
return iterator(logical_pos, logical_pos < length()
238-
? PhysicalIndex(logical_pos)
239-
: RunEndsArray(array_span).length);
259+
if (logical_pos < length()) {
260+
return iterator(logical_pos, PhysicalIndex(logical_pos));
261+
}
262+
// If logical_pos is above the valid range, use length() as the logical
263+
// position and calculate the physical address right after the last valid
264+
// physical position. Which is the physical index of the last logical
265+
// position, plus 1.
266+
return (length() == 0) ? iterator(0, PhysicalIndex(0))
267+
: iterator(length(), PhysicalIndex(length() - 1) + 1);
240268
}
241269

242270
/// \brief Create an iterator representing the logical begin of the run-end
243271
/// encoded array
244-
Iterator begin() const { return iterator(0); }
272+
Iterator begin() const { return iterator(0, PhysicalIndex(0)); }
245273

246274
/// \brief Create an iterator representing the first invalid logical position
247275
/// of the run-end encoded array
248276
///
249-
/// The Iterator returned by end() should not be
277+
/// \warning Avoid calling end() in a loop, as it will recompute the physical
278+
/// length of the array on each call (O(log N) cost per call).
279+
///
280+
/// \par You can write your loops like this instead:
281+
/// \code
282+
/// for (auto it = array.begin(), end = array.end(); it != end; ++it) {
283+
/// // ...
284+
/// }
285+
/// \endcode
286+
///
287+
/// \par Or this version that does not look like idiomatic C++, but removes
288+
/// the need for calling end() completely:
289+
/// \code
290+
/// for (auto it = array.begin(); !it.is_end(array); ++it) {
291+
/// // ...
292+
/// }
293+
/// \endcode
250294
Iterator end() const {
251-
// NOTE: the run ends array length is not necessarily what
252-
// PhysicalIndex(length()) would return but it is a cheap to obtain
253-
// physical offset that is invalid.
254-
return iterator(length(), RunEndsArray(array_span).length);
295+
return iterator(length(),
296+
(length() == 0) ? PhysicalIndex(0) : PhysicalIndex(length() - 1) + 1);
255297
}
256298

257299
// Pre-condition: physical_pos < RunEndsArray(array_span).length);
258300
inline int64_t run_end(int64_t physical_pos) const {
259301
assert(physical_pos < RunEndsArray(array_span).length);
260-
// Logical index of the end of the currently active run
261-
const int64_t logical_run_end = run_ends_[physical_pos] - offset();
302+
// Logical index of the end of the run at physical_pos with offset applied
303+
const int64_t logical_run_end =
304+
std::max<int64_t>(static_cast<int64_t>(run_ends_[physical_pos]) - offset(), 0);
262305
// The current run may go further than the logical length, cap it
263306
return std::min(logical_run_end, length());
264307
}
@@ -281,12 +324,37 @@ class MergedRunsIterator {
281324
using LeftIterator = typename Left::Iterator;
282325
using RightIterator = typename Right::Iterator;
283326

327+
MergedRunsIterator(LeftIterator left_it, RightIterator right_it,
328+
int64_t common_logical_length, int64_t common_logical_pos)
329+
: ree_iterators_{std::move(left_it), std::move(right_it)},
330+
logical_length_(common_logical_length),
331+
logical_pos_(common_logical_pos) {}
332+
284333
public:
334+
/// \brief Construct a MergedRunsIterator positioned at logical position 0.
335+
///
336+
/// Pre-condition: left.length() == right.length()
285337
MergedRunsIterator(const Left& left, const Right& right)
286-
: ree_iterators_{left.begin(), right.begin()}, logical_length_(left.length()) {
338+
: MergedRunsIterator(left.begin(), right.begin(), left.length(), 0) {
287339
assert(left.length() == right.length());
288340
}
289341

342+
static Result<MergedRunsIterator> MakeBegin(const Left& left, const Right& right) {
343+
if (left.length() != right.length()) {
344+
return Status::Invalid(
345+
"MergedRunsIterator expects RunEndEncodedArraySpans of the same length");
346+
}
347+
return MergedRunsIterator(left, right);
348+
}
349+
350+
static Result<MergedRunsIterator> MakeEnd(const Left& left, const Right& right) {
351+
if (left.length() != right.length()) {
352+
return Status::Invalid(
353+
"MergedRunsIterator expects RunEndEncodedArraySpans of the same length");
354+
}
355+
return MergedRunsIterator(left.end(), right.end(), left.length(), left.length());
356+
}
357+
290358
/// \brief Return the left RunEndEncodedArraySpan child
291359
const Left& left() const { return std::get<0>(ree_iterators_).span; }
292360

@@ -295,15 +363,18 @@ class MergedRunsIterator {
295363

296364
/// \brief Return the initial logical position of the run
297365
///
298-
/// If isEnd(), this is the same as length().
366+
/// If is_end(), this is the same as length().
299367
int64_t logical_position() const { return logical_pos_; }
300368

369+
/// \brief Whether the iterator is at logical position 0.
370+
bool is_begin() const { return logical_pos_ == 0; }
371+
301372
/// \brief Whether the iterator has reached the end of both arrays
302-
bool isEnd() const { return logical_pos_ == logical_length_; }
373+
bool is_end() const { return logical_pos_ == logical_length_; }
303374

304375
/// \brief Return the logical position immediately after the run.
305376
///
306-
/// Pre-condition: !isEnd()
377+
/// Pre-condition: !is_end()
307378
int64_t run_end() const {
308379
const auto& left_it = std::get<0>(ree_iterators_);
309380
const auto& right_it = std::get<1>(ree_iterators_);
@@ -312,7 +383,7 @@ class MergedRunsIterator {
312383

313384
/// \brief returns the logical length of the current run
314385
///
315-
/// Pre-condition: !isEnd()
386+
/// Pre-condition: !is_end()
316387
int64_t run_length() const { return run_end() - logical_pos_; }
317388

318389
/// \brief Return a physical index into the values array of a given input,
@@ -352,6 +423,34 @@ class MergedRunsIterator {
352423
return prev;
353424
}
354425

426+
MergedRunsIterator& operator--() {
427+
auto& left_it = std::get<0>(ree_iterators_);
428+
auto& right_it = std::get<1>(ree_iterators_);
429+
430+
// The logical position of each iterator is the run_end() of the previous run.
431+
const int64_t left_logical_pos = left_it.logical_position();
432+
const int64_t right_logical_pos = right_it.logical_position();
433+
434+
if (left_logical_pos < right_logical_pos) {
435+
--right_it;
436+
logical_pos_ = std::max(left_logical_pos, right_it.logical_position());
437+
} else if (left_logical_pos > right_logical_pos) {
438+
--left_it;
439+
logical_pos_ = std::max(left_it.logical_position(), right_logical_pos);
440+
} else {
441+
--left_it;
442+
--right_it;
443+
logical_pos_ = std::max(left_it.logical_position(), right_it.logical_position());
444+
}
445+
return *this;
446+
}
447+
448+
MergedRunsIterator operator--(int) {
449+
MergedRunsIterator prev = *this;
450+
--(*this);
451+
return prev;
452+
}
453+
355454
bool operator==(const MergedRunsIterator& other) const {
356455
return logical_pos_ == other.logical_position();
357456
}
@@ -361,7 +460,7 @@ class MergedRunsIterator {
361460
private:
362461
std::tuple<LeftIterator, RightIterator> ree_iterators_;
363462
const int64_t logical_length_;
364-
int64_t logical_pos_ = 0;
463+
int64_t logical_pos_;
365464
};
366465

367466
} // namespace ree_util

0 commit comments

Comments
 (0)