New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beyond Ludicrous Speed #21057

Merged
merged 22 commits into from Jul 30, 2015

Conversation

Projects
None yet
@schneems
Member

schneems commented Jul 29, 2015

I've been working on quite a few performance improvements to Rails and the Rails ecosystem, but this is by far the single biggest set of improvements I've been able to make. Those other changes have maybe shaved off a few objects here and there, and maybe 1 or 2% faster request time if I'm extremely lucky.

This change shaves off 34,299 objects and 3,457,318 bytes (3.29 MiB) allocated on every request. This is a 29% decrease in objects and a 23% decrease in memory allocated per request. Taking us past ludicrous speed...

So just how much does this improve overall request time? To measure I used http://www.codetriage.com as an example app and https://github.com/schneems/derailed_benchmarks to generate load. I hit the app with 1,000 requests with my patch and then 1,000 requests against Rails master. I repeated several times until I saw a fairly stable standard deviation.

The average time for 1,000 request against master was 221.88 seconds and against this patch was 195.401 seconds. This gives us a...

11.9 % speed improvement

Where does that speed improvement come from? The big theme is not allocating objects when we don't need to. I used $ derailed exec perf:objects which uses memory_profiler to look for large pockets of allocated memory against http://www.codetriage.com. You can get an in-depth explanation of each change in the commit along with a benchmark of objects and memory saved. The largest area I was able to make gains on was link generation, by removing extraneous arrays, duplicated hashes, and getting rid of intermediate objects whenever possible. I measured this speedup directly:

# $ bin/rails console

require 'benchmark/ips'
repo = Repo.first

Benchmark.ips do |x|
  x.report("url_for speed") {
    app.url_for(repo)
  }
end

This showed 2,865 iterations per second on master and 4,143 iterations per second on my patch. Which is a 44% speed improvement in url generation.

These performance improvements preserve existing interfaces and behaviors, all tests pass.

@rafaelfranca

This comment has been minimized.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Jul 29, 2015

Member

If you want just to run tests you can always push to rails/rails since you have commit access.

Member

rafaelfranca commented Jul 29, 2015

If you want just to run tests you can always push to rails/rails since you have commit access.

schneems added some commits Jul 24, 2015

Decrease string allocations on AR#respond_to?
When a symbol is passed in, we call `to_s` on it which allocates a string. The two hardcoded symbols that are used internally are `:to_partial_path` and `:to_model`.

This change buys us 71,136 bytes of memory and 1,777 fewer objects per request.
Decrease string allocations in apply_inflections
In `apply_inflections` a string is down cased and some whitespace stripped in the front (which allocate strings). This would normally be fine, however `uncountables` is a fairly small array (10 elements out of the box) and this method gets called a TON. Instead we can keep an array of valid regexes for each uncountable so we don't have to allocate new strings.

This change buys us 325,106 bytes of memory and 3,251 fewer objects per request.
Decrease string allocations in url_options
The request.script_name is dup-d which allocates an extra string. It is most commonly an empty string "". We can save a ton of string allocations by checking first if the string is empty, if so we can use a frozen empty string instead of duplicating an empty string.

This change buys us 35,714 bytes of memory and 893 fewer objects per request.
Speed up journey extract_parameterized_parts
Micro optimization: `reverse.drop_while` is slower than `reverse_each.drop_while`. This doesn't save any object allocations.

Second, `keys_to_keep` is typically a very small array. The operation `parameterized_parts.keys - keys_to_keep` actually allocates two arrays. It is quicker (I benchmarked) to iterate over each and check inclusion in array manually.

This change buys us 1774 fewer objects per request
Speed up journey missing_keys
Most routes have a `route.path.requirements[key]` of `/[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/` yet every time this method is called a new regex is generated on the fly with `/\A#{DEFAULT_INPUT}\Z/`. OBJECT ALLOCATIONS BLERG!

This change uses a special module that implements `===` so it can be used in a case statement to pull out the default input. When this happens, we use a pre-generated regex.

This change buys us 1,643,465 bytes of memory and 7,990 fewer objects per request.
Decrease route_set allocations
In handle_positional_args `Array#-=` is used which allocates a new array. Instead we can iterate through and delete elements, modifying the array in place.

Also `Array#take` allocates a new array. We can build the same by iterating over the other element.

This change buys us 106,470 bytes of memory and 2,663 fewer objects per request.
Reduce hash allocations in route_set
When generating a url with `url_for` the hash of arguments passed in, is dup-d and merged a TON. I wish I could clean this up better, and might be able to do it in the future. This change removes one dup, since it's literally right after we just dup-d the hash to pass into this constructor.

This may be a breaking, change but the tests pass...so :shipit: we can revert if it causes problems

This change buys us 205,933 bytes of memory and 887 fewer objects per request.
Decrease string allocation in content_tag_string
When an unknonwn key is passed to the hash in `PRE_CONTENT_STRINGS` it returns nil, when you call "#{nil}" it allocates a new empty string. We can get around this allocation by using a default value `Hash.new { "".freeze }`. We can avoid the `to_sym` call by pre-populating the hash with a symbol key in addition to a string key.

We can freeze some strings when using Array#* to reduce allocations.

Array#join can take frozen strings.

This change buys us 86,600 bytes of memory and 1,857 fewer objects per request.
Optimize hash key
No idea why on earth this hash key isn't already optimized by MRI, but it isn't. 💩

This change buys us 74,077 bytes of memory and 1,852 fewer objects per request.
Cut string ActionView template allocations
The instrument method creates new strings, the most common action to instrument is "!render_template` so we can detect when that action is occurring and use a frozen string instead.

This change buys us 113,714 bytes of memory and 1,790 fewer objects per request.
Cut string allocations in content_tag_string
content_tag's first argument is will generate a string with an html tag so `:a` will generate: `<a></a>`. When this happens, the symbol is implicitly `to_s`-d so a new string is allocated. We can get around that by using a frozen string instead which

This change buys us 74,236 bytes of memory and 1,855 fewer objects per request.

@schneems schneems changed the title from Running tests to Beyond Ludicrous Speed Jul 30, 2015

@schneems

This comment has been minimized.

Show comment
Hide comment
@schneems

schneems Jul 30, 2015

Member

Build is green, for some reason it's not showing up on the PR https://travis-ci.org/rails/rails/builds/73305474

Member

schneems commented Jul 30, 2015

Build is green, for some reason it's not showing up on the PR https://travis-ci.org/rails/rails/builds/73305474

@jeremy

This comment has been minimized.

Show comment
Hide comment
@jeremy

jeremy Jul 30, 2015

Member

Nice work @schneems !

Member

jeremy commented Jul 30, 2015

Nice work @schneems !

@vipulnsward

This comment has been minimized.

Show comment
Hide comment
@vipulnsward
Member

vipulnsward commented Jul 30, 2015

🏇

@arthurnn

This comment has been minimized.

Show comment
Hide comment
@arthurnn

arthurnn Jul 30, 2015

Member

Squash! =)

Member

arthurnn commented Jul 30, 2015

Squash! =)

@rafaelfranca

This comment has been minimized.

Show comment
Hide comment
@rafaelfranca
Member

rafaelfranca commented Jul 30, 2015

:shipit:

@rafaelfranca

This comment has been minimized.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Jul 30, 2015

Member

Really nice work!

Member

rafaelfranca commented Jul 30, 2015

Really nice work!

@arthurnn

This comment has been minimized.

Show comment
Hide comment
@arthurnn

arthurnn Jul 30, 2015

Member

❤️ ❤️ ❤️ ❤️ ❤️

Member

arthurnn commented Jul 30, 2015

❤️ ❤️ ❤️ ❤️ ❤️

@pixeltrix

This comment has been minimized.

Show comment
Hide comment
@pixeltrix

pixeltrix Jul 30, 2015

Member

@schneems well done 👏

Member

pixeltrix commented Jul 30, 2015

@schneems well done 👏

if tests.key?(key)
missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key]
case tests[key]
when nil

This comment has been minimized.

@jeremy

jeremy Jul 30, 2015

Member

Changes a presence check to a nil check—can this value legitimately be nil?

@jeremy

jeremy Jul 30, 2015

Member

Changes a presence check to a nil check—can this value legitimately be nil?

This comment has been minimized.

@schneems

schneems Jul 30, 2015

Member

I don't think you can have a required key be nil. I ran this against the test suite and came up with nothing:

          puts tests.inspect if tests.values.include?(nil)
@schneems

schneems Jul 30, 2015

Member

I don't think you can have a required key be nil. I ran this against the test suite and came up with nothing:

          puts tests.inspect if tests.values.include?(nil)
@@ -617,7 +621,7 @@ def current_controller
def use_recall_for(key)
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
if !named_route_exists? || segment_keys.include?(key)
@options[key] = @recall.delete(key)
@options[key] = @recall[key]

This comment has been minimized.

@jeremy

jeremy Jul 30, 2015

Member

Red flag, kind of thing that may cause subtle/untested regressions.

@jeremy

jeremy Jul 30, 2015

Member

Red flag, kind of thing that may cause subtle/untested regressions.

This comment has been minimized.

@schneems

schneems Jul 30, 2015

Member

I agree, it raised some red flags with me when I first did it. Here's the specific commit:

schneems@9060b92

It's a fairly large savings in memory. Here's why I think it's okay

This method moves a key/value pair from recall to options

def use_recall_for(key)
  if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
    if !named_route_exists? || segment_keys.include?(key)
      @options[key] = @recall[key]
    end
  end
end

I changed it so that recall is preserved i.e. the value is copied not "moved." This method gets called 3 times for the keys :controller, :action, and :id:

# This pulls :controller, :action, and :id out of the recall.
# The recall key is only used if there is no key in the options
# or if the key in the options is identical. If any of
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
  use_recall_for(:controller) or return
  use_recall_for(:action) or return
  use_recall_for(:id)
end

Based on the comment every time options would have a key, it should be favored over recall, so it doesn't matter that recall also has the key. But let's not trust comments, let's look at the code. After we are done in this class the formatter is called:

@set.formatter.generate(named_route, options, recall, PARAMETERIZE)

Here the first thing that is done is to merge the two hashes:

def generate(name, options, path_parameters, parameterize = nil)
  constraints = path_parameters.merge(options)

So whether we delete the key in recall (which becomes path_parameters) the constraints hash will be the same.

You might be thinking "well i bet path_parameters is uesed somewhere else" and you would be right

It's used here:

parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)

Yet once again, it's immediately merged:

def extract_parameterized_parts(route, options, recall, parameterize = nil)
  parameterized_parts = recall.merge(options)

In both cases we will use the key in options if it exists. I'm pretty comfortable with this change, but I would like some more eyes. Maybe I missed a really subtle and untested use-case, for which we should certainly add some tests.

@schneems

schneems Jul 30, 2015

Member

I agree, it raised some red flags with me when I first did it. Here's the specific commit:

schneems@9060b92

It's a fairly large savings in memory. Here's why I think it's okay

This method moves a key/value pair from recall to options

def use_recall_for(key)
  if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
    if !named_route_exists? || segment_keys.include?(key)
      @options[key] = @recall[key]
    end
  end
end

I changed it so that recall is preserved i.e. the value is copied not "moved." This method gets called 3 times for the keys :controller, :action, and :id:

# This pulls :controller, :action, and :id out of the recall.
# The recall key is only used if there is no key in the options
# or if the key in the options is identical. If any of
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
  use_recall_for(:controller) or return
  use_recall_for(:action) or return
  use_recall_for(:id)
end

Based on the comment every time options would have a key, it should be favored over recall, so it doesn't matter that recall also has the key. But let's not trust comments, let's look at the code. After we are done in this class the formatter is called:

@set.formatter.generate(named_route, options, recall, PARAMETERIZE)

Here the first thing that is done is to merge the two hashes:

def generate(name, options, path_parameters, parameterize = nil)
  constraints = path_parameters.merge(options)

So whether we delete the key in recall (which becomes path_parameters) the constraints hash will be the same.

You might be thinking "well i bet path_parameters is uesed somewhere else" and you would be right

It's used here:

parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)

Yet once again, it's immediately merged:

def extract_parameterized_parts(route, options, recall, parameterize = nil)
  parameterized_parts = recall.merge(options)

In both cases we will use the key in options if it exists. I'm pretty comfortable with this change, but I would like some more eyes. Maybe I missed a really subtle and untested use-case, for which we should certainly add some tests.

This comment has been minimized.

@jeremy

jeremy Jul 30, 2015

Member

@tenderlove stubbed some toes on the same thing—ring a bell?

@jeremy

jeremy Jul 30, 2015

Member

@tenderlove stubbed some toes on the same thing—ring a bell?

This comment has been minimized.

@schneems

schneems Jul 30, 2015

Member

Via campfire AP says he's OK with it. If any weirdness comes up on master about URL generation for the next while i'll be happy to take a look.

@schneems

schneems Jul 30, 2015

Member

Via campfire AP says he's OK with it. If any weirdness comes up on master about URL generation for the next while i'll be happy to take a look.

This comment has been minimized.

@mfazekas

mfazekas Apr 6, 2016

Contributor

See #24438

what was overlooked is a few mutations to @recall

@recall[:action] ||= 'index'

@recall[:action] = @options.delete(:action)

this is now going to mutate the recall passed in by callers.

@mfazekas

mfazekas Apr 6, 2016

Contributor

See #24438

what was overlooked is a few mutations to @recall

@recall[:action] ||= 'index'

@recall[:action] = @options.delete(:action)

this is now going to mutate the recall passed in by callers.

schneems added some commits Jul 25, 2015

Avoid calling to_s on nil in journey/formatter
When `defaults[key]` in `generate` in the journey formatter is called, it often returns a `nil` when we call `to_s` on a nil, it allocates an empty string. We can skip this check when the default value is nil.

This change buys us 35,431 bytes of memory and 887 fewer objects per request.

Thanks to @matthewd for help with the readability
Freeze a string in comparator
Saves 888 string objects per request.
Only allocate new string when needed
Instead of calling `sub` on every link_to call for controller, we can detect when the string __needs__ to be allocated and only then create a new string (without the leading slash), otherwise, use the string that is given to us.

Saves 888 string objects per request, 35,524 bytes.
Avoid hash duplication by skipping mutation
If we don't mutate the `recall` hash, then there's no reason to duplicate it. While this change doesn't get rid of that many objects, each hash object it gets rid of was massive.

Saves 888 string objects per request, 206,013 bytes (thats 0.2 mb which is kinda a lot).
@toreriklinnerud

This comment has been minimized.

Show comment
Hide comment
@toreriklinnerud

toreriklinnerud Aug 6, 2015

Thanks for this - great work that will benefit everyone using Rails! ❤️

toreriklinnerud commented Aug 6, 2015

Thanks for this - great work that will benefit everyone using Rails! ❤️

@davekapp

This comment has been minimized.

Show comment
Hide comment
@davekapp

davekapp Aug 6, 2015

Amazing work. Congrats to you and everyone else and I owe you a 🍺 sometime. :)

davekapp commented Aug 6, 2015

Amazing work. Congrats to you and everyone else and I owe you a 🍺 sometime. :)

end
# Move 'index' action from options to recall
def normalize_action!
if @options[:action] == 'index'
if @options[:action] == 'index'.freeze

This comment has been minimized.

@Johnius

Johnius Aug 6, 2015

Maybe it's a stupid question. But why do you freeze strings?

@Johnius

Johnius Aug 6, 2015

Maybe it's a stupid question. But why do you freeze strings?

This comment has been minimized.

@schneems

schneems Aug 6, 2015

Member

This explains the freeze method for strings 
http://tmm1.net/ruby21-fstrings/

This article explains why you might want I to use frozen strings for performance with some benchmarks http://www.sitepoint.com/unraveling-string-key-performance-ruby-2-2/

@schneems

schneems Aug 6, 2015

Member

This explains the freeze method for strings 
http://tmm1.net/ruby21-fstrings/

This article explains why you might want I to use frozen strings for performance with some benchmarks http://www.sitepoint.com/unraveling-string-key-performance-ruby-2-2/

This comment has been minimized.

@dmitry

dmitry Aug 7, 2015

Contributor

Isn't it better to have a constant defined in a class/module?

@dmitry

dmitry Aug 7, 2015

Contributor

Isn't it better to have a constant defined in a class/module?

This comment has been minimized.

@schneems
@schneems
@radar

This comment has been minimized.

Show comment
Hide comment
@radar

radar Aug 7, 2015

Contributor

Excellent work @schneems! 🎉 🍻

Contributor

radar commented Aug 7, 2015

Excellent work @schneems! 🎉 🍻

@jjgh

This comment has been minimized.

Show comment
Hide comment
@jjgh

jjgh Aug 7, 2015

@schneems thanks for making Rails so much better, one more time! 👏 👏 👏

jjgh commented Aug 7, 2015

@schneems thanks for making Rails so much better, one more time! 👏 👏 👏

@@ -164,7 +164,7 @@ def deconstantize
#
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
def parameterize(sep = '-')
def parameterize(sep = '-'.freeze)

This comment has been minimized.

@kgrz

kgrz Aug 7, 2015

Do you think it's better to have a single place that has all the frozen strings? Sort of like boot.rb, but that will have the calls to freeze for these tokens (- here, :: in the next file etc.

@kgrz

kgrz Aug 7, 2015

Do you think it's better to have a single place that has all the frozen strings? Sort of like boot.rb, but that will have the calls to freeze for these tokens (- here, :: in the next file etc.

This comment has been minimized.

@bquorning

bquorning Aug 7, 2015

Contributor

You’d still have to call freeze when you want to use the frozen string, so there is no point in freezing them up front.

>> '-'.freeze.object_id
=> 70222534974420
>> '-'.object_id
=> 70222535341760
>> '-'.freeze.object_id
=> 70222534974420
>> '-'.object_id
=> 70222535533680
@bquorning

bquorning Aug 7, 2015

Contributor

You’d still have to call freeze when you want to use the frozen string, so there is no point in freezing them up front.

>> '-'.freeze.object_id
=> 70222534974420
>> '-'.object_id
=> 70222535341760
>> '-'.freeze.object_id
=> 70222534974420
>> '-'.object_id
=> 70222535533680

This comment has been minimized.

@kgrz

kgrz Aug 7, 2015

Ah, TIL, thank you!

@kgrz

kgrz Aug 7, 2015

Ah, TIL, thank you!

This comment has been minimized.

@kgrz

kgrz Aug 7, 2015

@bquorning That said, I remember a way I did that in a different project. That was by using constants in cases like these. For example, def parameterize(sep = DASH_TOKEN). That way, the object_ids will be same no matter where this token gets used, IIRC.

@kgrz

kgrz Aug 7, 2015

@bquorning That said, I remember a way I did that in a different project. That was by using constants in cases like these. For example, def parameterize(sep = DASH_TOKEN). That way, the object_ids will be same no matter where this token gets used, IIRC.

This comment has been minimized.

@bquorning

bquorning Aug 7, 2015

Contributor

True, if you assign the frozen strings to constants up front, you can re-use the same instance all over the place. Plus, they won't be GC'ed.

@bquorning

bquorning Aug 7, 2015

Contributor

True, if you assign the frozen strings to constants up front, you can re-use the same instance all over the place. Plus, they won't be GC'ed.

This comment has been minimized.

@bquorning

bquorning Aug 7, 2015

Contributor

To quote @schneems from rack/rack#737:

“While we could certainly go overboard and pre-define ALL strings as constants, that would be pretty gnarly to work with. This patch goes after the largest of the low hanging fruit.”

@bquorning

bquorning Aug 7, 2015

Contributor

To quote @schneems from rack/rack#737:

“While we could certainly go overboard and pre-define ALL strings as constants, that would be pretty gnarly to work with. This patch goes after the largest of the low hanging fruit.”

This comment has been minimized.

@kgrz

kgrz Aug 7, 2015

Yes. More cleaner, IMO. That was what I wanted to convey when I first posted the comment. Evidently, I wasn't clear. Sorry about that.

@kgrz

kgrz Aug 7, 2015

Yes. More cleaner, IMO. That was what I wanted to convey when I first posted the comment. Evidently, I wasn't clear. Sorry about that.

This comment has been minimized.

@kgrz

kgrz Aug 7, 2015

Let me clarify my first comment again:

I meant that it might be nice to have these tokens like -, ::, '' etc to, say, DASH, DOUBLE_COLON, EMPTY_STRING etc defined in a separate file that's loaded upfront and those constants used everywhere instead of multiple "-".freeze calls all over the code.

@kgrz

kgrz Aug 7, 2015

Let me clarify my first comment again:

I meant that it might be nice to have these tokens like -, ::, '' etc to, say, DASH, DOUBLE_COLON, EMPTY_STRING etc defined in a separate file that's loaded upfront and those constants used everywhere instead of multiple "-".freeze calls all over the code.

This comment has been minimized.

@schneems

schneems Aug 7, 2015

Member

Specifying frozen strings as constants is slower. Constants work internally as a global hash, every time you call a constant Ruby has to do a hash lookup. If you use String#freeze inline, you don't have to do the lookup and your code is faster. Also ''.freeze is shorter than EMPTY_STRING.

require 'benchmark/ips'

HELLO = "hello".freeze
Benchmark.ips do |x|
  x.report("freeze")   { "hello".freeze + "world" }
  x.report("constant") { HELLO + "world" }
end
Calculating -------------------------------------
              freeze   100.611k i/100ms
            constant    99.036k i/100ms
-------------------------------------------------
              freeze      3.630M (± 7.9%) i/s -     18.110M
            constant      3.470M (±11.6%) i/s -     17.034M
@schneems

schneems Aug 7, 2015

Member

Specifying frozen strings as constants is slower. Constants work internally as a global hash, every time you call a constant Ruby has to do a hash lookup. If you use String#freeze inline, you don't have to do the lookup and your code is faster. Also ''.freeze is shorter than EMPTY_STRING.

require 'benchmark/ips'

HELLO = "hello".freeze
Benchmark.ips do |x|
  x.report("freeze")   { "hello".freeze + "world" }
  x.report("constant") { HELLO + "world" }
end
Calculating -------------------------------------
              freeze   100.611k i/100ms
            constant    99.036k i/100ms
-------------------------------------------------
              freeze      3.630M (± 7.9%) i/s -     18.110M
            constant      3.470M (±11.6%) i/s -     17.034M
@nitinstp23

This comment has been minimized.

Show comment
Hide comment
@nitinstp23

nitinstp23 commented Aug 7, 2015

Nice work @schneems 🍻

@arteezy

This comment has been minimized.

Show comment
Hide comment
@arteezy

arteezy commented Aug 7, 2015

@schneems bravo 👏

@marcgg

This comment has been minimized.

Show comment
Hide comment
@marcgg

marcgg Aug 7, 2015

Contributor

👍 👍 👍 Thanks a lot for this

Contributor

marcgg commented Aug 7, 2015

👍 👍 👍 Thanks a lot for this

@arun057

This comment has been minimized.

Show comment
Hide comment
@arun057

arun057 commented Aug 7, 2015

Nice work @schneems

@robinw777

This comment has been minimized.

Show comment
Hide comment
@robinw777

robinw777 Aug 9, 2015

Sorry for a newbie question - I don't understand why "#{action}.action_view".freeze is not applicable to the other branch condition, and why this can save object allocation. Could you explain? Thanks!

Is it because to evaluate "#{action}.action_view", a new string will always be created?

Sorry for a newbie question - I don't understand why "#{action}.action_view".freeze is not applicable to the other branch condition, and why this can save object allocation. Could you explain? Thanks!

Is it because to evaluate "#{action}.action_view", a new string will always be created?

This comment has been minimized.

Show comment
Hide comment
@schneems

schneems Aug 9, 2015

Owner

Try out some benchmarks

require 'benchmark/ips'

action = "!render_template"
Benchmark.ips do |x|
  x.report("frozen dynamic") { "#{action}.action_view".freeze }
  x.report("frozen static")  { "!render_template.action_view".freeze }
  x.report("not frozen")     { "#{action}.action_view" }
end
Calculating -------------------------------------
      frozen dynamic    88.653k i/100ms
       frozen static   138.259k i/100ms
          not frozen    92.168k i/100ms
-------------------------------------------------
      frozen dynamic      2.085M (±10.0%) i/s -     10.372M
       frozen static     10.060M (±12.5%) i/s -     49.358M
          not frozen      2.242M (± 9.3%) i/s -     11.152M

The freeze method does 2 things, if called on a string literal "hello".freeze then it will only ever allocate one string, it also sets a flag on the string letting it know it cannot be modified. If we create a dynamic string "#{action}.action_view".freeze It must allocate a new string. We are joining two strings that may have never been joined together before and it will create a totally new string. Once that string is created, we flip the flag letting everyone know it cannot be modified. This actually takes longer than not calling freeze on the dynamic string since we have to perform the extra operation. So yes, your statement is correct #{action}.action_view" will always create a new string

Owner

schneems replied Aug 9, 2015

Try out some benchmarks

require 'benchmark/ips'

action = "!render_template"
Benchmark.ips do |x|
  x.report("frozen dynamic") { "#{action}.action_view".freeze }
  x.report("frozen static")  { "!render_template.action_view".freeze }
  x.report("not frozen")     { "#{action}.action_view" }
end
Calculating -------------------------------------
      frozen dynamic    88.653k i/100ms
       frozen static   138.259k i/100ms
          not frozen    92.168k i/100ms
-------------------------------------------------
      frozen dynamic      2.085M (±10.0%) i/s -     10.372M
       frozen static     10.060M (±12.5%) i/s -     49.358M
          not frozen      2.242M (± 9.3%) i/s -     11.152M

The freeze method does 2 things, if called on a string literal "hello".freeze then it will only ever allocate one string, it also sets a flag on the string letting it know it cannot be modified. If we create a dynamic string "#{action}.action_view".freeze It must allocate a new string. We are joining two strings that may have never been joined together before and it will create a totally new string. Once that string is created, we flip the flag letting everyone know it cannot be modified. This actually takes longer than not calling freeze on the dynamic string since we have to perform the extra operation. So yes, your statement is correct #{action}.action_view" will always create a new string

@coderxin

This comment has been minimized.

Show comment
Hide comment
@coderxin

coderxin Aug 10, 2015

@schneems nice work! 👏

coderxin commented Aug 10, 2015

@schneems nice work! 👏

@kennym

This comment has been minimized.

Show comment
Hide comment
@kennym

kennym Aug 10, 2015

Contributor

@schneems this is awesome :-) 👍

Contributor

kennym commented Aug 10, 2015

@schneems this is awesome :-) 👍

@dmitry

This comment has been minimized.

Show comment
Hide comment
@dmitry

dmitry Aug 10, 2015

Contributor

Still a question, why not to use constants, instead of forzen? Because of the readability?

Contributor

dmitry commented Aug 10, 2015

Still a question, why not to use constants, instead of forzen? Because of the readability?

@matthewd

This comment has been minimized.

Show comment
Hide comment
@matthewd

matthewd Aug 10, 2015

Member

@dmitry there is no reason to use constants. This is how you spell an immutable string literal in ruby; we want immutable string literals, so that's what we're doing.

(Also, @schneems has already pointed out that constants may be slower.)

Member

matthewd commented Aug 10, 2015

@dmitry there is no reason to use constants. This is how you spell an immutable string literal in ruby; we want immutable string literals, so that's what we're doing.

(Also, @schneems has already pointed out that constants may be slower.)

@dmitry

This comment has been minimized.

Show comment
Hide comment
@dmitry

dmitry Aug 10, 2015

Contributor

@matthewd thanks for pointing out.

Interestingly on my computer (with 2.1.5 and 2.2.2 rubies) this benchmark produces almost the same results for constant/freeze strings. But readability of freeze is better, except the times when it's repeating many times in a code, and you would like to inspect them with IDE or grep.

Contributor

dmitry commented Aug 10, 2015

@matthewd thanks for pointing out.

Interestingly on my computer (with 2.1.5 and 2.2.2 rubies) this benchmark produces almost the same results for constant/freeze strings. But readability of freeze is better, except the times when it's repeating many times in a code, and you would like to inspect them with IDE or grep.

@garysweaver

This comment has been minimized.

Show comment
Hide comment
@garysweaver

garysweaver Aug 10, 2015

Contributor

👍 👍 Nice work!

Contributor

garysweaver commented Aug 10, 2015

👍 👍 Nice work!

@@ -75,13 +75,21 @@ def parameterize(string, sep = '-')
# Turn unwanted chars into the separator
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep)
unless sep.nil? || sep.empty?
re_sep = Regexp.escape(sep)
if sep == "-".freeze

This comment has been minimized.

@heliocola

heliocola Aug 24, 2015

@schneems the "sep" variable in the definition of the parameterize function is not using the .freeze when sep is not present in the call. Will this if catch or miss the case when somebody call parameterize("string")?
If the comparison takes in consideration the string (and not the object id), this probably won't be a problem... Does it make sense?
I noticed you used the sep = "-".freeze in the parameterize function on file 'activesupport/lib/active_support/core_ext/string/inflections.rb'

@heliocola

heliocola Aug 24, 2015

@schneems the "sep" variable in the definition of the parameterize function is not using the .freeze when sep is not present in the call. Will this if catch or miss the case when somebody call parameterize("string")?
If the comparison takes in consideration the string (and not the object id), this probably won't be a problem... Does it make sense?
I noticed you used the sep = "-".freeze in the parameterize function on file 'activesupport/lib/active_support/core_ext/string/inflections.rb'

This comment has been minimized.

@schneems

schneems Aug 24, 2015

Member

Not sure what is wrong. Are you concerned that "-".freeze does not == "-" ? It does

puts "-".freeze == "-"
true
puts "-" == "-".freeze
true
@schneems

schneems Aug 24, 2015

Member

Not sure what is wrong. Are you concerned that "-".freeze does not == "-" ? It does

puts "-".freeze == "-"
true
puts "-" == "-".freeze
true

This comment has been minimized.

@heliocola

heliocola Aug 24, 2015

The comparison was the main thing I thought about.
If you call it without the second parameter it will have a different string "-" created but the comparison will work.

@heliocola

heliocola Aug 24, 2015

The comparison was the main thing I thought about.
If you call it without the second parameter it will have a different string "-" created but the comparison will work.

@alejandrodevs

This comment has been minimized.

Show comment
Hide comment
@alejandrodevs

alejandrodevs commented Oct 1, 2015

@schneems Nice work!

@lucascaton

This comment has been minimized.

Show comment
Hide comment
@lucascaton

lucascaton Dec 14, 2015

Contributor

@schneems Nice one! ツ

Contributor

lucascaton commented Dec 14, 2015

@schneems Nice one! ツ

@RKushnir

This comment has been minimized.

Show comment
Hide comment
@RKushnir

RKushnir Dec 31, 2015

Why don't you use Hash#each_key if you only need keys?

Why don't you use Hash#each_key if you only need keys?

@swapab

This comment has been minimized.

Show comment
Hide comment
@swapab

swapab Feb 16, 2016

Is this PR available in rails 3.2.22 or 3.2.22.1 ?

swapab commented Feb 16, 2016

Is this PR available in rails 3.2.22 or 3.2.22.1 ?

@dmitry

This comment has been minimized.

Show comment
Hide comment
@dmitry

dmitry Feb 16, 2016

Contributor

@swapnilabnave no, it's only available for v5.0.0.beta2 v5.0.0.beta1.1 v5.0.0.beta1. You can see that by checking merge commit: 5373bf2

Contributor

dmitry commented Feb 16, 2016

@swapnilabnave no, it's only available for v5.0.0.beta2 v5.0.0.beta1.1 v5.0.0.beta1. You can see that by checking merge commit: 5373bf2

@swapab

This comment has been minimized.

Show comment
Hide comment
@swapab

swapab commented Feb 16, 2016

@dmitry Thanks!

@Chizuru-Maxienne

This comment has been minimized.

Show comment
Hide comment
@Chizuru-Maxienne

Chizuru-Maxienne Feb 16, 2016

Keep up the good work! @schneems 👏 👏 👍 🍰

Chizuru-Maxienne commented Feb 16, 2016

Keep up the good work! @schneems 👏 👏 👍 🍰

@tiagoovieira

This comment has been minimized.

Show comment
Hide comment
@tiagoovieira

tiagoovieira commented Feb 22, 2016

🔝

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment