Last active
May 31, 2019 17:43
-
-
Save pushrax/bd2d3bb99c46ed0570cfbb7d5e82d742 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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