New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stash and retrieve computed regex from a cache #598
Conversation
Profiler Diff Summary--- master branch https://travis-ci.org/gettalong/kramdown/jobs/536192573
+++ PR branch https://travis-ci.org/gettalong/kramdown/jobs/536298613
- Total allocated: 220.89 MB (1783975 objects)
- Total retained: 1.11 MB (1582 objects)
+ Total allocated: 210.65 MB (1737868 objects)
+ Total retained: 1.11 MB (1583 objects) |
|
No, didn't forget, just didn't have time... |
@ashmaroli Besides a 4,5% memory improvement and a 2,5% object allocation improvement, is it faster CPU-wise? |
I'm not sure how to benchmark this appropriately, both Was hoping you could provide me with the appropriate sample content that covers multiple use-cases... Until then, we could benchmark just the change.. Disclaimer: Micro-benchmarking such as this may be flawed because it disregards the overhead from rest of the codebase: require 'benchmark/ips'
class KParser
# Based purely on the two expressions generated while parsing Jekyll's docs
STOP_RE = /(\])|!?\[/
SPAN_START = /\*|_|`|<|<|\[|!?\[|[^\\]?["']|\$|\{:|&|--|\.\.\.|(?:\\| )?(?:<<|>>)|( |\\)(?=\n)|\\|~~/
def computed_pattern
/(?=#{Regexp.union(STOP_RE, SPAN_START)})/
end
def stashed_pattern
@stashed_pattern ||= {}
@stashed_pattern[STOP_RE] ||= {}
@stashed_pattern[STOP_RE][SPAN_START] ||= /(?=#{Regexp.union(STOP_RE, SPAN_START)})/
end
end
parser = KParser.new
return unless parser.computed_pattern == parser.stashed_pattern
Benchmark.ips do |x|
x.report('computed_pattern') { parser.computed_pattern }
x.report('stashed_pattern') { parser.stashed_pattern }
x.compare!
end |
Okay, so I checked how often the regexp would be compiled when generating the kramdown documentation, 1423 times. I didn't notice any runtime differences. Nonetheless, I think it's okay to merge this since the meaning of the change is quite clear when reading the code. One thing: Please move the creation of the hash into the initialization code with something like this: |
I think you're directing me to use an autovivifying hash: @cache ||= Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
@cache[foo][bar] ||= result However, from what I recall about profiling memory usage, the autovivified hash uses more memory (about 4-6x) than I'll try and rerun some tests.. |
Oh, we don't need the recursive auto-hash. We only need two levels, one for |
Apologies for jumping the gun back there. I've made the requested changes. Thanks. |
I thought about using a constant, too, but didn't mention it because I don't think there is much additional benefit to it. |
Thanks for the changes! I have merged them and moved the initialization of the hash into the class initializer - no need to perform that operation everytime. |
The Thanks for merging the change. |
The only difference is that the check of the LHS expression being truthy doesn't need to be done. So instructions are saved resulting in faster execution. As for the ETA: One last look through the open issues/PRs and then I will release the new version. So either today if I find the time, else tomorrow. |
Summary
Use a memoized cache to stash and retrieve computed regular expressions to avoid allocating duplicate regexes.
Context
Results from profiling Jekyll documentation source with
memory_profiler
Before
After