Skip to content

Instantly share code, notes, and snippets.

@shugo
Last active June 19, 2026 04:07
Show Gist options
  • Select an option

  • Save shugo/b58640e4b6c6df76f857159d79a5eb74 to your computer and use it in GitHub Desktop.

Select an option

Save shugo/b58640e4b6c6df76f857159d79a5eb74 to your computer and use it in GitHub Desktop.
Benchmark of Proc#with_refinements in multi-Ractor mode
module M
refine String do
def shout = upcase + "!"
end
end
N = 2_000_000
prc = ->(s) { s }
# Warm up the memo
prc.with_refinements(M)
# Baseline: plain proc call
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
N.times { prc.call("x") }
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
baseline = (t1 - t0) / N * 1e9
# Phase 1: Single-Ractor mode (rb_multi_ractor_p() == false)
# Memo hit path only
times_single = []
3.times do
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
N.times { prc.with_refinements(M) }
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
times_single << (t1 - t0) / N * 1e9
end
single = times_single.min
# Create a Ractor to flip multi-Ractor mode on permanently
r = Ractor.new { true }
r.value
# Phase 2: Multi-Ractor mode, no contention (main Ractor only)
times_multi = []
3.times do
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
N.times { prc.with_refinements(M) }
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
times_multi << (t1 - t0) / N * 1e9
end
multi = times_multi.min
puts "=== RB_VM_LOCKING() overhead on with_refinements memo hit ==="
puts
printf(" Proc#call (baseline): %6.1f ns/call\n", baseline)
printf(" with_refinements (1 Ractor): %6.1f ns/call\n", single)
printf(" with_refinements (multi): %6.1f ns/call\n", multi)
printf(" Lock overhead (no contention):%6.1f ns/call (%.2fx)\n",
multi - single, multi / single)
require 'objspace'
module M
refine String do
def shout = upcase + "!"
end
end
module A
refine(String) { def shout = "A" }
end
# Measure per-memo overhead precisely using a single proc
prc = ->(s) { s.shout }
GC.start; GC.start; GC.start
before = GC.stat(:heap_live_slots)
refined = prc.with_refinements(M)
GC.start; GC.start; GC.start
after = GC.stat(:heap_live_slots)
printf("Single with_refinements:\n")
printf(" New live objects: +%d\n", after - before)
# Measure the T_DATA/struct size directly
memo_iseq = RubyVM::InstructionSequence.of(prc)
printf(" iseq memsize (original prc): %d bytes\n", ObjectSpace.memsize_of(memo_iseq))
refined_iseq = RubyVM::InstructionSequence.of(refined)
printf(" iseq memsize (refined prc): %d bytes\n", ObjectSpace.memsize_of(refined_iseq))
# Measure memo replacement (alternating modules)
prc2 = ->(s) { s.shout }
prc2.with_refinements(M) # warm
GC.start; GC.start; GC.start
before2 = GC.stat(:heap_live_slots)
prc2.with_refinements(A) # replace memo
GC.start; GC.start; GC.start
after2 = GC.stat(:heap_live_slots)
printf("\nMemo replacement (A -> B):\n")
printf(" Net live objects after replace+GC: %+d\n", after2 - before2)
# Bulk test: 1000 distinct blocks
N = 1000
BLOCK_SRC = "->(s) { s.shout }"
blocks = eval("[" + ([BLOCK_SRC] * N).join(",") + "]")
GC.start; GC.start; GC.start
before3 = GC.stat(:heap_live_slots)
before3_mem = GC.stat(:malloc_increase_bytes)
refined_all = blocks.map { |b| b.with_refinements(M) }
GC.start; GC.start; GC.start
after3 = GC.stat(:heap_live_slots)
printf("\nBulk with_refinements x%d (same module, shared memo):\n", N)
printf(" New live objects: +%d\n", after3 - before3)
printf(" Per-block overhead: %.1f objects\n", (after3 - before3).to_f / N)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment