diff --git a/.rubocop.yml b/.rubocop.yml index 558e59a..ec2ada8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,6 +8,9 @@ Metrics/BlockLength: Exclude: - 'spec/**/*_spec.rb' +Metrics/ModuleLength: + Enabled: false + RSpec/SpecFilePathFormat: Enabled: false diff --git a/README.md b/README.md index 0c9563a..5e79e94 100644 --- a/README.md +++ b/README.md @@ -335,7 +335,8 @@ gem "i18n" Benchmark scripts are defined in the [`benchmarks`](benchmarks) folder and can be run with Rake: ```console -rake benchmarks:case +rake benchmark:case +rake benchmark:inheritance ``` ## Contributing diff --git a/Rakefile b/Rakefile index 2557ba4..2bb6a13 100644 --- a/Rakefile +++ b/Rakefile @@ -22,4 +22,9 @@ namespace :benchmark do task :case do require_relative 'benchmarks/case' end + + desc 'Run benchmark for enum lookups on classes with inheritance' + task :inheritance do + require_relative 'benchmarks/inheritance' + end end diff --git a/benchmarks/inheritance.rb b/benchmarks/inheritance.rb new file mode 100644 index 0000000..5e8ec9a --- /dev/null +++ b/benchmarks/inheritance.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) + +require 'benchmark' +require 'ruby-enum' + +## +# Base enum, no inheritance. +class Colors + include Ruby::Enum + + define :RED, 'red' + define :GREEN, 'green' +end + +## +# Subclass, adds its own enum on top of an inherited one (1 level). +class SubColors < Colors + define :BLUE, 'blue' +end + +## +# Sub-subclass, adds its own enum on top of 2 inherited levels. +class SubSubColors < SubColors + define :YELLOW, 'yellow' +end + +n = 1_000_000 + +def benchmark(label, iterations, &block) + time = Benchmark.realtime { iterations.times(&block) } + puts "#{label}: #{time.round(4)}" + time +end + +puts "Running #{n} iterations of each scenario below\n\n" + +puts '--- .value lookups, by depth of inheritance ---' +base_value = benchmark('base class (no inheritance)', n) { Colors.value(:RED) } +sub_value = benchmark('subclass (1 level)', n) { SubColors.value(:RED) } +sub_sub_value = benchmark('sub-subclass (2 levels)', n) { SubSubColors.value(:RED) } + +puts "\n--- Other methods on a subclass (1 level) ---" +benchmark('.keys', n) { SubColors.keys } +benchmark('.key?', n) { SubColors.key?(:RED) } +benchmark('.to_h', n) { SubColors.to_h } +count = 0 +benchmark('.each', n) { SubColors.each { |_k, _v| count += 1 } } + +puts "\n--- Comparison ---" +puts "subclass value is #{(sub_value / base_value).round(2)}x base class value" +puts "sub-subclass value is #{(sub_sub_value / base_value).round(2)}x base class value" diff --git a/lib/ruby-enum/enum.rb b/lib/ruby-enum/enum.rb index 66cac1c..70ded84 100644 --- a/lib/ruby-enum/enum.rb +++ b/lib/ruby-enum/enum.rb @@ -47,6 +47,9 @@ def store_new_instance(key, value) new_instance = new(key, value) _own_enum_hash[key] = new_instance _own_enums_by_value[value] = new_instance + + # Invalidate memoized, merged hashes since this class' own enums changed. + @_enum_hash = @_enums_by_value = nil end def const_missing(key) @@ -173,22 +176,22 @@ def _own_enums_by_value # with keys defined in this class taking precedence over those inherited # from a superclass. def _enum_hash - if superclass < Ruby::Enum - superclass.send(:_enum_hash).merge(_own_enum_hash) - else - _own_enum_hash - end + @_enum_hash ||= if superclass < Ruby::Enum + superclass.send(:_enum_hash).merge(_own_enum_hash) + else + _own_enum_hash + end end # Returns the enums-by-value hash for this class merged with all of its # superclasses, with values defined in this class taking precedence over # those inherited from a superclass. def _enums_by_value - if superclass < Ruby::Enum - superclass.send(:_enums_by_value).merge(_own_enums_by_value) - else - _own_enums_by_value - end + @_enums_by_value ||= if superclass < Ruby::Enum + superclass.send(:_enums_by_value).merge(_own_enums_by_value) + else + _own_enums_by_value + end end def upper?(s)