Skip to content

Decide on STORE_FAST_LOAD_FAST #160921

Open
Open

Description

@eendebakpt

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagetype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      ⌂ Proxy