Skip to content

Reduce memory allocations for `StrongMemoize`

What does this MR do and why?

The strong_memoize is one of the most frequently used methods. Doing a few improvements we can significantly reduce GC pressure (4x) and CPU pressure (1.66x).

Comments:

  • As shown by the https://gitlab-org.gitlab.io/-/gitlab/-/jobs/2147998302/artifacts/coverage/index.html#1d4d1359e610fa76c91f0dd77b07994b9140eb42 the strong_memoize is called 40_501_201 times for specs...
  • The main usage pattern of strong_memoize is to pass strong_memoize(:name), but the strong_memoize("name") is possible as well
  • The change retains and tests both (string and symbol)
  • Previously, each call would use 4 heap slots, since the ivar was executed 2x
  • Previously, the ivar would do name.to_s and create a new string with "@#{name}", this for symbol would result in two heap slots
  • Now, the implementation is optimised to have exactly single allocation
  • Now, the implementation is optimised to call ivar exactly once

Benchmark

  #strong_memoize
    memory allocation
      for Symbol
        performant
                     user     system      total        real
legacy non-nil   0.742533   0.013884   0.756417 (  0.756915)
legacy nil       0.734473   0.000594   0.735067 (  0.735587)
new non-nil      0.463679   0.003887   0.467566 (  0.468097)
new nil          0.441397   0.000000   0.441397 (  0.441593)
      for String
        performant
                     user     system      total        real
legacy non-nil   0.567696   0.000000   0.567696 (  0.568327)
legacy nil       0.564160   0.000000   0.564160 (  0.564414)
new non-nil      0.447927   0.000000   0.447927 (  0.448299)
new nil          0.439982   0.000000   0.439982 (  0.440183)
benchmark.rb
[:method_name, "method_name"].each do |argument|
  context "for #{argument.class}" do
    it 'performant' do
      n = 1_000_000

      result = Benchmark.bm(14) do |x|
        x.report("legacy non-nil") do
          object.clear_memoization(argument)

          n.times do
            object.legacy_strong_memoize(argument) { 10 }
          end
        end

        x.report("legacy nil") do
          object.clear_memoization(argument)

          n.times do
            object.legacy_strong_memoize(argument) { nil }
          end
        end

        x.report("new non-nil") do
          object.clear_memoization(argument)

          n.times do
            object.strong_memoize(argument) { 10 }
          end
        end

        x.report("new nil") do
          object.clear_memoization(argument)

          n.times do
            object.strong_memoize(argument) { nil }
          end
        end
      end
    end
  end
end

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Kamil Trzciński (Back 2025-01-01)

Merge request reports

Loading