Skip to content
This repository has been archived by the owner on Jun 3, 2023. It is now read-only.

Reduce number of String allocations #24

Open
nashbridges opened this issue Sep 7, 2018 · 0 comments
Open

Reduce number of String allocations #24

nashbridges opened this issue Sep 7, 2018 · 0 comments

Comments

@nashbridges
Copy link

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:

image

image

We can do nothing with cruby/json.rb, because it's the oj's part:

def emit_array_start(size)
@state << :array
@oj.push_array
end
def emit_array_end
@state.pop
@oj.pop
end

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:

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):

class KeywordHandler
def tag(_) ":" end
def rep(s) s.to_s end
def string_rep(s) rep(s) end
end

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:

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant