8000 Two sorbet-runtime call-validation optimizations by djudd-stripe · Pull Request #4158 · sorbet/sorbet · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Two sorbet-runtime call-validation optimizations #4158

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

Merged
merged 8 commits into from
Apr 27, 2021

Conversation

djudd-stripe
Copy link
Contributor
@djudd-stripe djudd-stripe commented Apr 18, 2021

Two changes:

  1. Deprecate T::Profile, and rip out the sampled timing code from call validation.
  2. Add a new semi-fast path for call validation with up to four positional arguments and no keyword arguments or other special cases, very similar to the existing fast path except without the restriction that types must be T::Types::Simple. (Also method-vs-procedure is left for a runtime check, for now, to avoid blowing up code size and because one extra branch didn't seem like a big deal relative to the rest. edit: when merging the generator code, it was simpler to just leave this distinction in for both paths.)

These are really related only in intent; I actually implemented T::Profile support in the new fast-ish path before ripping it out. So I'm happy to split into separate PRs if folks prefer.

Motivation

  1. I don't believe this timer measures anything useful. It doesn't account for a significant proportion of runtime typechecking overhead, probably the majority of overhead, because (a) it doesn't capture the impact of adding the define_method stack frame itself, and (b) it doesn't capture any impact of T.let, T.must, T.unsafe etc. A tool like StackProf will give a much better picture, and (if configured well) with much lower measurement overhead. Microbenchmarks show >20% speedup on single-argument methods.

  2. I don't have hard statistics but my impression is that a large number of relatively-simple methods fail to qualify for the existing fast path because they use something like T::Boolean, T.nilable, or T.untyped, even as just one argument among otherwise simple ones. Microbenchmarks show a >40% speedup on single-nilable-argument methods.

bundle exec rake bench:typecheck results for 2.7, slightly abridged, on:

master:

T::Types::Simple#valid?: 41.252 ns
T::Types::Union#valid?: 139.641 ns
T.let(..., Integer): 428.153 ns
sig {params(x: Integer).void}: 337.683 ns
T.let(..., T.nilable(Integer)): 1.036 μs
sig {params(x: T.nilable(Integer)).void}: 862.766 ns
T.let(..., Example): 409.354 ns
sig {params(x: Example).void}: 307.338 ns
T.let(..., T.nilable(Example)): 1.043 μs
sig {params(x: T.nilable(Example)).void}: 875.392 ns
sig {params(s: Symbol, x: Integer, y: Integer).void} (with kwargs): 1.504 μs

just the new fast path:

T::Types::Simple#valid?: 42.637 ns
T::Types::Union#valid?: 142.595 ns
T.let(..., Integer): 389.78 ns
sig {params(x: Integer).void}: 330.284 ns
T.let(..., T.nilable(Integer)): 1.004 μs
sig {params(x: T.nilable(Integer)).void}: 492.045 ns
T.let(..., Example): 396.391 ns
sig {params(x: Example).void}: 313.137 ns
T.let(..., T.nilable(Example)): 1.01 μs
sig {params(x: T.nilable(Example)).void}: 500.454 ns
sig {params(s: Symbol, x: Integer, y: Integer).void} (with kwargs): 1.507 μs

with both changes:

T::Types::Simple#valid?: 40.547 ns
T::Types::Union#valid?: 161.091 ns
T.let(..., Integer): 405.242 ns
sig {params(x: Integer).void}: 215.672 ns
T.let(..., T.nilable(Integer)): 1.038 μs
sig {params(x: T.nilable(Integer)).void}: 377.882 ns
T.let(..., Example): 393.038 ns
sig {params(x: Example).void}: 207.139 ns
T.let(..., T.nilable(Example)): 1.078 μs
sig {params(x: T.nilable(Example)).void}: 394.03 ns
sig {params(s: Symbol, x: Integer, y: Integer).void} (with kwargs): 1.424 μs

(Only the changes in the sig results could be meaningful, but I've left the others in as points of comparison & indicators of the noise level.)

Test plan

Relying on existing tests

@djudd-stripe djudd-stripe requested a review from a team as a code owner April 18, 2021 17:39
@djudd-stripe djudd-stripe requested review from elliottt and removed request for a team April 18, 2021 17:39
@jez
Copy link
Collaborator
jez commented Apr 18, 2021

Do the api method tests use T::Profile right now in Stripe codebase? Is the plan to delete or change those tests?

@djudd-stripe
Copy link
Contributor Author

@jez they don't. If you're talking about the no-hot-runtime-checking tests, they use a custom mechanism so they can report more information about which methods are called. T::Profile is only used in a couple of production log-lines and I don't believe it provides real value there: https://livegrep.corp.stripe.com/search/stripe?q=T%3A%3AProfile%20repo%3Apay-server&fold_case=auto&regex=false&context=true

(It was maybe a bit more valuable before the introduction of the no-hot-runtime-checking tests, and when recursive checking was the default, so it was easier to introduce a regression and having a number right in the canonical log line could be valuable. I don't think this really applies anymore.)

@@ -1,30 +1,25 @@
# typed: true
# frozen_string_literal: true

# Deprecated, kept only for partial backwards compatibility
Copy link
Collaborator

Choose a reason for hiding this comment

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

When do you plan to remove this completely?

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 sure. Could potentially remove it completely now if you want; I'll have to do some advance work in pay-server anyway to keep tests passing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah i don't think we every documented or publicized this much, so i think it's safe to just remove the ~5 usages of it in Stripe's codebase pre-emptively and then land this PR.

@jez
Copy link
Collaborator
jez commented Apr 19, 2021

T::Profile is only used in a couple of production log-lines and I don't believe it provides real value there

Gotcha, so it seems like it's basically just populating some log lines, and since you've made a case that those log lines are not even really accurate, that it's not worth keeping these things around.

Comment on lines -124 to -125
fmt::print(" # This block is called for every `sig`. It's critical to keep it fast and\n"
" # reduce number of allocations that happen here.\n"
Copy link
Collaborator

Choose a reason for hiding this comment

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

ooc why have these comments been dropped?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not literally true - none of these individual methods is called for every sig anymore - and it didn't seem like it was adding enough value to be worth trying to reword.

Copy link
Collaborator

Choose a reason for hiding this comment

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

wfm

@elliottt elliottt removed their request for review April 19, 2021 20:38
Comment on lines 281 to 290
" CallValidation.report_error(\n"
" method_sig,\n"
" message,\n"
" 'Return value',\n"
" nil,\n"
" return_type,\n"
" return_value,\n"
" caller_offset: -1\n"
" )\n"
" end\n"
Copy link
Collaborator

Choose a reason for hiding this comment

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

The other two (methods, procedures) take almost the same codepath but then branch on validatorKind in a few places.

It seems like the "medium" path is pretty similar to the fast path, differing only by

  • the names of the individual methods
  • the condition used to check the types

Do you think it's worth trying to share more code between the different kinds of validators?

Or at least factor out some of the shared 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.

The handling of return_type is different too, but that doesn't affect your point much.

I agree that more code could be shared; I started off with a separate method because I wasn't sure how much would end up being shared, but it ended up being most of it.

I think merging might make things less readable - I already find the switches on ValidatorKind a bit hard to read -but it would make it less likely that some difference could be introduced accidentally, so maybe it's worth it. WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

i tend to think that in terms of "hard to read," if you're trying to change this code you're going to be looking at the generated code, imagining how you'd like the generated code to change, and back-solving for a diff to the the generator (i.e., the C++ code is not read in a vacuum, it is read in tandem with the generated code).

Given that, I place more value on the second part (introducing an accidental divergence).

So yeah, if you don't feel strongly one way or another i'd love to share more of the repetitive code between these two.

but it ended up being most of it

for what it's worth, this is basically the same process and realization that I had when I wrote it initially (it started out as two, and then I realized that so little was different)

Copy link
Collaborator
@jez jez left a comment

Choose a reason for hiding this comment

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

Change looks mostly good to me. I have one question about rollout / deprecation strategy, and one question about how we can share a little more code.

Happy to approve after having a chat about those things.

@djudd-stripe djudd-stripe force-pushed the djudd/semi-fast-path branch from 839173f to ede071e Compare April 19, 2021 23:42
@djudd-stripe
Copy link
Contributor Author

@jez updated to remove T::Profile fully and to share more call-validation-generator code. The diff does look much cleaner with the latter change. The Stripe codebase is also now prepped for this (PR just merged).

@djudd-stripe djudd-stripe merged commit 45d99dc into master Apr 27, 2021
@djudd-stripe djudd-stripe deleted the djudd/semi-fast-path branch April 27, 2021 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0