From bf5bfb1ddfb150f5dbabc16f8d014c8134910b5d Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Fri, 14 Aug 2026 21:58:27 -0400 Subject: [PATCH] Document Ruby::Enum performance overhead in README Add benchmarks/basic.rb comparing basic Ruby::Enum operations (constant access, value, key, key?, keys, values) against their plain Ruby equivalents (constants, Hash), and a rake benchmark:basic task to run it. Add a new 'Performance' subsection under Benchmarks in README.md documenting, with real numbers, that constant access has no measurable overhead while hash-backed lookups carry roughly 3-5x overhead vs. a plain Hash, that this overhead does not grow with subclass depth (see rake benchmark:inheritance), and that Ruby::Enum::Case is the one notable outlier at 50-100x slower than a native case statement (see rake benchmark:case). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + README.md | 12 +++++++++ Rakefile | 5 ++++ benchmarks/basic.rb | 61 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 benchmarks/basic.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c46e9b..79f1b43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * [#49](https://github.com/dblock/ruby-enum/issues/49): Fixed `NoMethodError` on `keys`, `values`, `key?`, `value?`, `key`, `value`, `to_h`, `parse` and `each` when a subclass defines no enums of its own - [@dblock](https://github.com/dblock). * [#49](https://github.com/dblock/ruby-enum/issues/49): `keys`, `key?`, `value?`, `key`, `value`, `to_h`, `parse` and `each` now include enums defined in a superclass, matching the existing behavior of `values` - [@dblock](https://github.com/dblock). +* [#61](https://github.com/dblock/ruby-enum/pull/61): Document performance overhead in README, add `rake benchmark:basic` - [@dblock](https://github.com/dblock). * Your contribution here. ### 1.1.0 (2026/6/20) diff --git a/README.md b/README.md index 5e79e94..dcd0a08 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Enum-like behavior for Ruby, heavily inspired by [this](http://www.rubyfleebie.c - [Exhaustive case matcher](#exhaustive-case-matcher) - [I18n support](#i18n-support) - [Benchmarks](#benchmarks) + - [Performance](#performance) - [Contributing](#contributing) - [Copyright and License](#copyright-and-license) - [Related Projects](#related-projects) @@ -335,10 +336,21 @@ gem "i18n" Benchmark scripts are defined in the [`benchmarks`](benchmarks) folder and can be run with Rake: ```console +rake benchmark:basic rake benchmark:case rake benchmark:inheritance ``` +### Performance + +Constant access (e.g. `Colors::RED`) has no measurable overhead versus a plain Ruby constant, since it's just a constant lookup either way. Basic operations backed by a hash lookup - `value`, `key`, `key?`, `value?`, `keys`, `values` - carry some overhead (roughly 3-5x) compared to using a plain `Hash` directly, due to the extra method dispatch and object wrapping `Ruby::Enum` does internally. Run `rake benchmark:basic` to measure this on your own machine. + +This overhead is constant regardless of how deep a subclass hierarchy is - `keys`, `key?`, `value?`, `key`, `value`, `to_h`, `parse` and `each` merge and memoize enums inherited from superclasses, so a subclass' lookups are effectively as fast as the base class' (see `rake benchmark:inheritance`). + +The one notable exception is `Ruby::Enum::Case`, whose exhaustive `case`-like matcher is significantly slower (on the order of 50-100x, see `rake benchmark:case`) than a native Ruby `case`/`when` statement, since it builds and evaluates lambdas on every call rather than being optimized by the Ruby VM. Prefer a native `case` statement in hot code paths and reserve `Ruby::Enum::Case` for cases where its exhaustiveness check is worth the overhead. + +For most applications this overhead is negligible in absolute terms (low single-digit microseconds per call), but it's worth being aware of in very hot code paths. + ## Contributing You're encouraged to contribute to ruby-enum. See [CONTRIBUTING](CONTRIBUTING.md) for details. diff --git a/Rakefile b/Rakefile index 2bb6a13..6e78748 100644 --- a/Rakefile +++ b/Rakefile @@ -18,6 +18,11 @@ RuboCop::RakeTask.new(:rubocop) task default: %i[rubocop spec] namespace :benchmark do + desc 'Run benchmark for basic Ruby::Enum operations vs. plain Ruby equivalents' + task :basic do + require_relative 'benchmarks/basic' + end + desc 'Run benchmark for the Ruby::Enum::Case' task :case do require_relative 'benchmarks/case' diff --git a/benchmarks/basic.rb b/benchmarks/basic.rb new file mode 100644 index 0000000..32a70df --- /dev/null +++ b/benchmarks/basic.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) + +require 'benchmark' +require 'ruby-enum' + +## +# Ruby::Enum equivalent of a plain Hash/constant lookup. +class Colors + include Ruby::Enum + + define :RED, 'red' + define :GREEN, 'green' + define :BLUE, 'blue' +end + +# Plain Ruby equivalents, doing the same lookups without Ruby::Enum. +PLAIN_HASH = { RED: 'red', GREEN: 'green', BLUE: 'blue' }.freeze + +module PlainConstants + RED = 'red' + GREEN = 'green' + BLUE = 'blue' +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 '--- Constant access ---' +enum_const_time = benchmark('Ruby::Enum constant (Colors::RED)', n) { Colors::RED } +plain_const_time = benchmark('plain Ruby constant (PlainConstants::RED)', n) { PlainConstants::RED } + +puts "\n--- Key to value lookup ---" +enum_value_time = benchmark('Ruby::Enum (Colors.value(:RED))', n) { Colors.value(:RED) } +hash_value_time = benchmark('plain Hash (PLAIN_HASH[:RED])', n) { PLAIN_HASH[:RED] } + +puts "\n--- Value to key lookup ---" +benchmark('Ruby::Enum (Colors.key(\'red\'))', n) { Colors.key('red') } +benchmark('plain Hash (PLAIN_HASH.key(\'red\'))', n) { PLAIN_HASH.key('red') } + +puts "\n--- Existence checks ---" +benchmark('Ruby::Enum (Colors.key?(:RED))', n) { Colors.key?(:RED) } +benchmark('plain Hash (PLAIN_HASH.key?(:RED))', n) { PLAIN_HASH.key?(:RED) } + +puts "\n--- Enumerating all keys/values ---" +benchmark('Ruby::Enum (Colors.keys)', n) { Colors.keys } +benchmark('plain Hash (PLAIN_HASH.keys)', n) { PLAIN_HASH.keys } +benchmark('Ruby::Enum (Colors.values)', n) { Colors.values } +benchmark('plain Hash (PLAIN_HASH.values)', n) { PLAIN_HASH.values } + +puts "\n--- Comparison ---" +puts "Ruby::Enum constant access is #{(enum_const_time / plain_const_time).round(2)}x plain Ruby constant access" +puts "Ruby::Enum .value is #{(enum_value_time / hash_value_time).round(2)}x plain Hash#[]"