bpo-45367: Specialize BINARY_MULTIPLY by sweeneyde · Pull Request #28727 · python/cpython · GitHub
Skip to content

bpo-45367: Specialize BINARY_MULTIPLY - #28727

Merged
markshannon merged 10 commits into
python:mainfrom
sweeneyde:multiply
Oct 14, 2021
Merged

markshannon merged 10 commits into
python:mainfrom
sweeneyde:multiply

Conversation

@sweeneyde

@sweeneyde sweeneyde commented Oct 5, 2021

Copy link
Copy Markdown
Member

@sweeneyde sweeneyde added the performance Performance or resource usage label Oct 5, 2021
@sweeneyde sweeneyde closed this Oct 5, 2021
@sweeneyde sweeneyde reopened this Oct 5, 2021
@sweeneyde sweeneyde added the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Oct 5, 2021
@bedevere-bot

Copy link
Copy Markdown

@bedevere-bot bedevere-bot removed the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Oct 5, 2021
@sweeneyde

sweeneyde commented Oct 5, 2021

Copy link
Copy Markdown
Member Author

The failing Tests / Windows (x86) check already has an open ticket at bpo-41682.

@sweeneyde

Copy link
Copy Markdown
Member Author

On -m test test_long test_float test_math test_list test_dict I got

Specialization stats:
    binary_multiply.specialization_success : 87
    binary_multiply.specialization_failure : 1635
    binary_multiply.hit : 1332363
    binary_multiply.deferred : 103712
    binary_multiply.miss : 15
    binary_multiply.deopt : 3
    binary_multiply.unquickened : 482

and on bm_nbody.py I got

Specialization stats:
    binary_multiply.specialization_success : 38
    binary_multiply.specialization_failure : 8
    binary_multiply.hit : 10800672
    binary_multiply.deferred : 336
    binary_multiply.miss : 0
    binary_multiply.deopt : 0
    binary_multiply.unquickened : 237

Comment thread Include/internal/pycore_code.h Outdated
@markshannon

Copy link
Copy Markdown
Member

Looks promising.

Apologies for the merge conflicts from #28723.
Once you've fixed those and the above comment, I will benchmark it.

@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.

Thanks Dennis, this looks good and I think it's almost ready to merge :). Just needs some stable benchmarks on non-Windows platforms.

Comment thread Python/ceval.c Outdated
Comment thread Python/specialize.c
Comment thread Python/specialize.c Outdated
@markshannon markshannon self-assigned this Oct 5, 2021
Comment thread Python/specialize.c
goto success;
}
else {
SPECIALIZATION_FAIL(BINARY_MULTIPLY, SPEC_FAIL_OTHER);

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.

Maybe this should be refined further. In another PR, though.

@Fidget-Spinner Fidget-Spinner Oct 5, 2021

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.

I agree. I don't think it will touch pyperformance, but it seems that in our own test suite almost 10% aren't hits #28727 (comment).

This probably varies wildly, I'd imagine test_string has lots of str * num too.
Oh gosh I just realized I'd read the entire PR as BINARY_ADD not BINARY_MULTIPLY. Please ignore my delusional ramblings.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Most of the non-hits are deferred, not specialization failure, so I think that's because the nature of a lot of test code is to only be run once, without many tight loops.

@Fidget-Spinner Fidget-Spinner Oct 5, 2021

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.

Thanks for pointing that out Dennis. I can't believe I missed that :). That means nearly 100% actual specialization on hot code. Hooray!

Off-topic: I've recently wondered if pyperformance is somewhat out of touch (no offence intended to its contributors). Many of its benchmarks (nbody, nqueens, etc.) have been found by the JS folks to not be realistic. The Pyston benchmark suite seems way more comprehensive.

@markshannon markshannon Oct 5, 2021

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.

"deferred" means that the ADAPTIVE instruction is counting down until its next specialization attempt.
This only happens after a specialization failure.

Look at the numbers. deferred ≈ sum(specialization-failures) * ADAPTIVE_CACHE_BACKOFF

@markshannon

Copy link
Copy Markdown
Member

Significant but small speedup

It does show speedups for the benchmarks we would expect, but there are significant slowdowns for some other benchmarks; notably the pickle benchmarks.

@Fidget-Spinner

Copy link
Copy Markdown
Member

Significant but small speedup

I've personally disregarded anything from pyperformance in the range of +-1.05x. That said, I wholly believe nbody sped up, I just don't trust pickle slowing down. There's no BINARY_MULTIPLY there and it's a call to a C function (unless we snagged some PGO problem again :( ). I will try to bench soon.

@sweeneyde

Copy link
Copy Markdown
Member Author

I ran -m test test_dict test_math test_long test_heapq test_bisect test_list test_dict with some more detailed specialization failure stats.

    binary_multiply.specialization_success : 96
    binary_multiply.specialization_failure : 1650
    binary_multiply.hit : 1584206
    binary_multiply.deferred : 104756
    binary_multiply.miss : 15
    binary_multiply.deopt : 3
    binary_multiply.unquickened : 485
    binary_multiply.specialization_failure_kinds[12] : 56    # different types, other
    binary_multiply.specialization_failure_kinds[13] : 8     # str * int
    binary_multiply.specialization_failure_kinds[14] : 0     # int * str
    binary_multiply.specialization_failure_kinds[15] : 1459  # float * int
    binary_multiply.specialization_failure_kinds[16] : 135   # int * float

I'm guessing specializing int/float mixtures would be beneficial. I wonder if it's better to have a single slightly-more-complex opcode or add more opcodes. I was thinking something like

        TARGET(BINARY_MULTIPLY_FLOAT) {
            PyObject *left = SECOND();
            PyObject *right = TOP();
            double dleft, dright;
            if (PyFloat_CheckExact(left)) {
                dleft = ((PyFloatObject *)left)->ob_fval;
                if (PyFloat_CheckExact(right)) {
                    dright = ((PyFloatObject *)right)->ob_fval;
                }
                else if (PyLong_CheckExact(right) && IS_MEDIUM_VALUE(right)) {
                    dright = (double)medium_value(right);
                }
                else {
                    DEOPT_IF(1, BINARY_MULTIPLY);
                }
            }
            else if (PyLong_CheckExact(left) && IS_MEDIUM_VALUE(left)) {
                DEOPT_IF(!PyFloat_CheckExact(right), BINARY_MULTIPLY);
                dleft = (double)medium_value(left);
                dright = ((PyFloatObject *)right)->ob_fval;
            }
            else {
                DEOPT_IF(1, BINARY_MULTIPLY);
            }
            STAT_INC(BINARY_MULTIPLY, hit);
            record_hit_inline(next_instr, oparg);
            PyObject *prod = PyFloat_FromDouble(dleft * dright);
            SET_SECOND(prod);
            Py_DECREF(right);
            Py_DECREF(left);
            STACK_SHRINK(1);
            if (prod == NULL) {
                goto error;
            }
            DISPATCH();
        }

@sweeneyde

Copy link
Copy Markdown
Member Author

The most recent change gets the results of -m test test_dict test_math test_long test_heapq test_bisect test_list test_dict down to

    binary_multiply.specialization_success : 108
    binary_multiply.specialization_failure : 100
    binary_multiply.hit : 1679780
    binary_multiply.deferred : 5962
    binary_multiply.miss : 15
    binary_multiply.deopt : 3
    binary_multiply.unquickened : 485

@markshannon

Copy link
Copy Markdown
Member

@sweeneyde

Copy link
Copy Markdown
Member Author

My thinking was that two DEOPT_IFs is the same number of branches as two if-statements in the float-float case, and if we're going to deoptimize we might as well check if it's actually an int that we're multiplying by so we can quickly recover and avoid deoptimizing. It seems common enough for an int to slip into places where floats can be, or to have multiplications of the form pi * r * r where r is an integer. Or as you mentioned on the bpo issue, there might be an instruction that is int*float for the first iteration but becomes float*float on subsequent iterations.

To clarify, are you advising that the increased complexity of the opcode is not worth the increased percentage of instructions specialized? Is the solution to

  • Use something like commit c64540a with only strictly float*float and int*int specialized
  • Use commit aea424e with float*(int_or_float) rolled into one opcode
  • Add separate specializations for float*int and int*float*?

I am struggling to run the whole pyperformance suite at once, but as an example, bm_spectral_norm goes from

commit c64540af47ff336b074611d33e130e06cc06e5c2
    binary_multiply.specialization_success : 20
    binary_multiply.specialization_failure : 1062
    binary_multiply.hit : 5341671
    binary_multiply.deferred : 67921
    binary_multiply.miss : 24
    binary_multiply.deopt : 3
    binary_multiply.unquickened : 111

to

commit aea424ead30756f11d29e10643ccbfda82567676
    binary_multiply.specialization_success : 17
    binary_multiply.specialization_failure : 9
    binary_multiply.hit : 5409279
    binary_multiply.deferred : 337
    binary_multiply.miss : 0
    binary_multiply.deopt : 0
    binary_multiply.unquickened : 111

when incorporating int*float and float*int into BINARY_MULTIPLY_FLOAT.

I suppose we could gather specialization stats for what happens if int*float and float*int are added, and if those stats are just as good then the first-iteration-versus-the-rest type instability is not an issue.

@Fidget-Spinner

Fidget-Spinner commented Oct 6, 2021

Copy link
Copy Markdown
Member

nbody sped up, everything else looks in the realm of noise for pyperformance: https://gist.github.com/Fidget-Spinner/6fd3149fc82497d028b02046765ba8d8

@markshannon

Copy link
Copy Markdown
Member

To clarify, are you advising that the increased complexity of the opcode is not worth the increased percentage of instructions specialized? Is the solution to

* Use something like commit c64540a with only strictly `float*float` and `int*int` specialized

Yes.

* Use commit [aea424e](https://github.com/python/cpython/commit/aea424ead30756f11d29e10643ccbfda82567676) with `float*(int_or_float)` rolled into one opcode

* Add separate specializations for `float*int` and `int*float*`?

I don't think there is sufficient evidence that these are useful. At least, not yet.

I am struggling to run the whole pyperformance suite at once, but as an example, bm_spectral_norm goes from

commit c64540af47ff336b074611d33e130e06cc06e5c2
    binary_multiply.specialization_success : 20
    binary_multiply.specialization_failure : 1062
    binary_multiply.hit : 5341671
    binary_multiply.deferred : 67921
    binary_multiply.miss : 24
    binary_multiply.deopt : 3
    binary_multiply.unquickened : 111

The deferred operations only represent about 1%. Not worth worrying about.

@markshannon

Copy link
Copy Markdown
Member

In general, it is not the number of branches that matters, as much as their predictability.
Although there about ~1% deferred, in the numbers for bm_spectral_norm that you give, the number of misses is very low (~4 per million) meaning that the branches are extremely predictable.

@sweeneyde

Copy link
Copy Markdown
Member Author

I am beginning to think this PR may not be worth it at all: even on microbenchmarks, after PGO, there's not that much benefit:

Program:

from pyperf import Runner

runner = Runner()

runner.timeit(
    "int: x*x",
    setup="from itertools import repeat; it = repeat(1, 1_000_000)",
    stmt="for x in it: x*x")

runner.timeit(
    "float: x*x",
    setup="from itertools import repeat; it = repeat(1.0, 1_000_000)",
    stmt="for x in it: x*x")

runner.timeit(
    "int: x*...*x",
    setup="from itertools import repeat; it = repeat(1, 1_000_000)",
    stmt="for x in it: x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x")

runner.timeit(
    "float: x*...*x",
    setup="from itertools import repeat; it = repeat(1.0, 1_000_000)",
    stmt="for x in it: x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x")

Results from PGO on WSL:

Benchmark ./floatbench.json ../multiply_worktree/floatbench.json
int: x*...*x 205 ms 174 ms: 1.17x faster
float: x*...*x 238 ms 179 ms: 1.33x faster
Geometric mean (ref) 1.12x faster

Benchmark hidden because not significant (2): int: x*x, float: x*x

Results from pgo using build.bat (MSVC):

Benchmark ./floatbench.json ../multiply/floatbench.json
int: x*x 28.5 ns 27.7 ns: 1.03x faster
float: x*x 28.4 ns 27.9 ns: 1.02x faster
int: x*...*x 262 ms 220 ms: 1.19x faster
float: x*...*x 260 ms 253 ms: 1.03x faster
Geometric mean (ref) 1.06x faster

@sweeneyde sweeneyde closed this Oct 10, 2021
@markshannon

Copy link
Copy Markdown
Member

@sweeneyde
What sort of speedups were you expecting?

The speedups are worthwhile for less than 100 additional lines of code, IMO.

The speedups on the micro benchmark shows that it works.
The overall speedup, while modest, shows that it works in a wider context.

Don't forget that BINARY_MULTIPLY only represents a fairly small fraction of the total executed for the benchmark suite as a whole. This PR was never going to speed things up that much, but it is still a useful contribution.

@sweeneyde sweeneyde reopened this Oct 11, 2021
@sweeneyde

Copy link
Copy Markdown
Member Author

Okay, that makes sense, thanks. Re-opened.

@markshannon markshannon added the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Oct 13, 2021
@bedevere-bot

Copy link
Copy Markdown

🤖 New build scheduled with the buildbot fleet by @markshannon for commit 31c3bb9 🤖

If you want to schedule another build, you need to add the ":hammer: test-with-buildbots" label again.

@bedevere-bot bedevere-bot removed the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Oct 13, 2021
@markshannon

Copy link
Copy Markdown
Member

Buildbot failures seem to be the usual suspects.

@markshannon
markshannon merged commit 3b3d30e into python:main Oct 14, 2021
@bedevere-bot

Copy link
Copy Markdown

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

Labels

performance Performance or resource usage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants