Skip to content

Faster copies of slotted objects, Accidental, and Pitch - #2072

Merged
mscuthbert merged 4 commits into
masterfrom
copy-speedups
Oct 10, 2026
Merged

mscuthbert merged 4 commits into
masterfrom
copy-speedups

Conversation

@mscuthbert

@mscuthbert mscuthbert commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Found an easy place to do some substantial speedups of Music21Object/ProtoM21Object with slots -- since we can't just copy the slots of the current class but also need to copy everything in the slots of the ancestors (unless we call super().deepcopy and they handle it), we need to have a way of figuring out all the slot names of everything up the ancestor chain. So we've used _getSlotsRecursive...but the thing is: the recursive slots never change for a class. So we should cache it. Big improvements in deepcopying speed:

copy before after
Accidental 1115 ns 394 ns
Pitch, no accidental 877 ns 490 ns
Pitch, sharp or natural 1966 ns 803 ns
Duration 3089 ns 2777 ns
Tie 2181 ns 1888 ns
Beams 8203 ns 6906 ns
Volume 1345 ns 1087 ns
Note, sharp 12626 ns 10738 ns
Measure of 4 notes 99703 ns 89099 ns
pickle Note 11332 ns 9838 ns

For m21 slotted objects, _getSlotsRecursive now caches its result once per class as a frozenset (callers only loop over it).

The numbers above for Pitch and Accidental are incorrect. they save about the same 220ns that everything else does. Agent had tried to have Accidental.__deepcopy__ and Pitch.__deepcopy__ list every attribute directly and figure out the best way to deal with that. I'm not ready to give up the flexibility of subclassing these classes etc.

AI-assisted (Claude)

…copy

_getSlotsRecursive caches its result per class as a frozenset (callers
only loop over it), which speeds up copying and pickling every slotted
object.  Accidental.__deepcopy__ and Pitch.__deepcopy__ assign each
attribute directly instead of looping with getattr/setattr; Accidental's
copy no longer carries over _client.  Pitch attributes set after __init__
are still deep-copied.

Accidental        1115 ns -> 394 ns
Pitch, sharp      1966 ns -> 803 ns
Pitch, no acc.     877 ns -> 490 ns
Duration          3089 ns -> 2777 ns
Beams             8203 ns -> 6906 ns
Note, sharp      12626 ns -> 10738 ns
pickle Note      11332 ns -> 9838 ns

AI-assisted (Claude)
# Conflicts:
#	music21/test/test_pitch.py
@coveralls

coveralls commented Oct 8, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 93.372% (+0.001%) from 93.371% — copy-speedups into master

@mscuthbert
mscuthbert marked this pull request as ready for review October 8, 2026 09:37
Accidental.__deepcopy__ and Pitch.__deepcopy__ go back to master's
loops, so a slot or attribute added to a class or its parents is still
copied without anyone updating a list.  The tests that checked those
lists go too.

Accidental.__init__ calls super().__init__() again instead of inlining
StyleMixin.__init__ (#2070), which listed a parent's attributes in a
subclass.  That costs about 30 ns per Accidental (91 -> 121 ns).

What is left is the per-class _getSlotsRecursive cache.  Best of 7,
master -> branch:
  copy Accidental            1128 ->   787 ns
  copy Pitch, sharp          1964 ->  1627 ns
  copy Duration              3153 ->  2797 ns
  copy Note, sharp          12793 -> 11982 ns
  copy Measure of 4 notes  101429 -> 97116 ns
  pickle Note               11731 -> 10043 ns

AI-assisted (Claude)
@mscuthbert
mscuthbert merged commit 9a9aeca into master Oct 10, 2026
7 checks passed
@mscuthbert
mscuthbert deleted the copy-speedups branch October 10, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants