Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
61 changes: 61 additions & 0 deletions benchmarks/basic.rb
Original file line number Diff line number Diff line change
@@ -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#[]"
Loading