Repository navigation
Decide on STORE_FAST_LOAD_FAST #160921
Description
Feature or enhancement
After #160032 (comment) the usage of STORE_FAST_LOAD_FAST as almost none. It should either be removed or replaced with STORE_FAST_LOAD_FAST_BORROW.
Opcode counts for all code objects from compiling the 2071 files of Lib/ (nested code objects included, CACHE entries excluded):
| Opcode | main (2e1666f) | PR which adds STORE_FAST_LOAD_FAST_BORROW |
|---|---|---|
STORE_FAST_LOAD_FAST |
66 | 0 |
STORE_FAST_LOAD_FAST_BORROW |
0 | 1,997 |
STORE_FAST |
152,996 | 151,065 |
LOAD_FAST_BORROW |
539,756 | 538,149 |
LOAD_FAST (strong) |
20,858 | 20,924 |
LOAD_FAST_BORROW_LOAD_FAST_BORROW |
46,070 | 45,875 |
STORE_FAST_STORE_FAST |
12,069 | 12,069 |
| all instructions | 5,147,291 | 5,145,554 |
As long as superinstructions are limited to single lines, there are not many cases where we will have a STORE_FAST_LOAD_FAST. Some benchmarks:
| Benchmark (5,000 elements) | main | this PR |
|---|---|---|
[x for x in lst] |
32.0 us | 29.5 us: 1.08x faster |
[x + 1 for x in lst] |
73.1 us | 68.4 us: 1.07x faster |
[x.real for x in lst] |
97.7 us | 89.2 us: 1.10x faster |
for i in range(5000): i |
36.6 us | 34.1 us: 1.07x faster |
Callgrind on the first benchmark: 131.7 vs 122.8 instructions per iteration. From the benchmarks only the second and third make sense in real-world code. The first and the last do not. In the pystats of the pyperformance suite the pair is about 0.08% of all executed instructions (94% of them directly after a FOR_ITER).
Scripts used for the opcode counts and the benchmark
"""Compile every .py under LIBDIR with the running interpreter and count opcodes.
Usage: python -I count_opcodes.py LIBDIR"""
import collections, dis, json, os, sys
libdir = sys.argv[1]
CACHE = dis.opmap['CACHE']
counts = collections.Counter()
def walk(co):
for i in range(0, len(co.co_code), 2):
op = co.co_code[i]
if op != CACHE:
counts[dis.opname[op]] += 1
for c in co.co_consts:
if isinstance(c, type(co)):
walk(c)
for root, dirs, files in os.walk(libdir):
for f in sorted(files):
if f.endswith('.py'):
p = os.path.join(root, f)
try:
with open(p, 'rb') as fh:
co = compile(fh.read(), p, 'exec')
except (SyntaxError, ValueError):
continue
walk(co)
print(json.dumps({'total': sum(counts.values()), 'counts': counts}, sort_keys=True))import sys, timeit
lst = list(range(5000))
def ident(it): return [x for x in it]
def plus(it): return [x + 1 for x in it]
def attr(it): return [x.real for x in it]
def rng(n):
for i in range(n): i
cases = [("[x for x in lst]", lambda: ident(lst)), ("[x + 1 for x in lst]", lambda: plus(lst)),
("[x.real for x in lst]", lambda: attr(lst)), ("for i in range(5000): i", lambda: rng(5000))]
print(sys.version.split()[0])
for name, fn in cases:
best = min(timeit.repeat(fn, number=500, repeat=21)) / 500 * 1e6
print(f" {name:28s} {best:7.1f} us")@markshannon @methane I have no preference. If you have no preference either, i'll toss a coin.
Has this already been discussed elsewhere?
No response given
Links to previous discussion of this feature:
No response
Activity