Issue #16049 has been reported by Dan0042 (Daniel DeLorme).

----------------------------------------
Bug #16049: optimization for frozen dynamic string literals "#{exp}".dup and +"#{exp}"
https://bugs.ruby-lang.org/issues/16049

* Author: Dan0042 (Daniel DeLorme)
* Status: Open
* Priority: Normal
* Assignee: 
* Target version: 
* ruby -v: ruby 2.7.0dev (2019-04-22 trunk 67701) [x86_64-linux]
* Backport: 2.5: UNKNOWN, 2.6: UNKNOWN
----------------------------------------
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 = "#{i}"
  when :freeze then v = "#{i}".freeze
  when :dup    then v = "#{i}".dup
  when :plus   then v = +"#{i}"
  when :minus  then v = -"#{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   dup      plus     minus

'x'   false                  200021   200021   300021   200021   300021
'x'   true                   200021   200021   300021   300021   200021

i     false                  300021   300021   400021   300021   400021
i     true                   300021   300021   400021   400021   300021
```

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 `"#{i}".dup` and `+"#{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. According to the results above it's the same thing; we only have the extra 100k strings created by `i.to_s`. But if I change the script to measure memory 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 following memory usage 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.





-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>