Open
Conversation
The core change replaces the 375-entry regex alternation used for unit resolution with hash-based longest-match lookups, and eliminates intermediate Unit object creation across all hot paths. All changes are pure Ruby. Key optimizations: - Hash-based tokenizer (resolve_unit_token) replaces regex scanning - compute_base_scalar_fast/compute_signature_fast avoid intermediate Unit creation during initialization - Lazy to_base caching on the instance (computed only when needed) - batch_define defers regex cache invalidation during definition loading - eliminate_terms uses count_units helper to avoid dup/flatten allocations - convert_to uses unit_array_scalar for direct scalar computation - Same-unit fast path for addition/subtraction skips base conversion - Cache uses O(1) hash lookup instead of O(n) array scan Performance improvements (1160 tests still pass): - Cold start: 3.8x faster (358ms -> 95ms) - Uncached parsing: 17-19x faster - to_base: 886x faster (lazy caching) - Conversions: 1.6-3.4x faster - Addition/subtraction: 1.6-1.7x faster Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement C extension (~550 lines) that accelerates finalize_initialization, eliminate_terms, and convert_to scalar math. Temperature units fall back to pure Ruby. All 1165 tests pass in both C and pure Ruby (RUBY_UNITS_PURE=1) modes. Benchmark results appended to plan_v2.md. Key speedups vs pure Ruby: cold start 2.4x, uncached parse 1.4-2.2x, hash/numeric constructor 2.6-3.3x, conversions 1.0-1.8x, arithmetic 1.2-1.5x. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… to scalar 1 In v4.1.0, Unit.new(nil, "m") silently succeeded by string-interpolating nil to "" and parsing " m" as a unit with implicit scalar 1. The pattern matching rewrite rejected nil via `if first` guards. Add explicit `[nil, String => second]` match that delegates to parse_single_arg, restoring the v4.1.0 behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug 1: parse_array now matches [Numeric, Unit] so that
Unit.new(9.29, Unit.new("1 m^2")) works instead of raising ArgumentError.
Bug 2: resolve_expression_tokens uses greedy multi-token lookahead so
space-containing aliases like "square meter" resolve as a single unit
instead of being split into unrecognized individual tokens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The C extension was making ~22-25 rb_funcall calls per finalize_initialization, each costing 200-700ns of Ruby method dispatch overhead. This commit eliminates most of them: - Access Definition properties (kind, display_name, scalar, numerator, denominator) via rb_ivar_get instead of rb_funcall (~300-700ns savings each) - Look up definitions via rb_hash_aref on the definitions hash instead of calling the definition() class method - Inline base? and unity? checks in C using ivar access and memcmp - Fetch class-level hashes (definitions, prefix_values, unit_values) once and pass to all helper functions - Use symbol pointer comparison instead of rb_equal for kind matching - Move temperature_tokens? check into C (strncmp vs Ruby array+regex) - Use rb_obj_freeze instead of rb_funcall(obj, :freeze) Results (hash constructor, most isolated benchmark): - Simple base unit: 5.01μs → 2.80μs (1.8x faster) - Compound unit: 13.30μs → 4.64μs (2.9x faster) - Arithmetic: 1.25-1.33x faster across add/sub/mul/div Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Security: - Add string type guards in defn_is_base() before RSTRING_PTR to prevent segfault on nil/non-string ivars - Add nil/type check on numerator/denominator in rb_unit_finalize(), falling back to Ruby path if ivars aren't arrays Correctness: - Add missing power-range validation in compute_signature_fast (Ruby fast path) matching the C code and unit_signature_vector behavior Performance: - Cache rb_intern() calls for keys/concat/==/to_r as static IDs instead of resolving on every invocation - Add fast path in units() returning cached @unit_name for default args Cleanup: - Remove dead C methods _c_units_string and _c_base_check (defined but never called from Ruby) - Remove unused static IDs id_iv_base_unit and id_iv_output - Exclude plan*.md and .claude/ from gem package - Remove extra blank line Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Rakefile requires rake/extensiontask but the gem was missing from the Gemfile, causing bundle exec rake to fail in CI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rake-compiler tries to compile the C extension on JRuby which doesn't support native extensions. Guard the ExtensionTask and the spec:compile dependency behind a RUBY_ENGINE check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a temperature unit like "tempF" was cached, parsing "0 tempF" would copy the cached unit and multiply base_scalar by 0. This is incorrect because temperature conversions involve an offset (e.g. 0°F = 255.37K, not 0K). Reset base_scalar to nil for temperature units so update_base_scalar recomputes it correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Faster Implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Our application makes heavy use of this excellent gem. The performance of the gem was however a bottleneck to our application.
This version is what we now run in production without known issue. This is a pretty major rewrite of the library and might now be in line with the direction you want to take this gem. If so, feel free to close this PR. I however wanted to take the time to put up a PR for this as this might be useful to others.
Summary
Major performance overhaul of ruby-units, replacing regex-based unit parsing with hash-based architecture and adding an optional C extension for hot-path acceleration. All 1173 tests pass in both C and pure Ruby (
RUBY_UNITS_PURE=1) modes. Still compatible with JRuby via pure ruby mode.Ruby-level optimizations
resolve_unit_token) replaces the 375-entry regex alternation for unit resolutioncompute_base_scalar_fast/compute_signature_fastavoid creating intermediate Unit objects during initializationto_basecaching on the instance — computed once, then memoizedbatch_definedefers regex cache invalidation during definition loading (cold start)eliminate_termsuses acount_unitshelper to avoiddup/flattenallocationsconvert_tousesunit_array_scalarfor direct scalar computation without intermediate arrays+/-skips base conversion when numerator/denominator match exactlyHash#key?(O(1)) instead ofArray#include?(O(n))units()fast path returns cached@unit_namefor default argumentsC extension (~870 lines, optional)
Accelerates
finalize_initialization,eliminate_terms, andconvert_toscalar math. Falls back to pure Ruby for temperature units and when the extension is unavailable (JRuby,RUBY_UNITS_PURE=1, etc.).Key C-level techniques:
rb_ivar_get) instead ofrb_funcallfor Definition properties (~300-700ns savings per call)rb_hash_aref) instead of Ruby method dispatch for definitionsstrncmpin C, returning false to signal Ruby fallbackrb_internIDs for all method/ivar lookupsdefinitions/prefix_values/unit_valuesfetched once and passed to all helpersPerformance vs master
Benchmarked on Ruby 4.0.1, aarch64-linux. Numbers are iterations/second (higher is better).
Unit creation (uncached — cache cleared each iteration)
Unit creation (cached / constructor variants)
Unit conversions
Arithmetic
Complexity scaling (uncached, batch of units per iteration)
🤖 Generated with Claude Code