flavio
flavio

Reputation: 87

Why freezing hash literal is not the same as freezing string literal?

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

Answers (2)

tadman
tadman

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

J&#246;rg W Mittag
J&#246;rg W Mittag

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

Related Questions