Permalink
Browse files

Freeze string literals when not mutated.

I wrote a utility that helps find areas where you could optimize your program using a frozen string instead of a string literal, it's called [let_it_go](https://github.com/schneems/let_it_go). After going through the output and adding `.freeze` I was able to eliminate the creation of 1,114 string objects on EVERY request to [codetriage](codetriage.com). How does this impact execution?

To look at memory:

```ruby
require 'get_process_mem'

mem = GetProcessMem.new
GC.start
GC.disable
1_114.times { " " }
before = mem.mb

after = mem.mb
GC.enable
puts "Diff: #{after - before} mb"

```

Creating 1,114 string objects results in `Diff: 0.03125 mb` of RAM allocated on every request. Or 1mb every 32 requests.

To look at raw speed:

```ruby
require 'benchmark/ips'

number_of_objects_reduced = 1_114

Benchmark.ips do |x|
  x.report("freeze")    { number_of_objects_reduced.times { " ".freeze } }
  x.report("no-freeze") { number_of_objects_reduced.times { " " } }
end
```

We get the results

```
Calculating -------------------------------------
              freeze     1.428k i/100ms
           no-freeze   609.000  i/100ms
-------------------------------------------------
              freeze     14.363k (± 8.5%) i/s -     71.400k
           no-freeze      6.084k (± 8.1%) i/s -     30.450k
```

Now we can do some maths:

```ruby
ips = 6_226k # iterations / 1 second
call_time_before = 1.0 / ips # seconds per iteration 

ips = 15_254 # iterations / 1 second
call_time_after = 1.0 / ips # seconds per iteration 

diff = call_time_before - call_time_after

number_of_objects_reduced * diff * 100

# => 0.4530373333993266 miliseconds saved per request
```

So we're shaving off 1 second of execution time for every 220 requests. 

Is this going to be an insane speed boost to any Rails app: nope. Should we merge it: yep. 

p.s. If you know of a method call that doesn't modify a string input such as [String#gsub](https://github.com/schneems/let_it_go/blob/b0e2da69f0cca87ab581022baa43291cdf48638c/lib/let_it_go/core_ext/string.rb#L37) please [give me a pull request to the appropriate file](https://github.com/schneems/let_it_go/blob/b0e2da69f0cca87ab581022baa43291cdf48638c/lib/let_it_go/core_ext/string.rb#L37), or open an issue in LetItGo so we can track and freeze more strings. 

Keep those strings Frozen

![](https://www.dropbox.com/s/z4dj9fdsv213r4v/let-it-go.gif?dl=1)
  • Loading branch information...
schneems committed Jul 19, 2015
1 parent ff54b96 commit 5bb1d4d288d019e276335465d0389fd2f5246bfd
Showing with 60 additions and 60 deletions.
  1. +1 −1 actionpack/lib/abstract_controller/base.rb
  2. +1 −1 actionpack/lib/abstract_controller/helpers.rb
  3. +1 −1 actionpack/lib/action_controller/log_subscriber.rb
  4. +2 −2 actionpack/lib/action_controller/metal/helpers.rb
  5. +4 −4 actionpack/lib/action_dispatch/http/parameter_filter.rb
  6. +1 −1 actionpack/lib/action_dispatch/http/url.rb
  7. +1 −1 actionpack/lib/action_dispatch/journey/nodes/node.rb
  8. +3 −3 actionpack/lib/action_dispatch/journey/router/utils.rb
  9. +3 −3 actionpack/lib/action_dispatch/middleware/static.rb
  10. +1 −1 actionpack/lib/action_dispatch/routing/route_set.rb
  11. +1 −1 actionview/lib/action_view/helpers/asset_tag_helper.rb
  12. +1 −1 actionview/lib/action_view/helpers/asset_url_helper.rb
  13. +3 −3 actionview/lib/action_view/lookup_context.rb
  14. +4 −4 actionview/lib/action_view/template.rb
  15. +2 −2 actionview/lib/action_view/template/resolver.rb
  16. +1 −1 activemodel/lib/active_model/attribute_methods.rb
  17. +1 −1 activemodel/lib/active_model/naming.rb
  18. +1 −1 activemodel/lib/active_model/validations/validates.rb
  19. +1 −1 activerecord/lib/active_record/attribute_methods.rb
  20. +1 −1 activerecord/lib/active_record/attribute_methods/read.rb
  21. +1 −1 activerecord/lib/active_record/attribute_methods/write.rb
  22. +1 −1 activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
  23. +1 −1 activerecord/lib/active_record/relation/delegation.rb
  24. +3 −3 activerecord/lib/active_record/relation/predicate_builder.rb
  25. +1 −1 activesupport/lib/active_support/core_ext/load_error.rb
  26. +1 −1 activesupport/lib/active_support/core_ext/module/delegation.rb
  27. +3 −3 activesupport/lib/active_support/dependencies.rb
  28. +10 −10 activesupport/lib/active_support/inflector/methods.rb
  29. +1 −1 activesupport/lib/active_support/inflector/transliterate.rb
  30. +1 −1 activesupport/lib/active_support/message_encryptor.rb
  31. +2 −2 activesupport/lib/active_support/message_verifier.rb
  32. +1 −1 activesupport/lib/active_support/multibyte/unicode.rb
@@ -96,7 +96,7 @@ def clear_action_methods!
# ==== Returns
# * <tt>String</tt>
def controller_path
@controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous?
@controller_path ||= name.sub(/Controller$/, ''.freeze).underscore unless anonymous?
end

# Refresh the cached action_methods when a new action_method is added.
@@ -181,7 +181,7 @@ def add_template_helper(mod)
end

def default_helper_module!
module_name = name.sub(/Controller$/, '')
module_name = name.sub(/Controller$/, ''.freeze)
module_path = module_name.underscore
helper module_path
rescue LoadError => e
@@ -25,7 +25,7 @@ def process_action(event)
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
end
message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
message << " (#{additions.join(" | ")})" unless additions.blank?
message << " (#{additions.join(" | ".freeze)})" unless additions.blank?
message
end
end
@@ -73,7 +73,7 @@ def helper_attr(*attrs)

# Provides a proxy to access helpers methods from outside the view.
def helpers
@helper_proxy ||= begin
@helper_proxy ||= begin
proxy = ActionView::Base.new
proxy.config = config.inheritable_copy
proxy.extend(_helpers)
@@ -100,7 +100,7 @@ def modules_for_helpers(args)
def all_helpers_from_path(path)
helpers = Array(path).flat_map do |_path|
extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') }
names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) }
names.sort!
end
helpers.uniq!
@@ -34,11 +34,11 @@ def self.compile(filters)
end
end

deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.") }
deep_strings, strings = strings.partition { |s| s.include?("\\.") }
deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) }
deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) }

regexps << Regexp.new(strings.join('|'), true) unless strings.empty?
deep_regexps << Regexp.new(deep_strings.join('|'), true) unless deep_strings.empty?
regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty?
deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty?

new regexps, deep_regexps, blocks
end
@@ -245,7 +245,7 @@ def raw_host_with_port
# req = Request.new 'HTTP_HOST' => 'example.com:8080'
# req.host # => "example.com"
def host
raw_host_with_port.sub(/:\d+$/, '')
raw_host_with_port.sub(/:\d+$/, ''.freeze)
end

# Returns a \host:\port string for this request, such as "example.com" or
@@ -30,7 +30,7 @@ def to_sym
end

def name
left.tr '*:', ''
left.tr '*:'.freeze, ''.freeze
end

def type
@@ -14,10 +14,10 @@ class Utils # :nodoc:
# normalize_path("/%ab") # => "/%AB"
def self.normalize_path(path)
path = "/#{path}"
path.squeeze!('/')
path.sub!(%r{/+\Z}, '')
path.squeeze!('/'.freeze)
path.sub!(%r{/+\Z}, ''.freeze)
path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
path = '/' if path == ''
path = '/' if path == ''.freeze
path
end

@@ -35,7 +35,7 @@ def match?(path)
paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]

if match = paths.detect { |p|
path = File.join(@root, p.force_encoding('UTF-8'))
path = File.join(@root, p.force_encoding('UTF-8'.freeze))
begin
File.file?(path) && File.readable?(path)
rescue SystemCallError
@@ -76,7 +76,7 @@ def ext
end

def content_type(path)
::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze)
end

def gzip_encoding_accepted?(env)
@@ -112,7 +112,7 @@ def initialize(app, path, cache_control = nil, index: 'index')
def call(env)
case env['REQUEST_METHOD']
when 'GET', 'HEAD'
path = env['PATH_INFO'].chomp('/')
path = env['PATH_INFO'].chomp('/'.freeze)
if match = @file_handler.match?(path)
env['PATH_INFO'] = match
return @file_handler.call(env)
@@ -671,7 +671,7 @@ def use_relative_controller!

# Remove leading slashes from controllers
def normalize_controller!
@options[:controller] = controller.sub(%r{^/}, '') if controller
@options[:controller] = controller.sub(%r{^/}, ''.freeze) if controller
end

# Move 'index' action from options to recall
@@ -237,7 +237,7 @@ def image_tag(source, options={})
# image_alt('underscored_file_name.png')
# # => Underscored file name
def image_alt(src)
File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize
File.basename(src, '.*'.freeze).sub(/-[[:xdigit:]]{32}\z/, ''.freeze).tr('-_'.freeze, ' '.freeze).capitalize
end

# Returns an HTML video tag for the +sources+. If +sources+ is a string,
@@ -127,7 +127,7 @@ def asset_path(source, options = {})
return "" unless source.present?
return source if source =~ URI_REGEXP

tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '')
tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, ''.freeze)

if extname = compute_asset_extname(source, options)
source = "#{source}#{extname}"
@@ -173,13 +173,13 @@ def detail_args_for(options)
# name instead of the prefix.
def normalize_name(name, prefixes) #:nodoc:
prefixes = prefixes.presence
parts = name.to_s.split('/')
parts = name.to_s.split('/'.freeze)
parts.shift if parts.first.empty?
name = parts.pop

return name, prefixes || [""] if parts.empty?

parts = parts.join('/')
parts = parts.join('/'.freeze)
prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]

return name, prefixes
@@ -204,7 +204,7 @@ def initialize(view_paths, details = {}, prefixes = [])
# add :html as fallback to :js.
def formats=(values)
if values
values.concat(default_formats) if values.delete "*/*"
values.concat(default_formats) if values.delete "*/*".freeze
if values == [:js]
values << :html
@html_fallback_for_js = true
@@ -190,7 +190,7 @@ def refresh(view)
end

def inspect
@inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier
@inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", ''.freeze) : identifier
end

# This method is responsible for properly setting the encoding of the
@@ -337,13 +337,13 @@ def locals_code #:nodoc:
def method_name #:nodoc:
@method_name ||= begin
m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
m.tr!('-', '_')
m.tr!('-'.freeze, '_'.freeze)
m
end
end

def identifier_method_name #:nodoc:
inspect.tr('^a-z_', '_')
inspect.tr('^a-z_'.freeze, '_'.freeze)
end

def instrument(action, &block)
@@ -366,7 +366,7 @@ def resource_cache_call_match
end

def inferred_cache_name
@inferred_cache_name ||= @virtual_path.split('/').last.sub('_', '')
@inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze)
end
end
end
@@ -222,7 +222,7 @@ def build_query(path, details)
end

def escape_entry(entry)
entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze)
end

# Returns the file mtime from the filesystem.
@@ -234,7 +234,7 @@ def mtime(p)
# from the path, or the handler, we should return the array of formats given
# to the resolver.
def extract_handler_and_format_and_variant(path, default_formats)
pieces = File.basename(path).split('.')
pieces = File.basename(path).split('.'.freeze)
pieces.shift

extension = pieces.pop
@@ -372,7 +372,7 @@ def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc:
"define_method(:'#{name}') do |*args|"
end

extra = (extra.map!(&:inspect) << "*args").join(", ")
extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)

target = if send =~ CALL_COMPILABLE_REGEXP
"#{"self." unless include_private}#{send}(#{extra})"
@@ -192,7 +192,7 @@ def human(options={})
private

def _singularize(string)
ActiveSupport::Inflector.underscore(string).tr('/', '_')
ActiveSupport::Inflector.underscore(string).tr('/'.freeze, '_'.freeze)
end
end

@@ -115,7 +115,7 @@ def validates(*attributes)
key = "#{key.to_s.camelize}Validator"

begin
validator = key.include?('::') ? key.constantize : const_get(key)
validator = key.include?('::'.freeze) ? key.constantize : const_get(key)
rescue NameError
raise ArgumentError, "Unknown validator: '#{key}'"
end
@@ -42,7 +42,7 @@ def initialize

def [](name)
@method_cache.compute_if_absent(name) do
safe_name = name.unpack('h*').first
safe_name = name.unpack('h*'.freeze).first
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
@module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
@@ -37,7 +37,7 @@ module ClassMethods
protected

def define_method_attribute(name)
safe_name = name.unpack('h*').first
safe_name = name.unpack('h*'.freeze).first
temp_method = "__temp__#{safe_name}"

ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
@@ -24,7 +24,7 @@ module ClassMethods
protected

def define_method_attribute=(name)
safe_name = name.unpack('h*').first
safe_name = name.unpack('h*'.freeze).first
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name

generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
@@ -12,7 +12,7 @@ def initialize(type_metadata, oid: nil, fmod: nil)
end

def sql_type
super.gsub(/\[\]$/, "")
super.gsub(/\[\]$/, "".freeze)
end

def ==(other)
@@ -18,7 +18,7 @@ def initialize_relation_delegate_cache # :nodoc:
delegate = Class.new(klass) {
include ClassSpecificRelation
}
const_set klass.name.gsub('::', '_'), delegate
const_set klass.name.gsub('::'.freeze, '_'.freeze), delegate
cache[klass] = delegate
end
end
@@ -52,7 +52,7 @@ def self.references(attributes)
key
else
key = key.to_s
key.split('.').first if key.include?('.')
key.split('.'.freeze).first if key.include?('.'.freeze)
end
end.compact
end
@@ -123,10 +123,10 @@ def associated_predicate_builder(association_name)
end

def convert_dot_notation_to_hash(attributes)
dot_notation = attributes.keys.select { |s| s.include?(".") }
dot_notation = attributes.keys.select { |s| s.include?(".".freeze) }

dot_notation.each do |key|
table_name, column_name = key.split(".")
table_name, column_name = key.split(".".freeze)
value = attributes.delete(key)
attributes[table_name] ||= {}

@@ -23,7 +23,7 @@ def path
# Returns true if the given path name (except perhaps for the ".rb"
# extension) is the missing file which caused the exception to be raised.
def is_missing?(location)
location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '')
location.sub(/\.rb$/, ''.freeze) == path.sub(/\.rb$/, ''.freeze)
end
end

@@ -167,7 +167,7 @@ def delegate(*methods)
''
end

file, line = caller(1, 1).first.split(':', 2)
file, line = caller(1, 1).first.split(':'.freeze, 2)
line = line.to_i

to = to.to_s
@@ -149,7 +149,7 @@ def new_constants

# Normalize the list of new constants, and add them to the list we will return
new_constants.each do |suffix|
constants << ([namespace, suffix] - ["Object"]).join("::")
constants << ([namespace, suffix] - ["Object"]).join("::".freeze)
end
end
constants
@@ -431,7 +431,7 @@ def loadable_constants_for_path(path, bases = autoload_paths)

# Search for a file in autoload_paths matching the provided suffix.
def search_for_file(path_suffix)
path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb")
path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb".freeze)

autoload_paths.each do |root|
path = File.join(root, path_suffix)
@@ -516,7 +516,7 @@ def load_missing_constant(from_mod, const_name)

if file_path
expanded = File.expand_path(file_path)
expanded.sub!(/\.rb\z/, '')
expanded.sub!(/\.rb\z/, ''.freeze)

if loading.include?(expanded)
raise "Circular dependency detected while autoloading constant #{qualified_name}"
Oops, something went wrong.

0 comments on commit 5bb1d4d

Please sign in to comment.