Feature #16049
Updated by Dan0042 (Daniel DeLorme) over 4 years ago
When the decision was made that `frozen_string_literal: true` should also apply to dynamic string literals, it was mitigated with the following explanation: > "#{exp}".dup can be optimized so it won’t allocate extra objects like "...".freeze https://docs.google.com/document/u/1/d/1D0Eo5N7NE_unIySOKG9lVj_eyXf66BQPM4PKp7NvMyQ/pub However that does not appear to be the case currently. Using this script that generates 100k String objects: ```ruby # frozen_string_literal: true def allocated GC.stat[:total_allocated_objects] end GC.disable c = ARGV.shift.to_sym x_eq_i = ARGV.shift=="i" x = "x" before = allocated 100_000.times do |i| x = i.to_s if x_eq_i case c when :normal then v = "#{x}" "#{i}" when :freeze then v = "#{x}".freeze "#{i}".freeze when :minus :dup then v = -"#{x}" "#{i}".dup when :dup :plus then v = "#{x}".dup +"#{i}" when :plus :minus then v = +"#{x}" -"#{i}" else raise end end after = allocated printf "%d\n", after-before ``` I get the following number of objects allocated ``` x= frozen_string_literal normal freeze minus dup plus minus 'x' false 100001 100001 100001 200001 100001 200021 200021 300021 200021 300021 'x' true 100001 100001 100001 200001 200001 200021 200021 300021 300021 200021 i false 200001 200001 299999 300001 200001 300021 300021 400021 300021 400021 i true 200001 200001 200001 300001 300001 300021 300021 400021 400021 300021 ``` We Given that I create 100k strings in that loop, I have no idea why object count increases by 200k. I'm going to hope/assume there is some kind of reason for that. But we can also see that `"#{x}".dup` `"#{i}".dup` and `+"#{x}"` `+"#{i}"` allocate an extra object per iteration We also see that `-"#{i}"` does not have the same optimization as `"#{i}".freeze` ??? I also tested with `x = i.to_s` to see if deduplication of 100k identical strings was different from 100k different strings. In addition According to the expected results above it's the same thing; we only have the extra 100k strings created by `i.to_s`, there's an additional 100k extra strings created for `-"#{i}"` when frozen_string_literal is false??? There may also be a `i.to_s`. But if I change the script to measure memory leak here because while instead of object allocations: ```ruby def allocated kb = `ps -p#{$$} -orss`[/\d+/].to_i kb -= GC.stat[:heap_free_slots]*40/1024 (kb / 1024.0).round end ... 130_000.times do |i| ... ``` I get the number of objects increases by x3, following memory usage increases by x4. in MiB ``` x= frozen_string_literal normal freeze dup plus minus 'x' false 10 10 15 10 20* 'x' true 10 10 15 15 10 i false 15 15 20 15 25* i true 15 15 20 20 15 ``` Which is proportional to the previous numbers, except for those marked with an asterisk. Another mystery to me. Summary: I expected `"#{v}".dup` and `+"#{v}"` to behave the same regardless of frozen_string_literal (and optimize down to just one allocation) I expected `"#{v}".freeze` and `-"#{v}"` to behave the same regardless of frozen_string_literal (and optimize down to just one allocation) but they do not. I think they should. It would be nice.