Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce memory usage of .fetch_path #55

Merged
merged 1 commit into from
Aug 11, 2017

Conversation

NickLaMuro
Copy link
Member

@NickLaMuro NickLaMuro commented Aug 10, 2017

Similar rational to #54

A note about the implementation

Of note, I attempted to use both .each_with_object and .inject instead of implementing something myself. But each had it's own problems that were either broken or sub-optimal:

.each_with_object

The .each_with_object implementation looked something like this:

args.each.with_index.each_with_object(self) do |(key, i), result|

Obviously this allows us to omit the result = self line above it, but this implementation just failed to function in some scenarios and I was unable to determine the cause.

.inject

The .inject implementation was similar to the above .each_with_object implementation:

args.each.with_index.inject(self) do |result, (key, i)|

While actually functioning as expected, and just making sure to end the block with a final call of result, the combination of having to do .each, .with_index, and .inject to achieve having both a indexed loop and a resulting object actually caused some extra objects to be
created.

Being explicit here and avoiding the ruby convenience methods seemed to both prevent extraneous objects instantiation and provide the results expected.

Benchmarks

I used the existing benchmarks for #54 (an effort originating from ManageIQ/manageiq#15757) as a base, and applied these changes on top of them when doing the benchmarks to see the over all effect.

Total Mem Allocated Total Objects Allocated
BEFORE 68871116 bytes 854336 objects
AFTER #54 64535996 bytes 745958 objects
AFTER #54 & #55 (using .inject) 66617276 bytes 749166 objects
AFTER #54 & #55 (without .inject) 62531396 bytes 695843 objects

Comparing the objects allocated by gem, we can see that more_core_extensions is the only one to change and improve between runs:

BEFORE                                               AFTER PR #54                                         AFTER PR #54 & #55

allocated objects by gem                           | allocated objects by gem                           | allocated objects by gem                           
-----------------------------------                | -----------------------------------                | -----------------------------------
    220461  activerecord-5.0.5                     |     220461  activerecord-5.0.5                     |     220461  activerecord-5.0.5
    178489  more_core_extensions-3.3.0  <<<<<<     |     125700  activesupport-5.0.5                    |     125700  activesupport-5.0.5
    123360  activesupport-5.0.5                    |      77606  ruby-2.3.3/lib                         |      77606  ruby-2.3.3/lib
     77606  ruby-2.3.3/lib                         |      70824  manageiq-providers-vmware-b45ffbbaefb3 |      70824  manageiq-providers-vmware-b45ffbbaefb3
     70824  manageiq-providers-vmware-b45ffbbaefb3 |      67771  more_core_extensions/lib  <<<<<<       |      56948  manageiq/app
     56948  manageiq/app                           |      56948  manageiq/app                           |      33983  activemodel-5.0.5
     33983  activemodel-5.0.5                      |      33983  activemodel-5.0.5                      |      31718  manageiq/lib
     31718  manageiq/lib                           |      31718  manageiq/lib                           |      25557  pending
     25557  pending                                |      25557  pending                                |      24228  arel-7.1.4
     24228  arel-7.1.4                             |      24228  arel-7.1.4                             |      17656  more_core_extensions/lib  <<<<<<
      4675  american_date-1.1.1                    |       4675  american_date-1.1.1                    |       4675  american_date-1.1.1
      4125  other                                  |       4125  other                                  |       4125  other
      1804  fast_gettext-1.2.0                     |       1804  fast_gettext-1.2.0                     |       1804  fast_gettext-1.2.0
       555  tzinfo-1.2.3                           |        555  tzinfo-1.2.3                           |        555  tzinfo-1.2.3
         2  default_value_for-3.0.2                |          2  default_value_for-3.0.2                |          2  default_value_for-3.0.2
         1  config-1.3.0                           |          1  config-1.3.0                           |          1  config-1.3.0

Links

@Fryguy
Copy link
Member

Fryguy commented Aug 11, 2017

Really nice improvement in both #54 and #55. As mentioned in #54, let's just get some cross-type specs added.

By using recursion to loop through the keys for fetch_path, we are
creating new objects that are not needed every time with go further down
into the stack, because we are required to do a `args[1..-1].push`,
which will create a new array from the subset of the args, and pass it
into the next call of `.fetch_path`.

By not using recursion, we can completely remove any object allocations
that aren't necessary (besides the ones that are needed for the method's
spec), and still maintain the same functionality.  The added benefit is
that this also now not prone to `stack level too deep` errors because it
no longer requires recursion to function.

Of note, I attempted to use both `.each_with_object` and `.inject`
instead of implementing something myself.  But each had it's own
problems that were either broken or sub-optimal:

The `.each_with_object` implementation looked something like this:

    args.each.with_index.each_with_object(self) do |(key, i), result|

Obviously not requiring the `result = self` line above it.  This just failed to
function in some scenarios and I was unable to determine the cause.

The `.inject` implementation was similar to the above
`.each_with_object` implementation:

    args.each.with_index.inject(self) do |result, (key, i)|

While actually functioning as expected, and just making sure to end the
block with a final call of `result`, the combination of having to do
`.each`, `.with_index`, and `.inject` to achieve having both a indexed
loop and a resulting object actually caused some extra objects to be
created.

Being explicit here and avoiding the ruby convenience methods seemed to
both prevent extraneous objects instantiation and provide the results
expected.
@miq-bot
Copy link
Member

miq-bot commented Aug 11, 2017

Checked commit NickLaMuro@77e7a48 with ruby 2.2.6, rubocop 0.47.1, and haml-lint 0.20.0
1 file checked, 0 offenses detected
Everything looks fine. 🏆

@Fryguy Fryguy merged commit 19202e1 into ManageIQ:master Aug 11, 2017
@NickLaMuro
Copy link
Member Author

🎊

Fryguy added a commit that referenced this pull request Aug 11, 2017
Added
- Added Module#cache_with_timeout [[#51](#51)]

Changed
- Performance improvements to store_path [[#54](#54)]
  and fetch_path [[#55](#55)]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants