8000 Reduce allocations in keys#read key by Antsiscool · Pull Request #766 · mongomapper/mongomapper · GitHub
[go: up one dir, main page]

Skip to content

Reduce allocations in keys#read key - #766

Merged
smtlaissezfaire merged 2 commits into
mongomapper:masterfrom
Antsiscool:reduce-allocations-in-keys#read_key
Nov 3, 2026
Merged

smtlaissezfaire merged 2 commits into
mongomapper:masterfrom
Antsiscool:reduce-allocations-in-keys#read_key

Conversation

@Antsiscool
Copy link
Copy Markdown
Contributor

Ruby 3.0 added Symbol#name which returns an interned string. This reduces allocations.

master     :title        0.349s      1000002 allocations (1.00 per call)
master     "title"       0.338s            0 allocations (0.00 per call)
current    :title        0.304s            2 allocations (0.00 per call)
current    "title"       0.322s            0 allocations (0.00 per call)

master     :title        0.349s      1000002 allocations (1.00 per call)
master     "title"       0.337s            0 allocations (0.00 per call)
current    :title        0.299s            2 allocations (0.00 per call)
current    "title"       0.321s            0 allocations (0.00 per call)

Here is the benchmark code:

# Benchmarks MongoMapper::Plugins::Keys#read_key on the current checkout
# against master (or any git ref passed as the first argument).
#
#   ruby performance/read_key.rb          # compare against master
#   ruby performance/read_key.rb v0.17.0  # compare against another ref
#
# No database connection is needed; documents are only built in memory.

require 'rbconfig'
require 'tmpdir'

ITERATIONS = 1_000_000
ROUNDS = 5

if ENV['MM_BENCH_LIB']
  $:.unshift ENV['MM_BENCH_LIB']
  require 'mongo_mapper'

  class BenchDoc
    include MongoMapper::Document
    key :title, String
    key :count, Integer
  end

  doc = BenchDoc.load('_id' => BSON::ObjectId.new, 'title' => 'Testing', 'count' => 1)

  # The key is created once, outside the loop, so a String literal
  # doesn't add an allocation of its own to every call.
  [:title, 'title'].each do |key_name|
    # warm up
    10_000.times { doc[key_name] }

    GC.start
    GC.disable
    before = GC.stat(:total_allocated_objects)
    ITERATIONS.times { doc[key_name] }
    allocations = GC.stat(:total_allocated_objects) - before
    GC.enable

    # best of ROUNDS to reduce noise from other processes
    real = ROUNDS.times.map do
      start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      ITERATIONS.times { doc[key_name] }
      Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
    end.min

    puts format('%-10s %-8s %10.3fs %12d allocations (%.2f per call)',
                ENV['MM_BENCH_LABEL'], key_name.inspect, real, allocations, allocations.to_f / ITERATIONS)
  end
else
  root = `git rev-parse --show-toplevel`.strip
  ref = ARGV[0] || 'master'

  Dir.mktmpdir do |dir|
    system("git -C #{root} archive #{ref} lib | tar -x -C #{dir}", exception: true)

    puts "ruby #{RUBY_VERSION}, #{ITERATIONS} calls to read_key, best of #{ROUNDS} rounds"
    [[ref, File.join(dir, 'lib')], ['current', File.join(root, 'lib')]].each do |label, lib|
      env = { 'MM_BENCH_LIB' => lib, 'MM_BENCH_LABEL' => label }
      system(env, RbConfig.ruby, __FILE__, exception: true)
    end
  end
end

Symbol#name returns an interned frozen string, whilst Symbol#to_s allocates a new string.
@Antsiscool
Copy link
Copy Markdown
Contributor Author

@smtlaissezfaire This further reduces some allocations and improves the performance by storing ivar in a local variable.

@smtlaissezfaire
smtlaissezfaire merged commit dbe9faf into mongomapper:master Nov 3, 2026
66 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0