Exploring Ruby's # frozen_string_literal

Exploring Ruby's # frozen_string_literal

As of August 2024, Ruby 3.4 has not been released yet, although the Ruby Core team is working on it and continues to make progress on enhancements and refinements so our ruby apps can have the best performance. In this blog post we will talk about an interesting discussion happening about the # frozen_string_literal comment at the top of Ruby files.

Magic performant comment

The magic comment # frozen_string_literal pragma was introduced in Ruby 2.3. Mike Perham opens a new window documented it and included benchmarks which showed performance improvements in Ruby code when using the comment to freeze strings.

frozen_string_literal reduces the generated garbage by ~100MB or ~20%! Free performance by adding a one line comment.

Once the comment # frozen_string_literal: true has a true value set, it specifies that all string literals in the file will be frozen. Basically, when a string is frozen in Ruby, it means that its contents cannot be modified.

Testing it with Ruby 2.7.8:

In a micro scenario, we can benchmark and confirm that there is some performance improvement when we add this comment and set it to true in a Ruby file.

require 'benchmark'

time_without_frozen = Benchmark.realtime do
  1_000_000.times do
    str = "Hello, World!"
    str += " Adding some text."
  end
end

The time to run this benchmark without frozen_string_literal is 0.12221599998883903 seconds

# frozen_string_literal: true
require 'benchmark'

time_with_frozen = Benchmark.realtime do
  1_000_000.times do
    str = "Hello, World!"
    str += " Adding some text."
  end
end

The time to run this benchmark with frozen_string_literal is 0.0953260000096634 seconds.

The magic comment had an impact of approximately 22% in run time in this case of a simple few characters string interpolation.

Performance in Ruby 3.4-dev:

Testing the following benchmark in the Ruby 3.4-dev build on May 27th.

require 'benchmark'

n = 1_000_000

Benchmark.bmbm do |x|
  x.report("with frozen_string_literal: true") { n.times do; str = "Hello, World!"; str += " Adding some text"; end }
end

We have the following results:

                                          user     system      total        real
without frozen_string_literal: true   0.103639   0.000471   0.104110 (  0.106120)
                                       user     system      total        real
with frozen_string_literal: true   0.068708   0.000207   0.068915 (  0.069655)

The benchmark still shows a better result using # frozen_string_literal: true in Ruby 3.4, 0.036465s faster using the magic comment, approximately 34.37% of performance gain in this case.

Rubocop usage

Rubocop opens a new window is a very popular library used in Ruby projects. It brings a cop, Style/FrozenStringLiteralComment, that helps transitioning from mutable string literals to frozen string literals.

It will add the # frozen_string_literal: true magic comment to the top of files to enable frozen string literals. Frozen string literals may be default in future Ruby…The frozen string literal comment is only valid in Ruby 2.3+.

Note that the cop will accept files where the comment exists but is set to false instead of true.

StandardRB cop

There were already some discussions about having the Style/FrozenStringLiteralComment cop always on, but the decision was never made.

In this Pull Request opens a new window , there was a desire to bring this cop to life but it was never merged. StandardRB’s philosophy is that

“rules are not configurable and that every project styled by standard should be very nearly the same”

according to their contributor’s comment.

Behavior in Ruby 3.4?

There was a plan to make it the default behavior for Ruby 3.0. However, this plan was abandoned due to concerns about it being too disruptive as a breaking change without sufficient prior notice. Matz couldn’t agree to that due to HUGE compatibility issues opens a new window this might cause.

At this time, Matz expressed opens a new window a desire to enable # frozen_string_literal by default in the future. However, any such change would require a careful migration plan due to backward compatibility concerns. Simply flipping the switch could break a significant amount of existing code or lead to significant performance loss.

The typical approach for such changes calls for a deprecation period, during which warnings are emitted in advance of the change.

What changes in Ruby 3.4:

  • Files with # frozen_string_literal: true or # frozen_string_literal: false don’t change in behavior at all.
  • Files with no # frozen_string_literal comment are compiled to use the putchilledstring opcode instead of the regular putstring.
  • This opcode marks the string with a user flag and when these strings are mutated a warning is issued.

So you don’t need to run and delete the magic comment once you upgrade to Ruby 3.4, this Deprecation Warning will only be shown for files with no comment that are using putchilledstring on compilation.

Releases are planned in 3 steps and final step could be Ruby 4.0.

  • Release R0: introduce the deprecation warning (only if deprecation warnings enabled).
  • Release R1: make the deprecation warning show up regardless of verbosity level.
  • Release R2: make string literals frozen by default.

Conclusion

The # frozen_string_literal magic comment is a very simple and good one-line of code that can help improve performance by making string literals immutable. Not every project needs it, and it can raise discussions about Ruby’s garbage collector or if the benchmark done to prove its concept is valid.

This is a simple change, but not strictly required for you to upgrade to Ruby 3.4 once it’s released. Making it default in Ruby 3.4 or newer versions, however, could bring interesting performance improvements and enhance the language.

Are you running EOL Ruby in production? We can help you get up to date! Contact us! opens a new window

Get the book