Skip to content

gh-119476: Eliminate Unnecessary Function Version Checks in Tier Two Interpreter - #119491

Closed
saulshanabrook wants to merge 7 commits into
python:mainfrom
saulshanabrook:function-watchers
Closed

saulshanabrook wants to merge 7 commits into
python:mainfrom
saulshanabrook:function-watchers

Conversation

@saulshanabrook

@saulshanabrook saulshanabrook commented May 24, 2024 •

Copy link
Copy Markdown
Contributor

This PR removes duplicate _CHECK_FUNCTION uops in the tier two interpreter. It does this in a similar way to removing the _GUARD_TYPE_VERSION in #119365.

It stores the current function version we have checked against in the symbol and then when we want to check again if the function is the same version, we remove the check, iff the function version hasn't changed since then. We know if it has changed by adding a watcher that will trigger whenever a function version changes.

Note that _CHECK_FUNCTION is only produced by CALL_PY_GENERAL not byCALL_PY_EXACT_ARGS.

TODO:

  • Fix test that verifies it is removed
  • Add test to verify that on function version change it is not removed

@saulshanabrook

Copy link
Copy Markdown
Contributor Author

Hey @brandtbucher, I am trying to add a test case for this but am a bit confused as to why it's not working currently.

With this test, it doesn't eliminate the second _CHECK_FUNCTION:

def thing(f):
    x = 0
    for _ in range(100):
        x += f() + f()
    return x

# Specify an unused arg in the function so that
# CALL_PY_GENERAL is used over CALL_PY_EXACT_ARGS
res, ex = self._run_with_optimizer(thing, lambda _=1: 1)

What's confusing me is that the trace never seems to get to the second one to optimize. It seems like it quits before even getting there. If I like at the opts returned, I see it does include it twice:

_START_EXECUTOR
_TIER2_RESUME_CHECK
_ITER_CHECK_RANGE
_GUARD_NOT_EXHAUSTED_RANGE
_ITER_NEXT_RANGE
_STORE_FAST_2
_LOAD_FAST_1
_LOAD_FAST_0
_PUSH_NULL
_SET_IP
_CHECK_FUNCTION_VERSION
_PY_FRAME_GENERAL
_SAVE_RETURN_OFFSET
_PUSH_FRAME
_CHECK_VALIDITY
_RESUME_CHECK
_LOAD_FAST_0
_POP_FRAME
_LOAD_FAST_0
_PUSH_NULL
_SET_IP
_CHECK_PEP_523
_CHECK_FUNCTION_VERSION
_PY_FRAME_GENERAL
_SAVE_RETURN_OFFSET
_PUSH_FRAME
_CHECK_VALIDITY
_RESUME_CHECK
_LOAD_FAST_0
_POP_FRAME
_GUARD_BOTH_INT
_BINARY_OP_ADD_INT
_GUARD_BOTH_INT
_BINARY_OP_ADD_INT
_STORE_FAST_1
_JUMP_TO_TOP
_DEOPT
_EXIT_TRACE
_EXIT_TRACE
_ERROR_POP_N
_EXIT_TRACE
_ERROR_POP_N
_DEOPT
_DEOPT
_EXIT_TRACE
_ERROR_POP_N
_DEOPT
_EXIT_TRACE
_ERROR_POP_N
_EXIT_TRACE
_ERROR_POP_N

However, the optimize bytecode case for _CHECK_FUNCTION is only hit once. If we turn on tracing with env PYTHON_OPT_DEBUG=3 and copy out the piece just for run_with_optimizer we see we just run through the _CHECK_FUNCTION once:

 0 abs: _START_EXECUTOR (0, operand=0x1060bf0cc)  stack_level 1
   1 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x1060bf0cc)  stack_level 1
   2 abs: _TIER2_RESUME_CHECK (0, operand=0)  stack_level 1
   3 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x1060bf0a4)  stack_level 1
   4 abs: _ITER_CHECK_RANGE (20, operand=0)  stack_level 1
   5 abs: _GUARD_NOT_EXHAUSTED_RANGE (20, operand=0)  stack_level 1
   6 abs: _ITER_NEXT_RANGE (20, operand=0, error_target=0)  stack_level 2
   7 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x1060bf0a8)  stack_level 2
   8 abs: _STORE_FAST (2, operand=0)  stack_level 1
   9 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x1060bf0aa)  stack_level 1
  10 abs: _LOAD_FAST (1, operand=0)  stack_level 2
  11 abs: _LOAD_FAST (0, operand=0)  stack_level 3
  12 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x1060bf0ac)  stack_level 3
  13 abs: _PUSH_NULL (0, operand=0)  stack_level 4
  14 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x1060bf0ae)  stack_level 4
  15 abs: _CHECK_PEP_523 (0, operand=0)  stack_level 4
  16 abs: _CHECK_FUNCTION_VERSION (0, operand=0x13a7)  stack_level 4
  17 abs: _PY_FRAME_GENERAL (0, operand=0, error_target=0)  stack_level 3
   0 abs: _START_EXECUTOR (0, operand=0x10602e23a)  stack_level 1
   1 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e23a)  stack_level 1
   2 abs: _TIER2_RESUME_CHECK (0, operand=0)  stack_level 1
   3 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e1e6)  stack_level 1
   4 abs: _ITER_CHECK_RANGE (26, operand=0)  stack_level 1
   5 abs: _GUARD_NOT_EXHAUSTED_RANGE (26, operand=0)  stack_level 1
   6 abs: _ITER_NEXT_RANGE (26, operand=0, error_target=0)  stack_level 2
   7 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e1ea)  stack_level 2
   8 abs: _STORE_FAST (3, operand=0)  stack_level 1
   9 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e1ec)  stack_level 1
  10 abs: _NOP (0, operand=0)  stack_level 1
  11 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e1ee)  stack_level 1
  12 abs: _CHECK_FUNCTION (8, operand=0x1315)  stack_level 1
  13 abs: _LOAD_CONST_INLINE (8, operand=0x105b0cc50)  stack_level 2
  14 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e1f8)  stack_level 2
  15 abs: _CHECK_ATTR_MODULE (10, operand=0x4e)  stack_level 2
  16 abs: _LOAD_ATTR_MODULE (10, operand=0x12)  stack_level 2
  17 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e20c)  stack_level 2
  18 abs: _PUSH_NULL (0, operand=0)  stack_level 3
  19 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e20e)  stack_level 3
  20 abs: _LOAD_FAST (1, operand=0)  stack_level 4
  21 abs: _LOAD_FAST (3, operand=0)  stack_level 5
  22 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e210)  stack_level 5
  23 abs: _CALL_BUILTIN_FAST_WITH_KEYWORDS (2, operand=0, error_target=0)  stack_level 2
  24 abs: _CHECK_PERIODIC (2, operand=0)  stack_level 2
  25 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e218)  stack_level 2
  26 abs: _SWAP (2, operand=0)  stack_level 2
  27 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e21a)  stack_level 2
  28 abs: _POP_TOP (0, operand=0)  stack_level 1
  29 abs: _CHECK_VALIDITY_AND_SET_IP (0, operand=0x10602e21c)  stack_level 1
  30 abs: _EXIT_TRACE (0, operand=0)  stack_level 1

Is it stopping the trace after one function call? Even if I turn off the watcher invalidating the trace whenever the function version changes, it still behaves like this.

@brandtbucher

Copy link
Copy Markdown
Member

What happens if we use a global function instead of a local lambda? I’m not at my computer, but it looks to me like the optimizer is unable to continue projecting into the first call, and is stopping there…

@saulshanabrook

Copy link
Copy Markdown
Contributor Author

If I do something like this (or if I define the function inside the other function) I get similar behavior:

        def f(*args):
            return 1

        def thing(init):
            x = init
            for _ in range(100):
                x += f() + f()
            return x
        res, ex = self._run_with_optimizer(thing, 0)

@brandtbucher

Copy link
Copy Markdown
Member

I’ll take a look.

@brandtbucher

Copy link
Copy Markdown
Member

It looks like _PY_FRAME_GENERAL currently just bails out of the optimizer instead of setting up the new frame. I'm not sure why... maybe we just haven't gotten around to it yet? Maybe @markshannon knows.

@brandtbucher

Copy link
Copy Markdown
Member

I'm sure it's not "correct", but copying-and-pasting the body of _INIT_CALL_PY_EXACT_ARGS into the body of _CALL_PY_GENERAL in the optimizer (replacing what's there now) works fine and passes all tests, including the new one.

@saulshanabrook

Copy link
Copy Markdown
Contributor Author

I can try adding a test using the exact args version once #119510 is merged... Maybe that will work?

@brandtbucher

Copy link
Copy Markdown
Member

I can try adding a test using the exact args version once #119510 is merged... Maybe that will work?

Yup! That PR just landed a minute ago.

@saulshanabrook
saulshanabrook marked this pull request as ready for review May 30, 2024 00:36
@saulshanabrook
saulshanabrook marked this pull request as draft May 30, 2024 01:00
@saulshanabrook
saulshanabrook marked this pull request as ready for review June 10, 2024 16:19
@saulshanabrook

Copy link
Copy Markdown
Contributor Author

@brandtbucher Could you run the benchmarks on this? I updated the tests to use the new opcode and it seems to be working.

@Fidget-Spinner Fidget-Spinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We definitely want this change. It will reduce deopt points in tier 2 when the mythical partial evaluation pass is added in.

Sorry for dropping the ball on this!

@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open for 30 days with no activity.

@github-actions github-actions Bot added the stale Stale PR or inactive for long period of time. label Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review stale Stale PR or inactive for long period of time.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants