[perf] Increase test performance by 274% with this one weird trick... #1152
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Motivation
How to test the behavior?
(run the nwb unit tests)
Description
Profiling the pynwb tests, it turns out that a single
deepcopy
call accounted for almost 2/3 of the run time - the one withinhdmf.spec.spec.ConstructableDict.build_const_args
.This method is called in three places:
hdmf/src/hdmf/spec/spec.py
Line 93 in dfb1df7
hdmf/src/hdmf/spec/spec.py
Line 149 in dfb1df7
hdmf/src/hdmf/spec/spec.py
Line 630 in dfb1df7
When running the tests, I inserted an
inspect
call to capture the stack traces to see what the higher-order things that call this were, and the only methods that call this one are:NamespaceCatalog.__load_namespace
:hdmf/src/hdmf/spec/namespace.py
Line 433 in dfb1df7
NamespaceToBuilderHelper.__copy_spec
:hdmf/src/hdmf/backends/utils.py
Line 83 in dfb1df7
load_namespace
is only called when specs are loaded from a file, so a deepcopy is unnecessary here: the deepcopy protects the object being copy from mutation downstream, but the upstream object is discarded immediately and comes from a file (which is not mutated)the call tree above
__copy_spec
is linear with one fork at the end:NamespaceToBuilderHelper.convert_namespace
HDF5IO.__cache_spec
HDF5IO.export
HDF5IO.write
neither of which mutate the object after the
deepcopy
operation should get called, as far as I can tell.The thing that we would want to protect is some downstream consumer of the spec from mutating the spec (ie. it should always be a veridical reflect the spec source), but the deepcopy doesn't protect from that since it's only called when instantiating these spec objects.
When running the tests, however, i got a bunch of errors that i couldn't quite understand, but it seemed like it must be some confused equality comparison, and I noticed that the
Spec.__hash__
method was set as being just theid
of the object - usually dicts aren't hashable bc they are mutable, but that's usually not a problem, except it is here because it seems like these Spec objects are often used as keys in a dictionary.it seems like the primary thing that
deepcopy
was doing is giving the object a newid
- which is confirmed by simply usingcopy
instead ofdeepcopy
orhash(str(self))
as the__hash__
function. So it seems like in some places we are forwarding the objects around and getting "hash collisions" or "hash misses" when usingid
.SO i tried replacing it with the cheapest hash function money can buy, the aforementioned
hash(str(self))
(which is not ideal lol because it violates the other requirement of__hash__
), but just as a demo of what the problem is and what a potential solution is bc y'all know better how these classes are used...Results
So anyway, when running pynwb tests we go from this:
to this:
which is fun. (the errors are because i didn't have pytest installed)
this impact is way more dramatic in something like pynwb where we are loading a ton of complex specs over and over again than it is in the hdmf tests, but it is a pretty dramatic perf improvement for basically free, thought y'all woudl be interested.
Checklist
I have to run out the door rn but i'll return later to finish up any quality checks, but anyway mostly just a "hey here's free perf" pull request as a question bc i figure there are reasons this is not good to do (or you might want a "real hash function")
CHANGELOG.md
with your changes?ttyl xoxo <3