(Note that dis currently has a bug in displaying accurate location info in the presence of CACHEs. The correct information can be observed by working with co_positions directly or using the code from that PR.)
While developing specialist, I realized that there are lots of common code patterns that produce bytecode with unexpectedly large source ranges. In addition to being unhelpful for both friendly tracebacks (the original motivation) and things like bytecode introspection, I suspect these huge ranges may also be bloating the size of our internal position tables as well.
Consider the following function:
def analyze(path): # 1
upper = lower = total = 0 # 2
with open(path) as file: # 3
for line in file: # 4
for character in line: # 5
if character.isupper(): # 6
upper += 1 # 7
elif character.islower(): # 8
lower += 1 # 9
total += 1 # 10
return lower / total, upper / total # 11
import dis
from pprint import pprint as pp
def pos(p):
return (p.lineno, p.end_lineno, p.col_offset, p.end_col_offset)
pp([(pos(x.positions), x.opname, x.argval) for x in dis.get_instructions(analyze)])
Things that should probably span one line at most:
- The first
GET_ITER/FOR_ITER pair span all of lines 4 through 10.
- The second
GET_ITER/FOR_ITER pair spans all of lines 5 through 10.
- The first
POP_JUMP_FORWARD_IF_FALSE spans all of lines 6 through 9.
- The second
POP_JUMP_FORWARD_IF_FALSE spans all of lines 8 through 9.
- Ten instructions for
with cleanup each span all of lines 3 through 10.
Things that should probably be artificial:
- A
JUMP_FORWARD spans all of line 7.
- The first
JUMP_BACKWARD spans all of line 10.
- The second
JUMP_BACKWARD spans all of lines 5 through 10.
Things I don't get:
- A
NOP spans all of lines 4 through 10.
As a result, over half of the generated bytecode for this function claims to span line 9, for instance. Also not shown here: the instructions for building functions and classes have similarly huge spans.
I think this can be tightened up in the compiler by:
- Being more aggressive in calling
SET_LOC on child nodes.
- Being more aggressive in calling
UNSET_LOC before unconditional jumps.
Linked PRs
(Note that
discurrently has a bug in displaying accurate location info in the presence ofCACHEs. The correct information can be observed by working withco_positionsdirectly or using the code from that PR.)While developing
specialist, I realized that there are lots of common code patterns that produce bytecode with unexpectedly large source ranges. In addition to being unhelpful for both friendly tracebacks (the original motivation) and things like bytecode introspection, I suspect these huge ranges may also be bloating the size of our internal position tables as well.Consider the following function:
Things that should probably span one line at most:
GET_ITER/FOR_ITERpair span all of lines 4 through 10.GET_ITER/FOR_ITERpair spans all of lines 5 through 10.POP_JUMP_FORWARD_IF_FALSEspans all of lines 6 through 9.POP_JUMP_FORWARD_IF_FALSEspans all of lines 8 through 9.withcleanup each span all of lines 3 through 10.Things that should probably be artificial:
JUMP_FORWARDspans all of line 7.JUMP_BACKWARDspans all of line 10.JUMP_BACKWARDspans all of lines 5 through 10.Things I don't get:
NOPspans all of lines 4 through 10.As a result, over half of the generated bytecode for this function claims to span line 9, for instance. Also not shown here: the instructions for building functions and classes have similarly huge spans.
I think this can be tightened up in the compiler by:
SET_LOCon child nodes.UNSET_LOCbefore unconditional jumps.Linked PRs