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

optimise try-except for the happy path #226

Closed
iritkatriel opened this issue Jan 17, 2022 · 15 comments
Closed

optimise try-except for the happy path #226

iritkatriel opened this issue Jan 17, 2022 · 15 comments
Assignees

Comments

@iritkatriel
Copy link
Collaborator

The code is currently generated as something like this:

try-body
except-blocks
else-body
finally

if instead we do something like:

try-body
else-body
finally
except-blocks
finally (for except)

then there are fewer jumps in the happy path.

As a bonus, everything other than the except-blocks can be shared between try and try-star.

@CCLDArjun
Copy link

wouldn't the jump (with the current code) from try-body to else-body just be moved to a jump from finally to after except-blocks?

So happy path would go from

try-body
jump to else-body
except-blocks
else-body
finally

to

try-body
else-body
finally
jump to ...
except-blocks
...

amount of jumps in happy path would be the same

@iritkatriel
Copy link
Collaborator Author

Once you bring in effects like inline the finally block (#221) there are more jumps. The new layout can guarantee that the happy path has only one jump.

@iritkatriel
Copy link
Collaborator Author

Also, emitting the happy path code consecutively could open up the possibility of emitting the other paths’ code somewhere else completely (end of the function?) and ending up with no jumps at all for the happy path.

@iritkatriel
Copy link
Collaborator Author

I tried moving the else block before the except block and everything seems to work except a couple of trace tests fail (one more line is traced). I'm investigating (i.e., trying to understand the trace).

@iritkatriel
Copy link
Collaborator Author

iritkatriel commented Jan 21, 2022

I made a PR for the else-block change. I added a couple of tests and I think it's tracing correctly now.

This PR keeps us with the same number of jumps - there is a jump from the end of the else block to the block that begins right after the except handlers (the finally block if there is one). I will take care of this jump later, but note that already we gain from this in terms of code size because now there is only one exit in the happy path so the finally block doesn't get copied in order to be inlined after each exit from the else block as in (#221). And I believe this is a net gain because we never have more jumps/blocks than we did before.

@iritkatriel
Copy link
Collaborator Author

iritkatriel commented Jan 27, 2022

The else-block change was merged, and while looking into the finally block I noticed this:

>>> try:
...   raise TypeError(1)
... except:
...   raise ValueError(2)
... finally:
...   raise OSError(3)
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: 1

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
ValueError: 2

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
OSError: 3

The exception from the finally block has the exception from the except block as its context, which seems right, but the literal reading of the traceback is that the finally block is an exception handler of the except block, which is a little weird.

@iritkatriel
Copy link
Collaborator Author

(To be clear, this is how it was before the last PR was merged, it's not that the else block change broke something).

@gvanrossum
Copy link
Collaborator

Well, in a sense finally is a sort of exception handler (-ish).

See Brett's blog post:
https://snarky.ca/unravelling-finally-and-else-from-try/

@iritkatriel
Copy link
Collaborator Author

The next steps here are

  1. Emit finally immediately after else.
  2. Layout the except handler at the end of the function.

If we do 1 without 2 then we will reverse the gains of the else block move (we still need to jump over except, but now we inline finally, possibly copying it). So 2 should come first.

Currently 2 would mean we create two lists of blocks in the compiler, hot and cold, and then at the end of code gen we set b_next of the hot list to the head of the cold list.

I’m on the fence whether it’s worth doing this before the refactor .

@iritkatriel
Copy link
Collaborator Author

Now that the jumps are relative, we can reorder basic blocks. In python/cpython#91804 I marked basic blocks from inside "except" as "cold", and then added a reordering stage that pushes these blocks to the end of the function, followed by a repeat of the "remove jumps that just go to the next block" step.

The result can be seen here: python/cpython#91804 (comment)

@gvanrossum
Copy link
Collaborator

Cool!

@iritkatriel
Copy link
Collaborator Author

Micro benchmarks show that a try-except is about 8 or 9% faster in the no-exception case (I removed a try-except from the main loop of timeit to get an accurate reading, otherwise just timeit of "x = 1" showed a difference):

New:
% ./python.exe -m timeit "try:" " pass" "except OSError:" " pass" "x = 1"
20000000 loops, best of 5: 9.94 nsec per loop

Old:
% ./python.exe -m timeit "try:" " pass" "except OSError:" " pass" "x = 1"
20000000 loops, best of 5: 10.3 nsec per loop

@iritkatriel
Copy link
Collaborator Author

pyperformance didn't show a difference other than noise.

@iritkatriel
Copy link
Collaborator Author

PR at python/cpython#92769.

@ericsnowcurrently
Copy link
Collaborator

🎉

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

No branches or pull requests

4 participants