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

Reduce number of String allocations #24

@nashbridges

Description

@nashbridges

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions