Reputation: 87
I have been reading about ways to reduce memory usage in my Ruby/Rails app, and one thing that is mentioned is freezing objects. I have tried the code below (MRI, Ruby 2.3.3) and it does save memory, according to Activity Monitor, compared to not freezing the string.
pipeline = []
100_000.times { pipeline << 'hello world'.freeze }
However, if I try the same with a hash literal, it uses lots of memory, unless I assign the hash to a variable and freeze it before.
pipeline = []
100_000.times { pipeline << {hello: 'world'}.freeze } # Uses about 25MB
my_hash = {hello: 'world'}
my_hash.freeze
100_000.times { pipeline << my_hash} # This uses about 1MB
Can anyone explain why? I always thought the string case was a bit strange, because it looks like you're simply creating lots of different string objects, freezing each one separately, and adding lots of frozen objects to the array. Don't know why it works, but hey, it did. Now, the hash case is more in line with what I expected, but I don't know why it won't behave like the string.
Upvotes: 3
Views: 5337
Reputation: 211590
It's probably the case that the Ruby optimizer can identify that string as being the same from one loop to the next, but it's unable to identify that hash as being identical so it makes new ones. In the second variant you literally use the same hash so the optimizer can handle it.
For proof, look at this:
pipeline = []
100_000.times { pipeline << 'hello world'.freeze }
pipeline.map(&:object_id).uniq.length
# => 1
That's an array of identical objects, one allocation only.
pipeline = []
100_000.times { pipeline << {hello: 'world'}.freeze }
pipeline.map(&:object_id).uniq.length
# => 100000
That's 100,000 different objects.
Upvotes: 6
Reputation: 369458
Can anyone explain why? I always thought the string case was a bit strange, because it looks like you're simply creating lots of different string objects, freezing each one separately, and adding lots of frozen objects to the array.
The expression form
'string literal'.freeze
is a special expression form that is special-cased by the language. It not only freezes the string object, it also performs de-duplication. (Similar to symbols.)
It is a special-cased expression form. It is not evaluating the string literal and then sending it the message freeze
. Rather, it is treated as a single entity, a different form of string literal if you will.
In fact, the original proposal did introduce a different form of string literal like this:
'string literal'f
The proposal was changed to make it forwards-compatible: 'foo'f
would be a syntax error, if you had to run your code in older versions of Ruby, whereas 'foo'.freeze
just works the same way in older versions of Ruby, it only uses more memory.
Note: this means it only works for literals. Here, the string is de-duplicated:
'foo'.freeze
Here, it is not:
foo = 'foo'
foo.freeze
Don't know why it works, but hey, it did.
Basically, it works, because the language specification says so.
Now, the hash case is more in line with what I expected, but I don't know why it won't behave like the string.
Again, it doesn't work, because the language specification only special-cases string literals.
Upvotes: 3