Skip to content

Missed optimizations - #742

Merged
spastorino merged 1 commit into
rack:masterfrom
schneems:schneems/freeze-string
Feb 2, 2015
Merged

Missed optimizations#742
spastorino merged 1 commit into
rack:masterfrom
schneems:schneems/freeze-string

Conversation

@schneems

@schneems schneems commented Oct 3, 2014

Copy link
Copy Markdown
Contributor
  • freezing constant string to ensure it's not mutated
  • use constant where available
  • optimize respond_to? to not allocate array.

Discussed in #737 and #739

respond_to? takes two arguments all recent rubies:

Also method_missing will return a symbol from the first argument, we don't need to allocate a string:

cc/ @raggi

Comment thread lib/rack/body_proxy.rb Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the code provided by @raggi #739 (comment) better?.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will do less work in the to_ary case, provided a symbol is supplied. A constant string of "to_ary" would be faster than the regex though. it's a mix of optimizations and lazy at this point.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like using case with a string constant, updated the code.

@schneems
schneems force-pushed the schneems/freeze-string branch from 751b8d5 to 6c90af7 Compare October 6, 2014 14:37
@schneems

schneems commented Oct 6, 2014

Copy link
Copy Markdown
Contributor Author

Also noticed that the keys in Rack::Mock are not using the proper case in Content-Length https://github.com/rack/rack/blame/master/lib/rack/mock.rb#L116 I couldn't figure out if this was on purpose or not. Also it looks like similar perf optimizations were reverted 5 years ago: 71030b9#commitcomment-8050486

@schneems

schneems commented Oct 6, 2014

Copy link
Copy Markdown
Contributor Author

Build is now passing

@schneems
schneems force-pushed the schneems/freeze-string branch from 6c90af7 to 5f808aa Compare October 6, 2014 19:02
@schneems

schneems commented Oct 6, 2014

Copy link
Copy Markdown
Contributor Author

Updated to use a string in the case statement instead of a constant based on: 68e086d#commitcomment-8050564

Comment thread lib/rack/body_proxy.rb

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simpler to write as

case method_name
when :to_ary, 'to_ary'
  false
else
  super or @body.respond_to?(method_name, include_all)
end

Also, if this isn't super hotspot, would be easier to write if method_name.to_s == 'to_ary'. If this is a super hotspot, can also extract TO_ARY = 'to_ary'.freeze, to avoid extra object allocation on each request.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TO_ARY = 'to_ary'.freeze

Try this, in a case statement with and without. Ruby does this optimization for you, so it will actually be slower to use a constant. Read this thread https://groups.google.com/forum/#!topic/rack-devel/SKAE_yqN8-0

if method_name.to_s == 'to_ary'

This is slow, the reason is because of the to_s will allocate another string. The point of this PR in the first place was to get rid of that to_s

I appreciate you looking at this PR. Performance PRs take an extra long amount of time to put together due to all the benchmark running for proof. If you're suggesting a change for a performance reason, (even if it's to someone else's PR) please go the extra mile and benchmark your suggestion first. Here's all my benchmarks https://github.com/schneems/derailed_benchmarks this is at the application level. Most changes such as the one you suggested can be proved or disproved my making your own benchmark scripts in around 20 lines or less using benchmark/ips. Something to this: rails/rails#17250 .

@schneems

Copy link
Copy Markdown
Contributor Author

Hey @rkh @raggi merge or close please

@spastorino

Copy link
Copy Markdown
Contributor

I'd revert all these commits back to using string literals + in the cases where applies .freeze method. Favoring this way Ruby 2.1+ over old Ruby versions. In latests Rubies there are optimizations made when using .freeze, when using a string as a key of a Hash and also I think in 2.3 are coming optimizations for cases like s == 'hi'. So using literals will work better because we avoid paying the constant lookup time.

/cc @raggi @jeremy please jump in and let's decide what to do.

@schneems

Copy link
Copy Markdown
Contributor Author

Ruby 2.2 isn't out yet and won't be officially stable till december. Even then there are people who can't upgrade immediately that will benefit from the extra speed. Even if we don't want to do constant frozen strings, we should still update the respond_to and method_missing methods, as they are called on body proxy frequently and won't be helped by any string hash optimizations since they're not using hashes.

@spastorino

Copy link
Copy Markdown
Contributor

@schneems agreed about respond_to and method_missing. About the rest I prefer the code to stay as it always was and to favor more recent Ruby versions than older ones.
2.1 is already out and 2.2 will be out very soon, probably before another Rack version is out.

@raggi

raggi commented Oct 29, 2014

Copy link
Copy Markdown
Member

Lets be clear about speed here. In Rails this is faster because the GC is dominating your benchmarks. That's because rails is fat, and the heap is fragmenting during the benchmark (map it).

If you do a micro-benchmark under pure rack, where head slabs are regularly rotated then temporary garbage is completely irrelevant to the benchmark numbers. In these cases, the cost of constant lookup is greater than the cost of string construction.

I will not argue against a point that Rails accounts for most of our users, however, I'm still not 100% persuaded that Rails GC problems are (or should be) Rack core problems.

@raggi

raggi commented Oct 29, 2014

Copy link
Copy Markdown
Member

If we really wanted speed here, over all else, Symbols are the way forward. Ruby-core still doesn't have a native HWIA though, so, until that exists, again I'm not sure that's a good choice for Rack.

@raggi

raggi commented Oct 29, 2014

Copy link
Copy Markdown
Member

So after further thought, here's where I'm landing:

  • Users with simple and heap-light apps generally free all request generated objects periodically and don't suffer fragmentation or retention issues. They don't have large GC costs, but they also don't have complex performance challenges. If they have literals creating garbage, they're mostly unaffected. If they have constant lookup costs, they're also mostly unaffected. If they needed to optimize out one of these cases, maintaining whatever patches necessary to do so is relatively easy because their apps are small.
  • Users with complex and heap-heavy apps generally do not free all request generated objects on any kind of predictable frequency. They have very large GC costs and hard performance challenges. If they have literals creating garbage, they have a hard time overcoming this, and maintaining patches to assist is a large burden among many other burdens. They care significantly more about GC cost than constant lookup costs. If they need to maintain optimizations as patches this adds to an already significant upgrade burden.

Given these trade-offs I'd say we can make a choice to preference assisting people with the large heap problems, so long as it's not a huge penalty for those with simple apps. It's a global effort optimization, rather than a local roflscale optimization ;-)

@schneems

Copy link
Copy Markdown
Contributor Author

I did some benchmarks with 2.2.0

There are string optimizations going into 2.2.0 but only for hash access and creation (for now) https://gist.github.com/schneems/698df49c8c22cfea1d2c

Using a test Rails 4.2.0beta2 app I tested my previous commit and the current Ruby beta i'm seeing these differences: https://gist.github.com/schneems/df188395c862221071df

We're still using less memory and there is still a speed up but neither are as dramatic.

@raggi

raggi commented Oct 29, 2014

Copy link
Copy Markdown
Member

Another final note: as we transition into a multithreaded server world, the GC troubles will only get worse for all users, so in this case, going for the constants is also a good move.

@schneems

schneems commented Nov 5, 2014

Copy link
Copy Markdown
Contributor Author

What did we decide? From my benchmarks it's still faster on 2.2.

@spastorino

Copy link
Copy Markdown
Contributor

@schneems you never compared against "foo".freeze directly in the source instead of using a constant and there is where 2.2 will be faster in some (probably most) cases.

schneems referenced this pull request Jan 22, 2015
How many? Using `memory_profiler` and a Rails app (codetriage.com), master uses:

```
rack/lib x 7318
```

After this patch, the app uses:

```
rack/lib x 4598
```

Or `(7318 - 4598) / 7318.0 * 100 # => 37.16` % fewer objects __PER REQUEST__.

To do this, I extracted really commonly used strings into top level Rack constants. It makes for a bit of a big diff, but I believe the changes are worth it. 

Running benchmark/ips against the same app, I'm seeing a performance host of `2~4%` across the entire app response. This doesn't just make Rack faster, it will make your app faster.

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.
spastorino added a commit that referenced this pull request Feb 2, 2015
@spastorino
spastorino merged commit 558cf20 into rack:master Feb 2, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants