Skip to content

Optimize leading null unpack idiom in bytecode, allows bare BUILD_SET 0 #150737

@gesslerpd

Description

@gesslerpd

Feature or enhancement

Proposal:

Performance was discussed at various times within PEP 802 topic, submitting this proposal as the PEP is unlikely to be accepted.

Goal:

  • Remove performance penalty of ast.unparse(ast.Set(elts=[])) ({*()} can be used in advanced cases and will not change recommended way to write empty set set())
  • Remove performance penalty of incremental construction between 0-to-1/1-to-0 elements for set and tuple literals without special case considerations (e.g. x = *(), pre-seeded tuple literal without trailing comma pitfall)
  • Keep implementation in simple/self-contained in bytecode generation, avoiding any AST changes to language
  • Near-zero performance impact to bytecode generation (improves subsequent bytecode optimizations of target case)

Implementation examples:

Micro-benchmark

Run on windows x64bit release build: empty_unpack_benchmark.py

Compile time

Targeted cases:

Benchmark Main Flowgraph only Codegen leading only
[*()] 1.450 ms / 1.000x 1.393 ms / 0.961x 1.378 ms / 0.950x
{*()} 1.530 ms / 1.000x 1.488 ms / 0.973x 1.467 ms / 0.959x
(*(),) 1.916 ms / 1.000x 1.842 ms / 0.962x 1.832 ms / 0.956x
x = *(), 1.139 ms / 1.000x 1.088 ms / 0.955x 1.055 ms / 0.927x
xychart-beta
	title "Compile targeted mean ratio vs main"
	x-axis [Main, Flowgraph, Leading]
	y-axis "Ratio" 0 --> 1.01
	bar [1.000, 0.963, 0.948]
Loading

Control near-miss case comparisons moved less than +/-1% and are attributed to noise.

Runtime

The set() versus {*()} comparison is especially useful because it changes qualitatively across the two branches (~6.3% slower to ~29.5% faster):

Branch set() mean (ns) {*()} mean (ns) {*()} vs set()
Main 54.14 57.53 1.063x
Flowgraph only 53.49 38.01 0.711x
Codegen leading only 55.44 39.00 0.704x

Before this change, {*()} was paying for a redundant empty update and ended up slower than the constructor call. After the change, {*()} compiles down to a direct BUILD_SET 0; RETURN_VALUE path, while set() still has to load the global and perform a zero-argument call.

Has this already been discussed elsewhere?

I have already discussed this feature proposal on Discourse

Links to previous discussion of this feature:

https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676/237
https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676/216
https://discuss.python.org/t/pep-802-display-syntax-for-the-empty-set/101676/3

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-featureA feature request or enhancement
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions