While working on optimization on our codebase we tried to use bang methods to reduce object allocations where it made sense but we observed in benchmarks that the number of objects allocated decreased but overall memsize increased.
Reproduction Script:
# frozen_string_literal: true
require 'bundler/inline'
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem 'benchmark-memory', '0.1.2'
end
require 'benchmark/memory'
def with_bang(*methods)
methods.tap(&:flatten!)
end
def without_bang(*methods)
methods.flatten
end
Benchmark.memory do |x|
x.report("with_bang") { with_bang(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o) }
x.report("without_bang") { without_bang(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o) }
x.compare!
end
# Output
# Ruby version: ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-darwin19]
# INPUT: (:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o)
# Calculating -------------------------------------
# with_bang 160.000 memsize ( 0.000 retained)
# 1.000 objects ( 0.000 retained)
# 0.000 strings ( 0.000 retained)
# without_bang 80.000 memsize ( 0.000 retained)
# 2.000 objects ( 0.000 retained)
# 0.000 strings ( 0.000 retained)
# Comparison:
# without_bang: 80 allocated
# with_bang: 160 allocated - 2.00x more
# INPUT: (:a, :b, :c, :d, :e, [:f, :g], :h, :i, :j, :k, :l, :m, :n, :o)
# Calculating -------------------------------------
# with_bang 240.000 memsize ( 0.000 retained)
# 3.000 objects ( 0.000 retained)
# 0.000 strings ( 0.000 retained)
# without_bang 480.000 memsize ( 0.000 retained)
# 3.000 objects ( 0.000 retained)
# 0.000 strings ( 0.000 retained)
# Comparison:
# with_bang: 240 allocated
# without_bang: 480 allocated - 2.00x more
In my experimentation, I believe it is due to the splat operator conversion to an array. Following is the script which is hinting me to this conclusion.
# frozen_string_literal: true
require 'bundler/inline'
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem 'benchmark-memory', '0.1.2'
end
require 'benchmark/memory'
def with_splat(*methods)
methods.flatten!
end
def without_splat
methods = [:a, :b, :c, :d, :e, [:f, :g], :h, :i, :j, :k, :l, :m, :n, :o]
methods.flatten!
end
Benchmark.memory do |x|
x.report("with_splat") { with_splat(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o) }
x.report("without_splat") { without_splat }
x.compare!
end
# Output
# Ruby version: ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-darwin19]
# INPUT: (:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o)
# Calculating -------------------------------------
# with_splat 160.000 memsize ( 0.000 retained)
# 1.000 objects ( 0.000 retained)
# 0.000 strings ( 0.000 retained)
# without_splat 40.000 memsize ( 0.000 retained)
# 1.000 objects ( 0.000 retained)
# 0.000 strings ( 0.000 retained)
# Comparison:
# without_splat: 40 allocated
# with_splat: 160 allocated - 4.00x more
# INPUT: (:a, :b, :c, :d, :e, [:f, :g], :h, :i, :j, :k, :l, :m, :n, :o)
# Calculating -------------------------------------
# with_splat 240.000 memsize ( 0.000 retained)
# 3.000 objects ( 0.000 retained)
# 0.000 strings ( 0.000 retained)
# without_splat 240.000 memsize ( 0.000 retained)
# 3.000 objects ( 0.000 retained)
# 0.000 strings ( 0.000 retained)
# Comparison:
# with_splat: 240 allocated
# without_splat: 240 allocated - same
What am I missing to understand this behavior? And why is it behaving in this manner?
Thank you!
EDIT: I have added new input to the benchmark comparisons which contain a nested array. With new input, we are seeing a different result than the previous benchmark and I am more confused!