Skip to content

Instantly share code, notes, and snippets.

@pushrax
Last active May 31, 2019 17:43
Show Gist options
  • Save pushrax/bd2d3bb99c46ed0570cfbb7d5e82d742 to your computer and use it in GitHub Desktop.
Save pushrax/bd2d3bb99c46ed0570cfbb7d5e82d742 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
require 'memory_profiler'
def assert_equal(a, b)
puts "#{a} != #{b} at #{caller.last}" if a != b
end
def assert(allocated_memsize: nil, allocated_objects: nil)
report = MemoryProfiler.report { yield }
assert_equal(allocated_memsize, report.total_allocated_memsize) if allocated_memsize
assert_equal(allocated_objects, report.total_allocated) if allocated_objects
end
# An object is a 40-byte GC-heap allocation
assert(allocated_memsize: 40, allocated_objects: 1) { +"a" }
# Strings up to 23 bytes inline the string in the object
# The lost byte from 3*8=24 is for the null terminator
assert(allocated_memsize: 40) { "a" * 23 }
# Past this, they malloc() or use the transient heap in Ruby 2.6+
assert(allocated_memsize: 40 + 25) { "a" * 24 }
# Arrays up to 3 elements (3*8=24 bytes) inline the elements
assert(allocated_memsize: 40) { [1,2,3] }
# Past this, they malloc() one 8-byte VALUE per slot
assert(allocated_memsize: 40 + 4 * 8) { [1,2,3,4] }
assert(allocated_memsize: 40 + 5 * 8) { [1,2,3,4,5] }
assert(allocated_memsize: 40 + 6 * 8) { [1,2,3,4,5,6] }
# Empty hashes are just an object
assert(allocated_memsize: 40, allocated_objects: 1) { {} }
# But with just 1 element, they malloc()
assert(allocated_memsize: 40 + 152) { {a: 1} }
# You can fit 3 elements in this size though
assert(allocated_memsize: 40 + 152) { {a: 1, b: 2, c: 3} }
assert(allocated_memsize: 40 + 248) { {a: 1, b: 2, c: 3, d: 4} }
# Structs are like arrays in allocation
struct = Struct.new(:a, :b, :c)
assert(allocated_memsize: 40) { struct.new }
assert(allocated_memsize: 40) { struct.new(1,2,3) }
# They also malloc() after 3 elements, and are smaller than hashes
# Use a Struct over Hash for dynamic objects if you know the keys in advance
struct = Struct.new(:a, :b, :c, :d)
assert(allocated_memsize: 40 + 32) { struct.new(1,2,3,4) }
struct = Struct.new(:a, :b, :c, :d, :e)
assert(allocated_memsize: 40 + 40) { struct.new(1,2,3,4,5) }
struct = Struct.new(:a, :b, :c, :d, :e, :f)
assert(allocated_memsize: 40 + 48) { struct.new(1,2,3,4,5,6) }
# Classes with instance variables allocate almost like structs
cls = Class.new do
def initialize(a, b, c)
@a = a; @b = b; @c = c
end
end
assert(allocated_memsize: 40) { cls.new(1,2,3) }
cls = Class.new do
def initialize(a, b, c, d)
@a = a; @b = b; @c = c; @d = d
end
end
assert(allocated_memsize: 80) { cls.new(1,2,3,4) }
cls = Class.new do
def initialize(a, b, c, d, e)
@a = a; @b = b; @c = c; @d = d; @e = e
end
end
assert(allocated_memsize: 80) { cls.new(1,2,3,4,5) }
cls = Class.new do
def initialize(a, b, c, d, e, f)
@a = a; @b = b; @c = c; @d = d; @e = e; @f = f
end
end
assert(allocated_memsize: 96) { cls.new(1,2,3,4,5,6) }
cls = Class.new do
def initialize(a, b, c, d, e, f, g)
@a = a; @b = b; @c = c; @d = d; @e = e; @f = f; @g = g
end
end
assert(allocated_memsize: 96) { cls.new(1,2,3,4,5,6,7) }
# Nothing different about dynamic ivars
$ivars = %i(@a @b @c)
cls = Class.new do
def initialize
$ivars.each { |i| instance_variable_set(i, 42) }
end
end
assert(allocated_memsize: 40) { cls.new }
$ivars = %i(@a @b @c @d @e)
assert(allocated_memsize: 80) { cls.new }
$ivars = %i(@a @b @c @d @e @f @g)
assert(allocated_memsize: 96) { cls.new }
# Method arguments
def method(a:, b:, c:); end
assert(allocated_memsize: 0) { method(a: 1, b: 2, c: 3) }
def method(a:, b:, c:, d:, e:); end
assert(allocated_memsize: 0) { method(a: 1, b: 2, c: 3, d: 4, e: 5) }
def method(**args); end
assert(allocated_memsize: 40) { method }
def method(**args); end
assert(allocated_memsize: 576) { method(a: 1) }
def method(a: 1, **args); end
assert(allocated_memsize: 192) { method(a: 1) }
def method(a: 1, **args); end
assert(allocated_memsize: 192) { method(a: 1, b: 2) }
def method(a: 1, **args); end
assert(allocated_memsize: 288) { method(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6) }
def method(); end
assert(allocated_memsize: 0) { method }
def method(*); end
assert(allocated_memsize: 40) { method }
def method(*args); end
assert(allocated_memsize: 40) { method }
def method(*args); end
assert(allocated_memsize: 80) { method(1) }
def method(*args); end
assert(allocated_memsize: 80) { method(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42) }
def method(*args); end
array = (1..42).to_a
assert(allocated_memsize: 80) { method(*array) }
#####
def method(a, b: 1, **args); end
assert(allocated_memsize: 192, allocated_objects: 1) { method(1, b: 2, c: 3, d: 4) }
def method(a, b, **args); end
assert(allocated_memsize: 576, allocated_objects: 3) { method(1,2, c: 3, d: 4) }
# Calling String#match() with a regex that was created in scope avoids an allocation ???
re = /(.*)/
str = "a" * 32
assert(allocated_memsize: 393, allocated_objects: 3) { str.match(re)[1] }
assert(allocated_memsize: 320, allocated_objects: 2) { str.match(/(.*)/)[1] }
assert(allocated_memsize: 320, allocated_objects: 2) { re = /(.*)/; str.match(re)[1] }
# Regex named captures don't allocate more than positional captures,
# and make complex patterns easier to understand.
re = /(?<named_capture>.*)/
str = "a" * 32
assert(allocated_memsize: 393, allocated_objects: 3) { str.match(re)[:named_capture] }
assert(allocated_memsize: 320, allocated_objects: 2) { str.match(/(?<named_capture>.*)/)[:named_capture] }
re = /(.*) /
assert(allocated_memsize: 0, allocated_objects: 0) { str.match(re) }
assert(allocated_memsize: 0, allocated_objects: 0) { str.match(/(.*) /) }
# Yield
def base
yield 42
end
def one(&block)
base(&block)
end
def two
base { |x| yield x }
end
assert(allocated_memsize: 0) { one { |x| } }
assert(allocated_memsize: 0) { two { |x| } }
puts("checked all assertions")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment