Skip to content

Commit bf5bfb1

Browse files
dblockCopilot
andcommitted
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>
1 parent 82457aa commit bf5bfb1

4 files changed

Lines changed: 79 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* [#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).
44
* [#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).
5+
* [#61](https://github.com/dblock/ruby-enum/pull/61): Document performance overhead in README, add `rake benchmark:basic` - [@dblock](https://github.com/dblock).
56
* Your contribution here.
67

78
### 1.1.0 (2026/6/20)

‎README.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Enum-like behavior for Ruby, heavily inspired by [this](http://www.rubyfleebie.c
2727
- [Exhaustive case matcher](#exhaustive-case-matcher)
2828
- [I18n support](#i18n-support)
2929
- [Benchmarks](#benchmarks)
30+
- [Performance](#performance)
3031
- [Contributing](#contributing)
3132
- [Copyright and License](#copyright-and-license)
3233
- [Related Projects](#related-projects)
@@ -335,10 +336,21 @@ gem "i18n"
335336
Benchmark scripts are defined in the [`benchmarks`](benchmarks) folder and can be run with Rake:
336337

337338
```console
339+
rake benchmark:basic
338340
rake benchmark:case
339341
rake benchmark:inheritance
340342
```
341343

344+
### Performance
345+
346+
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.
347+
348+
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`).
349+
350+
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.
351+
352+
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.
353+
342354
## Contributing
343355

344356
You're encouraged to contribute to ruby-enum. See [CONTRIBUTING](CONTRIBUTING.md) for details.

‎Rakefile‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ RuboCop::RakeTask.new(:rubocop)
1818
task default: %i[rubocop spec]
1919

2020
namespace :benchmark do
21+
desc 'Run benchmark for basic Ruby::Enum operations vs. plain Ruby equivalents'
22+
task :basic do
23+
require_relative 'benchmarks/basic'
24+
end
25+
2126
desc 'Run benchmark for the Ruby::Enum::Case'
2227
task :case do
2328
require_relative 'benchmarks/case'

‎benchmarks/basic.rb‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
3+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4+
5+
require 'benchmark'
6+
require 'ruby-enum'
7+
8+
##
9+
# Ruby::Enum equivalent of a plain Hash/constant lookup.
10+
class Colors
11+
include Ruby::Enum
12+
13+
define :RED, 'red'
14+
define :GREEN, 'green'
15+
define :BLUE, 'blue'
16+
end
17+
18+
# Plain Ruby equivalents, doing the same lookups without Ruby::Enum.
19+
PLAIN_HASH = { RED: 'red', GREEN: 'green', BLUE: 'blue' }.freeze
20+
21+
module PlainConstants
22+
RED = 'red'
23+
GREEN = 'green'
24+
BLUE = 'blue'
25+
end
26+
27+
n = 1_000_000
28+
29+
def benchmark(label, iterations, &block)
30+
time = Benchmark.realtime { iterations.times(&block) }
31+
puts "#{label}: #{time.round(4)}"
32+
time
33+
end
34+
35+
puts "Running #{n} iterations of each scenario below\n\n"
36+
37+
puts '--- Constant access ---'
38+
enum_const_time = benchmark('Ruby::Enum constant (Colors::RED)', n) { Colors::RED }
39+
plain_const_time = benchmark('plain Ruby constant (PlainConstants::RED)', n) { PlainConstants::RED }
40+
41+
puts "\n--- Key to value lookup ---"
42+
enum_value_time = benchmark('Ruby::Enum (Colors.value(:RED))', n) { Colors.value(:RED) }
43+
hash_value_time = benchmark('plain Hash (PLAIN_HASH[:RED])', n) { PLAIN_HASH[:RED] }
44+
45+
puts "\n--- Value to key lookup ---"
46+
benchmark('Ruby::Enum (Colors.key(\'red\'))', n) { Colors.key('red') }
47+
benchmark('plain Hash (PLAIN_HASH.key(\'red\'))', n) { PLAIN_HASH.key('red') }
48+
49+
puts "\n--- Existence checks ---"
50+
benchmark('Ruby::Enum (Colors.key?(:RED))', n) { Colors.key?(:RED) }
51+
benchmark('plain Hash (PLAIN_HASH.key?(:RED))', n) { PLAIN_HASH.key?(:RED) }
52+
53+
puts "\n--- Enumerating all keys/values ---"
54+
benchmark('Ruby::Enum (Colors.keys)', n) { Colors.keys }
55+
benchmark('plain Hash (PLAIN_HASH.keys)', n) { PLAIN_HASH.keys }
56+
benchmark('Ruby::Enum (Colors.values)', n) { Colors.values }
57+
benchmark('plain Hash (PLAIN_HASH.values)', n) { PLAIN_HASH.values }
58+
59+
puts "\n--- Comparison ---"
60+
puts "Ruby::Enum constant access is #{(enum_const_time / plain_const_time).round(2)}x plain Ruby constant access"
61+
puts "Ruby::Enum .value is #{(enum_value_time / hash_value_time).round(2)}x plain Hash#[]"

0 commit comments

Comments
 (0)