-
Notifications
You must be signed in to change notification settings - Fork 28
Reduce number of String allocations #24
Description
Here's a memory allocation report taken from one of our Rails endpoints with miniprofiler in production:
Total allocated: 36329328 bytes (541884 objects)
Total retained: 4395083 bytes (18004 objects)
allocated memory by gem
-----------------------------------
14044335 transit-ruby-ff2e3acd071a
4148691 temple-0.7.6
3993243 slim-3.0.6
3794721 activesupport-5.0.7
2622300 actionview-5.0.7
1827160 front/app
1686968 other
834761 dalli-2.7.6
787136 transit-rails-6d6b533ba1df
494528 set
491982 actionpack-5.0.7
430072 activerecord-5.0.7
315464 concurrent-ruby-1.0.5
...
You see that transit-ruby is at the top, which is no surprise because the endpoint renders a heavy transit response. But if we look closely on allocations grouped by file, then there's definitely a room for improvement:
We can do nothing with cruby/json.rb, because it's the oj's part:
transit-ruby/lib/transit/marshaler/cruby/json.rb
Lines 31 to 39 in b4973f8
| def emit_array_start(size) | |
| @state << :array | |
| @oj.push_array | |
| end | |
| def emit_array_end | |
| @state.pop | |
| @oj.pop | |
| end |
transit-ruby/lib/transit/marshaler/cruby/json.rb
Lines 59 to 65 in b4973f8
| def emit_value(obj, as_map_key=false) | |
| if @state.last == :array | |
| @oj.push_value(obj) | |
| else | |
| as_map_key ? @oj.push_key(obj) : @oj.push_value(obj) | |
| end | |
| end |
Same with the marshaler/base.rb, which produces a string interpolation each time:
transit-ruby/lib/transit/marshaler/base.rb
Lines 87 to 94 in b4973f8
| def emit_string(prefix, tag, value, as_map_key, cache) | |
| encoded = "#{prefix}#{tag}#{value}" | |
| if cache.cacheable?(encoded, as_map_key) | |
| emit_value(cache.write(encoded), as_map_key) | |
| else | |
| emit_value(encoded, as_map_key) | |
| end | |
| end |
But write handlers produce a large amount of static strings for nothing (lines 210, 234):
transit-ruby/lib/transit/write_handlers.rb
Lines 209 to 213 in b4973f8
| class KeywordHandler | |
| def tag(_) ":" end | |
| def rep(s) s.to_s end | |
| def string_rep(s) rep(s) end | |
| end |
transit-ruby/lib/transit/write_handlers.rb
Lines 233 to 237 in b4973f8
| class IntHandler | |
| def tag(i) i > MAX_INT || i < MIN_INT ? "n" : "i" end | |
| def rep(i) i > MAX_INT || i < MIN_INT ? i.to_s : i end | |
| def string_rep(i) i.to_s end | |
| end |
Line 211 also produces a large amount of strings, but it's a symbol to string conversion, which I believe is inevitable.
Another offender is RollingCache:
transit-ruby/lib/transit/rolling_cache.rb
Lines 49 to 51 in b4973f8
| def cacheable?(str, as_map_key=false) | |
| str.size >= MIN_SIZE_CACHEABLE && (as_map_key || str.start_with?("~#","~$","~:")) | |
| end |
Solution
After adding # frozen_string_literal: true magic comment to the top of the mentioned files, I see the much nicer picture:
Total allocated: 31684710 bytes (425875 objects)
Total retained: 4398650 bytes (18009 objects)
allocated memory by gem
-----------------------------------
9395815 transit-ruby/lib
4148546 temple-0.7.6
3993613 slim-3.0.6
3796808 activesupport-5.0.7
2621277 actionview-5.0.7
1830296 front/app
1686968 other
834761 dalli-2.7.6
787136 transit-rails-6d6b533ba1df
494528 set
491790 actionpack-5.0.7
429992 activerecord-5.0.7
315464 concurrent-ruby-1.0.5
that is, 20% less object allocations in total.
As for execution speed, I don't see any improvements in my local tests, but having less pressure on GC is always good.
Usually, those comments are being added throughout entire library to be Rails 3 ready.

