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-44491: [C++] StatusConstant- cheaply copied const Status #44493

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

bkietz
Copy link
Member

@bkietz bkietz commented Oct 21, 2024

Rationale for this change

It'd be convenient to construct placeholder error Status cheaply.

What changes are included in this PR?

A constant error Status can now be constructed wrapped in a function like

Status UninitializedResult() {
  static StatusConstant uninitialized_result{StatusCode::UnknownError,
                                             "Uninitialized Result<T>"};
  return uninitialized_result;
}

Copying the constant status is relatively cheap (no heap interaction), so it's suitable for use as a placeholder error status.

Added bool Status::State::is_constant which causes copies to be shallow and skips destruction. Also Status::state_ is a const pointer now; this helps to ensure that it is obvious when we mutate state_ (as in AddContextLine).

Are these changes tested?

Minimal unit test added. The main consideration is probably benchmarks to make sure hot paths don't get much slower.

Are there any user-facing changes?

This API is not currently public; no user-facing changes.

Copy link

⚠️ GitHub issue #44491 has been automatically assigned in GitHub to PR creator.

Comment on lines 377 to 378
if (ARROW_PREDICT_TRUE(state_ == NULL)) return;
if (ARROW_PREDICT_FALSE(state_->is_static)) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably better to have a single condition here to make it obvious that the rest of the function is the cold code:

if (ARROW_PREDICT_TRUE(state_ == NULL || state_->is_static)) return;

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree that this is more obvious for human readers, but I think the priority here is to make it obvious to the compiler that the hottest condition is state_ == NULL, independent of any other consideration

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm saying it would be better to the compiler as well not only humans. What the previous code achieved with the ARROW_PREDICT_FALSE was telling the compiler that DeleteState() is code and shouldn't be inlined. Since lots of Status are destroyed all over the place, that reduces binary size and avoids pollution of the I-cache with cold code.

    if (ARROW_PREDICT_FALSE(state_ != NULL)) {
      DeleteState();
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I see what you mean, how about:

Suggested change
if (ARROW_PREDICT_TRUE(state_ == NULL)) return;
if (ARROW_PREDICT_FALSE(state_->is_static)) return;
if (ARROW_PREDICT_FALSE(state_ != NULL)) {
if (ARROW_PREDICT_FALSE(state_->is_static)) return;
delete state_;

Then it should still be clear that State::~State() is cold. I could also explicitly move the definition of State::~State() into status.cc, which should prevent inlining

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the previous structure is the best: call DeleteState after a single unlikely condition. And the is_static check lives inside DeleteState.

cpp/src/arrow/status.h Outdated Show resolved Hide resolved
@github-actions github-actions bot added awaiting changes Awaiting changes and removed awaiting committer review Awaiting committer review labels Oct 21, 2024
@bkietz bkietz marked this pull request as ready for review October 25, 2024 15:12
@github-actions github-actions bot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Oct 25, 2024
@bkietz bkietz changed the title GH-44491: [C++] Static Status draft GH-44491: [C++] StatusConstant- cheaply copied const Status Oct 25, 2024
ARROW_CHECK_NE(code, StatusCode::OK) << "Cannot construct ok status constant";
}

operator Status() const { // NOLINT(runtime/explicit)
Copy link
Member Author

Choose a reason for hiding this comment

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

We could alternatively make StatusConstant a subclass of Status

@github-actions github-actions bot added awaiting changes Awaiting changes and removed awaiting change review Awaiting change review labels Oct 25, 2024
@github-actions github-actions bot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Oct 25, 2024
@bkietz
Copy link
Member Author

bkietz commented Oct 25, 2024

@pitrou @felipecrv which benchmark would be convincing as a demonstration this doesn't slow us down? I don't think we have an explicit status or result benchmark

For now (since it seems to me to have the deepest Status bubbling stack),

@ursabot please benchmark command=cpp-micro --suite-filter=grouper

@github-actions github-actions bot added awaiting changes Awaiting changes and removed awaiting change review Awaiting change review labels Oct 25, 2024
@pitrou
Copy link
Member

pitrou commented Oct 25, 2024

I don't think we have an explicit status or result benchmark

We do, but for some reason I had stuffed it in type_benchmark.cc

BENCHMARK(ErrorSchemeNoError);
BENCHMARK(ErrorSchemeBool);
BENCHMARK(ErrorSchemeStatus);
BENCHMARK(ErrorSchemeResult);
BENCHMARK(ErrorSchemeException);
BENCHMARK(ErrorSchemeNoErrorNoInline);
BENCHMARK(ErrorSchemeBoolNoInline);
BENCHMARK(ErrorSchemeStatusNoInline);
BENCHMARK(ErrorSchemeResultNoInline);
BENCHMARK(ErrorSchemeExceptionNoInline);

@github-actions github-actions bot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Oct 25, 2024
@bkietz
Copy link
Member Author

bkietz commented Oct 25, 2024

@ursabot please benchmark command=cpp-micro --suite-filter=type

@ursabot
Copy link

ursabot commented Oct 25, 2024

Benchmark runs are scheduled for commit f7d3fff. Watch https://buildkite.com/apache-arrow and https://conbench.ursa.dev for updates. A comment will be posted here when the runs are complete.

Copy link

Thanks for your patience. Conbench analyzed the 3 benchmarking runs that have been run so far on PR commit f7d3fff.

There were 4 benchmark results indicating a performance regression:

The full Conbench report has more details.

@github-actions github-actions bot added awaiting changes Awaiting changes and removed awaiting change review Awaiting change review labels Oct 30, 2024
@felipecrv
Copy link
Contributor

@pitrou @felipecrv which benchmark would be convincing as a demonstration this doesn't slow us down? I don't think we have an explicit status or result benchmark

Can you check the binary size impact with bloaty?

The performance impact is very tiny but everywhere a Status gets destroyed (all over the place). I think checking binary size is a good proxy to keeping the shape of the code the same.

}

// We would prefer that this destructor *not* be inlined, since the vast majority of
// Statuses are OK and so inlining would superflously increase binary size.
Status::State::~State() = default;
Copy link
Contributor

Choose a reason for hiding this comment

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

My guess is that it's better to have this inlined, but avoid inlining DeleteState (which is effectively what the previous implementation was achieving with

    if (ARROW_PREDICT_FALSE(state_ != NULL)) {
      DeleteState();
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll try writing out the conditions and keeping DeleteState non-inline

StatusConstant(StatusCode code, std::string msg,
std::shared_ptr<StatusDetail> detail = nullptr)
: state_{code, std::move(msg), std::move(detail), /*is_constant=*/true} {
ARROW_CHECK_NE(code, StatusCode::OK) << "Cannot construct ok status constant";
Copy link
Contributor

Choose a reason for hiding this comment

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

Well, we could, but it's counter to the reason of StatusConstant existing. Message should be StatusConstant is not meant to be used to construct an OK status"

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

Successfully merging this pull request may close these issues.

4 participants