[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
deepcopycall 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
inspectcall 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_namespaceis 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_specis linear with one fork at the end:NamespaceToBuilderHelper.convert_namespaceHDF5IO.__cache_specHDF5IO.exportHDF5IO.writeneither of which mutate the object after the
deepcopyoperation 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 theidof 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
deepcopywas doing is giving the object a newid- which is confirmed by simply usingcopyinstead ofdeepcopyorhash(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.mdwith your changes?ttyl xoxo <3