Project

General

Profile

Feature #16049

Updated by Dan0042 (Daniel DeLorme) almost 1 year 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. 

Back