Skip to content
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

GH-35141: [C++] Versions of IsNull/IsValid that don't branch on type #35149

Closed
wants to merge 2 commits into from

Conversation

felipecrv
Copy link
Contributor

@felipecrv felipecrv commented Apr 14, 2023

Rationale for this change

See #35141.

What changes are included in this PR?

  • Add missing ARROW_EXPORT
  • Make code style consistent in all IsNull/IsValid member functions
  • Add IsNullFast<ArrowType> and IsValidFast<ArrowType>

Are these changes tested?

Yes.

Are there any user-facing changes?

New member functions added to ArrayData / Array / ArraySpan.

@felipecrv
Copy link
Contributor Author

Please suggest a better name for these function if you can think of one.

@github-actions github-actions bot added the awaiting review Awaiting review label Apr 14, 2023

template <typename ArrowType>
inline bool IsValidFast(int64_t i) const {
if constexpr (ArrowType::type_id == Type::NA) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add a DCHECK to check that ArrowType is equal to the real type in the Array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was tempted, but it adds too high of an overhead for this function as it's used from tight loops and is expected to be fully inlined. Even considering that it would be affecting only debug builds.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we just need care the release performance? An array using outerside ArrowType without checking is so dangerous...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug perf can make CI run much slower. Imagine changing the code from IsNull to IsNullFast and getting a slower build in return. I'm counting on people defaulting to use IsNull (safe) instead of IsNullFast. The latter is for people working on kernels which usually do many unsafe things and have more carefully written code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mapleFU note that all the special branches perform runtime type checks, so this is not completely unchecked.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, though it's still a bit wired for me. I think we can hear about others idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more clear: the body of these member functions in the .cc file, where logging.h can be used, contains runtime checks.

    } else if constexpr (ArrowType::type_id == Type::SPARSE_UNION) {
      return !IsNullSparseUnion(i);
    } else if constexpr (ArrowType::type_id == Type::DENSE_UNION) {
      return !IsNullDenseUnion(i);
    } else if constexpr (ArrowType::type_id == Type::RUN_END_ENCODED) {
      return !IsNullRunEndEncoded(i);

@github-actions github-actions bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Apr 17, 2023
if (buffers[0] != NULLPTR) {
return bit_util::GetBit(buffers[0]->data(), i + offset);
}
const auto type = this->type->id();
if (type == Type::SPARSE_UNION) {
return !internal::IsNullSparseUnion(*this, i);
}
if (type == Type::DENSE_UNION) {
} else if (type == Type::DENSE_UNION) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this line of change seems to be complained by clang-tidy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not on my editor. Where did you see this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this check is not enabled on Arrow's .clang-tidy and I don't think putting an else after a return is always bad.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this specific function, the use of else is not creating unnecessary indentation levels.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I was overwhelmed by this kind of warning in the internal toolchain from my employer. :)

if (buffers[0] != NULLPTR) {
return bit_util::GetBit(buffers[0]->data(), i + offset);
}
return null_count.load() != length;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a dumb question: if the validity bitmap does not exist, shouldn't null_count be either 0 or -1 instead of other values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NA is a known type that has no validity bitmap and null_count set to length. So in theory, this is very possible.

@@ -69,16 +69,37 @@ class ARROW_EXPORT Array {
// a potential inner-branch removal.
if (type_id() == Type::SPARSE_UNION) {
return !internal::IsNullSparseUnion(*data_, i);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change it to if else? I think it's not necessary here
clang-tidy has a check about this style ( https://clang.llvm.org/extra/clang-tidy/checks/readability/else-after-return.html ), although we don't enable this check, I think just return without if else is ok

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More compact. In sync with the if constexpr used in the other cases. Arrow's .clang-tidy doesn't complain about this. No extra indentation levels are introduced because of this.


template <typename ArrowType>
inline bool IsValidFast(int64_t i) const {
if constexpr (ArrowType::type_id == Type::NA) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, though it's still a bit wired for me. I think we can hear about others idea.

@felipecrv felipecrv marked this pull request as ready for review May 3, 2023 17:02
@felipecrv
Copy link
Contributor Author

@westonpace what do you think about this change? cc @zeroshade @benibus

kernels that call IsNull/IsValid in loops are usually templated on the data type. They can benefit by using a version of these functions that can if constexpr on the type to dispatch the correct call.

@pitrou
Copy link
Member

pitrou commented May 9, 2023

I'm skeptical about this. Did you find any kernels that call IsNull or IsValid? We have more efficient loop constructs for scanning entire null bitmaps, such as BitRunReader and BitBlockCounter.

(this is also handled internally by VisitArraySpanInline)

@felipecrv
Copy link
Contributor Author

We have more efficient loop constructs for scanning entire null bitmaps, such as BitRunReader and BitBlockCounter.

I'm aware, but these don't exist/work for REE and Union arrays (and potentially new formats in the future) as described in
#34361.

I added a fix that required adding branches to IsNull and IsValid. See #34408. The ifs are inlined, so compiler might even be able to elide them, but these versions would guarantee that.

Did you find any kernels that call IsNull or IsValid?

Yes, but only as fallback for the types that don't have validity defined by a single bitmap. For instance, my fix of the "hash_count" kernel has a fallback to IsNull that is currently what makes union arrays give correct results https://github.com/apache/arrow/pull/35129/files#diff-c50313f1d80194615c5674cb64a1437c272a76da19027dabf8fb61ebffd7e2afR408

For REEs, I wrote a completely custom counting loop.

Since I already have a switch there, it would be nice to tell the IsNull function exactly what type of array it's dealing with.

@pitrou
Copy link
Member

pitrou commented May 10, 2023

I'm aware, but these don't exist/work for REE and Union arrays (and potentially new formats in the future)

REE and Union are two different cases. For REE, it is better to iterate on runs, not logical values (exactly what you did for the "hash_count" kernel).

The ifs are inlined, so compiler might even be able to elide them, but these versions would guarantee that.

I'm not sure we care about compile-time guarantees here, since it's just a performance concern.

@felipecrv
Copy link
Contributor Author

I'm not sure we care about compile-time guarantees here, since it's just a performance concern.

The compile-time guarantee being that the generated code has a certain shape affecting performance and binary size. But as I see it, my case is weak. I will rebase, remove the first commit, add some use-cases and close the PR that I might re-open later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
4 participants