perf: Optimize field access to eliminate memory allocations#6112
perf: Optimize field access to eliminate memory allocations#6112skcc321 wants to merge 3 commits intomongodb:masterfrom
Conversation
Implement direct caching of demongoized field values on document instances to achieve zero memory allocations on repeated field access. Key optimizations: - Add @__demongoized_cache (Concurrent::Map) to each document for thread-safe value caching - Cache stores [raw_value, demongoized_value] tuples to detect stale cache - Invalidate cache on field writes and atomic operations - Handle edge cases: lazy defaults, localized fields, resizable values - Initialize field caches early for consistent object shape (JIT optimization) Performance improvements: - Zero allocations for cached field reads (String, Integer, Float, etc.) - Proper change tracking for resizable fields (Arrays, Hashes) - Thread-safe concurrent access using Concurrent::Map - Object shape consistency for JIT compiler optimization Testing: - Add comprehensive performance_spec.rb with 53 test cases - Cover allocation optimizations, cache invalidation, edge cases - Verify behavior with database-loaded documents and atomic operations - Test concurrent access patterns and resizable field mutations This optimization is always active and provides significant performance gains while maintaining full backward compatibility.
There was a problem hiding this comment.
Pull request overview
This PR introduces a demongoized-value caching layer and several fast paths to reduce allocations and speed up repeated field/attribute access in Mongoid documents, plus adds a dedicated performance spec suite to validate the allocation improvements.
Changes:
- Cache demongoized field values in generated getters (with invalidation on writes and atomic operations) and add fast paths for common “no dot” field-name cases.
- Add per-document caches for projection checks (
attribute_missing?) and initialize caches early for consistent object shape. - Add MRI-only
allocation_statsdependency and a comprehensive performance spec to assert zero allocations after warm-up.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/mongoid/fields/performance_spec.rb | Adds allocation-focused regression/performance tests for field access, caching, invalidation, and concurrency. |
| lib/mongoid/fields.rb | Implements fast paths and demongoized-value caching in generated field getters. |
| lib/mongoid/document.rb | Initializes per-document caches early (Concurrent::Map) to keep object shape stable and support cache usage. |
| lib/mongoid/attributes.rb | Adds demongoized-cache invalidation helper and caches projectors for attribute_missing?. |
| lib/mongoid/stateful.rb | Resets caches when resetting readonly/projection state (used during reload). |
| lib/mongoid/persistable/unsettable.rb | Clears demongoized cache when unsetting attributes in-memory. |
| lib/mongoid/persistable/renamable.rb | Clears demongoized cache entries for old/new names on rename. |
| lib/mongoid/persistable/pushable.rb | Clears demongoized cache when mutating array fields via $addToSet/$push. |
| lib/mongoid/persistable/pullable.rb | Clears demongoized cache when mutating array fields via $pull/$pullAll. |
| lib/mongoid/persistable/poppable.rb | Clears demongoized cache when mutating array fields via $pop. |
| lib/mongoid/persistable/multipliable.rb | Clears demongoized cache when mutating numeric fields via $mul. |
| lib/mongoid/persistable/logical.rb | Clears demongoized cache when mutating numeric fields via $bit. |
| lib/mongoid/persistable/incrementable.rb | Clears demongoized cache when mutating numeric fields via $inc. |
| Gemfile | Adds allocation_stats for MRI to support the new allocation specs. |
…emongoized_cache private - Use compute_if_absent for atomic projector cache operations to prevent race conditions - Make clear_demongoized_cache a private method to avoid expanding public API surface
| it 'handles concurrent projector cache access safely' do | ||
| band = Band.create!(name: 'Test') | ||
| errors = Concurrent::Array.new | ||
|
|
||
| threads = Array.new(10) do | ||
| Thread.new do | ||
| 100.times do | ||
| limited = Band.only(:name).find(band.id) | ||
| limited.attribute_missing?('rating') | ||
| rescue StandardError => e | ||
| errors << e | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
The concurrent projector-cache spec performs a DB query (Band.only(:name).find) inside a tight loop across 10 threads (1000 queries total). With the suite’s small connection pool (max_pool_size: 5), this can make CI slow/flaky and the test is not actually exercising concurrent access to the same document instance. Consider loading the projected document once outside the threads (or stubbing the projector/selected_fields) and then running the concurrent attribute_missing? calls against that same instance, or significantly reducing iterations/threads.
Move database query outside thread loop to properly test concurrent access to the same document instance. This eliminates 1000 DB queries, reduces connection pool contention, and makes the test faster and more reliable. Addresses review comment: mongodb#6112 (comment)
Optimize field access to eliminate memory allocations
Summary
Eliminates memory allocations for field access after warm-up through caching and fast-path optimizations, while preserving all existing functionality and ensuring thread
safety.
Motivation
Attribute reads in Mongoid currently perform type conversion (demongoization) on every access, causing unnecessary allocations and performance overhead. For applications with
frequent attribute access patterns, this can lead to significant memory pressure and reduced throughput.
Example of the problem:
This PR eliminates these repeated allocations through intelligent caching.
Performance Impact
100% allocation reduction across all native MongoDB field types:
All performance tests verified with
allocation_statsgem.Implementation Approach
This PR uses a direct caching approach - caching is embedded directly in field access methods and is always enabled.
Key Changes
Cache Demongoized Values (
lib/mongoid/fields.rb)Concurrent::Map#compute_if_absentfor atomic read-or-computeFast-path Optimizations (
lib/mongoid/fields.rb)splitoperations when field names don't contain dotscleanse_localized_field_names,traverse_association_tree,database_field_nameHash-based Projector Cache (
lib/mongoid/attributes.rb)__selected_fieldschangesattribute_missing?checksThread-Safe Caches (
lib/mongoid/document.rb)Concurrent::Mapfor thread-safe cache accesscompute_if_absentprevents race conditionsConsistent Object Shape (
lib/mongoid/document.rb)Advantages of this approach:
Trade-offs:
Safety Guarantees
demongoize()on first access - no unsafe shortcutsTest Coverage
performance tests covering:
.only,.without)Backward Compatibility
✅ 100% backward compatible
This implementation:
Example Usage
Breaking Changes
None. Internal optimizations only.
Related
See PR #6113 for a pluggable accessor classes caching implementation.