We have an application that displays thousands of currency values on a 
page, using the "number_to_currency" converter. When profiling the view 
rendering time, I found that a large amount of time was spent in the number 
helper classes. One level deeper, a bunch of time was spent calling 
I18n.translate to retrieve format options that were the same for the entire 
page.

I think it would be nice to optimize these helpers to allow subclasses of 
NumberConverter to be reused. I spent 10 minutes modifying the converters 
to allow them to be called more than once with the same options. After 
that, I did a quick test using bench.rb from the attached diff and saw 
roughly a 10x improvement in speed:

$ bundle exec ./bench.rb 

Rehearsal ------------------------------------------------------

original delimited   3.110000   0.070000   3.180000 (  3.262467)

reused delimited     0.270000   0.010000   0.280000 (  0.284962)

original currency   13.140000   0.100000  13.240000 ( 13.395538)

reused currency      1.240000   0.020000   1.260000 (  1.276842)

-------------------------------------------- total: 17.960000sec


                         user     system      total        real

original delimited   2.820000   0.010000   2.830000 (  2.846727)

reused delimited     0.250000   0.010000   0.260000 (  0.265952)

original currency   12.870000   0.080000  12.950000 ( 13.020219)

reused currency      1.220000   0.020000   1.240000 (  1.241132)

Are these changes something the Rails Core team is amenable to pursuing? 
Should I open a PR with my changes and some additional tests?

Thanks!

-md

-- 
You received this message because you are subscribed to the Google Groups "Ruby 
on Rails: Core" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to rubyonrails-core+unsubscr...@googlegroups.com.
To post to this group, send email to rubyonrails-core@googlegroups.com.
Visit this group at https://groups.google.com/group/rubyonrails-core.
For more options, visit https://groups.google.com/d/optout.
diff --git a/activesupport/bench.rb b/activesupport/bench.rb
new file mode 100755
index 0000000000..4a81de97f2
--- /dev/null
+++ b/activesupport/bench.rb
@@ -0,0 +1,35 @@
+#!/usr/bin/env ruby
+
+require 'benchmark'
+require 'active_support'
+
+count = 100_000
+number = 1_000_000
+
+Benchmark.bmbm do |x|
+  x.report('original delimited') do
+    count.times do
+      ActiveSupport::NumberHelper::NumberToDelimitedConverter.new(number).execute
+    end
+  end
+
+  x.report('reused delimited') do
+    converter = ActiveSupport::NumberHelper::NumberToDelimitedConverter.new
+    count.times do
+      converter.execute(number)
+    end
+  end
+
+  x.report('original currency') do
+    count.times do
+      ActiveSupport::NumberHelper::NumberToCurrencyConverter.new(number).execute
+    end
+  end
+
+  x.report('reused currency') do
+    converter = ActiveSupport::NumberHelper::NumberToCurrencyConverter.new
+    count.times do
+      converter.execute(number)
+    end
+  end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb
index 5ea9c8f113..b71e75069c 100644
--- a/activesupport/lib/active_support/number_helper/number_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_converter.rb
@@ -120,16 +120,19 @@ def self.convert(number, options)
         new(number, options).execute
       end
 
-      def initialize(number, options)
-        @number = number
-        @opts   = options.symbolize_keys
+      def initialize(*args)
+        raise if args.length > 2
+        @number = args.shift unless args.first.is_a?(Hash)
+        @opts   = (args.shift || {}).symbolize_keys
       end
 
-      def execute
+      def execute(number = self.number)
         if !number
           nil
         elsif validate_float? && !valid_float?
           number
+        elsif method(:convert).arity > 0
+          convert(number)
         else
           convert
         end
diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
index 1943b9e295..385260d17f 100644
--- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
@@ -7,8 +7,8 @@ module NumberHelper
     class NumberToCurrencyConverter < NumberConverter # :nodoc:
       self.namespace = :currency
 
-      def convert
-        number = self.number.to_s.strip
+      def convert(number = self.number)
+        number = number.to_s.strip
         format = options[:format]
 
         if number.to_f.negative?
diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
index d5b5706705..ae28b6c42c 100644
--- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
@@ -7,13 +7,13 @@ class NumberToDelimitedConverter < NumberConverter #:nodoc:
 
       DEFAULT_DELIMITER_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
 
-      def convert
-        parts.join(options[:separator])
+      def convert(number = self.number)
+        parts(number).join(options[:separator])
       end
 
       private
 
-        def parts
+        def parts(number)
           left, right = number.to_s.split(".".freeze)
           left.gsub!(delimiter_pattern) do |digit_to_delimit|
             "#{digit_to_delimit}#{options[:delimiter]}"
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
index 03eb6671ec..227cc1ccd1 100644
--- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
@@ -10,9 +10,9 @@ class NumberToHumanConverter < NumberConverter # :nodoc:
       self.namespace      = :human
       self.validate_float = true
 
-      def convert # :nodoc:
-        @number = RoundingHelper.new(options).round(number)
-        @number = Float(number)
+      def convert(number = self.number) # :nodoc:
+        number = RoundingHelper.new(options).round(number)
+        number = Float(number)
 
         # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
         unless options.key?(:strip_insignificant_zeros)
@@ -20,11 +20,11 @@ def convert # :nodoc:
         end
 
         units = opts[:units]
-        exponent = calculate_exponent(units)
-        @number = number / (10**exponent)
+        exponent = calculate_exponent(number, units)
+        number = number / (10**exponent)
 
         rounded_number = NumberToRoundedConverter.convert(number, options)
-        unit = determine_unit(units, exponent)
+        unit = determine_unit(number, units, exponent)
         format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, unit).strip
       end
 
@@ -34,7 +34,7 @@ def format
           options[:format] || translate_in_locale("human.decimal_units.format")
         end
 
-        def determine_unit(units, exponent)
+        def determine_unit(number, units, exponent)
           exp = DECIMAL_UNITS[exponent]
           case units
           when Hash
@@ -46,7 +46,7 @@ def determine_unit(units, exponent)
           end
         end
 
-        def calculate_exponent(units)
+        def calculate_exponent(number, units)
           exponent = number != 0 ? Math.log10(number.abs).floor : 0
           unit_exponents(units).find { |e| exponent >= e } || 0
         end
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
index 842f2fc8df..3694230b90 100644
--- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
@@ -8,21 +8,21 @@ class NumberToHumanSizeConverter < NumberConverter #:nodoc:
       self.namespace      = :human
       self.validate_float = true
 
-      def convert
-        @number = Float(number)
+      def convert(number = self.number)
+        number = Float(number)
 
         # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
         unless options.key?(:strip_insignificant_zeros)
           options[:strip_insignificant_zeros] = true
         end
 
-        if smaller_than_base?
+        if smaller_than_base?(number)
           number_to_format = number.to_i.to_s
         else
-          human_size = number / (base**exponent)
+          human_size = number / (base**exponent(number))
           number_to_format = NumberToRoundedConverter.convert(human_size, options)
         end
-        conversion_format.gsub("%n".freeze, number_to_format).gsub("%u".freeze, unit)
+        conversion_format.gsub("%n".freeze, number_to_format).gsub("%u".freeze, unit(number))
       end
 
       private
@@ -31,23 +31,23 @@ def conversion_format
           translate_number_value_with_default("human.storage_units.format", locale: options[:locale], raise: true)
         end
 
-        def unit
-          translate_number_value_with_default(storage_unit_key, locale: options[:locale], count: number.to_i, raise: true)
+        def unit(number)
+          translate_number_value_with_default(storage_unit_key(number), locale: options[:locale], count: number.to_i, raise: true)
         end
 
-        def storage_unit_key
-          key_end = smaller_than_base? ? "byte" : STORAGE_UNITS[exponent]
+        def storage_unit_key(number)
+          key_end = smaller_than_base?(number) ? "byte" : STORAGE_UNITS[exponent(number)]
           "human.storage_units.units.#{key_end}"
         end
 
-        def exponent
+        def exponent(number)
           max = STORAGE_UNITS.size - 1
           exp = (Math.log(number) / Math.log(base)).to_i
           exp = max if exp > max # avoid overflow for the highest unit
           exp
         end
 
-        def smaller_than_base?
+        def smaller_than_base?(number)
           number.to_i < base
         end
 
diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
index 4dcdad2e2c..3f8b592417 100644
--- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
@@ -5,7 +5,7 @@ module NumberHelper
     class NumberToPercentageConverter < NumberConverter # :nodoc:
       self.namespace = :percentage
 
-      def convert
+      def convert(number = self.number)
         rounded_number = NumberToRoundedConverter.convert(number, options)
         options[:format].gsub("%n".freeze, rounded_number)
       end
diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
index 96410f4995..e7c5161731 100644
--- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
@@ -3,7 +3,7 @@
 module ActiveSupport
   module NumberHelper
     class NumberToPhoneConverter < NumberConverter #:nodoc:
-      def convert
+      def convert(number = self.number)
         str = country_code(opts[:country_code]).dup
         str << convert_to_phone_number(number.to_s.strip)
         str << phone_ext(opts[:extension])
diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
index b7ad76bb62..2ae87e20ca 100644
--- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
@@ -6,7 +6,7 @@ class NumberToRoundedConverter < NumberConverter # :nodoc:
       self.namespace      = :precision
       self.validate_float = true
 
-      def convert
+      def convert(number = self.number)
         helper = RoundingHelper.new(options)
         rounded_number = helper.round(number)
 
@@ -37,10 +37,6 @@ def convert
 
       private
 
-        def calculate_rounded_number(multiplier)
-          (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier
-        end
-
         def digit_count(number)
           number.zero? ? 1 : (Math.log10(absolute_number(number)) + 1).floor
         end

Reply via email to