Skip to content

Comments

perf: Optimize field access to eliminate memory allocations (Accessor layer)#6113

Open
skcc321 wants to merge 1 commit intomongodb:masterfrom
skcc321:perf/optimize-field-access
Open

perf: Optimize field access to eliminate memory allocations (Accessor layer)#6113
skcc321 wants to merge 1 commit intomongodb:masterfrom
skcc321:perf/optimize-field-access

Conversation

@skcc321
Copy link

@skcc321 skcc321 commented Feb 21, 2026

Optimize field access to eliminate memory allocations by introducing Accessor layer for caching.

Summary

Introduces a clean, extensible Strategy Pattern architecture for attribute access in Mongoid, achieving zero memory allocations on subsequent field reads when caching is
enabled.

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:

user = User.find(id)

# Every read allocates memory for type conversion
10.times { user.name }  # 50 allocations (5 per read)
10.times { user.created_at }  # 111 allocations (11 per read)

This PR eliminates these repeated allocations through an opt-in caching strategy.

Performance Impact

100% allocation reduction across all native MongoDB field types:

Field Type Before (10x) After (10x) Reduction
String 50 0 100%
Integer 50 0 100%
Float 50 0 100%
Boolean 50 0 100%
Hash 40 0 100%
Array 40 0 100%
Time 111 0 100%
Date 50 0 100%
BSON::ObjectId 50 0 100%
Range 50 0 100%
Symbol 50 0 100%

All performance tests verified with allocation_stats gem.

Implementation

This PR uses the Strategy Pattern with pluggable accessor classes and opt-in configuration.

Core Architecture

The implementation uses two accessor strategies:

  1. Mongoid::Attributes::Accessor - Base accessor with no caching

    • Direct demongoization on every read
    • Zero memory overhead
    • Identical behavior to current Mongoid
  2. Mongoid::Attributes::CachingAccessor - Performance-optimized accessor

    • Thread-safe caching using Concurrent::Map
    • Zero allocations on warm cache reads
    • Smart cache invalidation on mutations

Configuration

Enable caching globally (opt-in):

# In initializer or application.rb
Mongoid::Config.cache_attribute_values = true

Or in mongoid.yml:

development:
  options:
    cache_attribute_values: true

Key Features

  • Zero allocations on cached reads for all field types
  • Thread-safe caching with Concurrent::Map
  • Automatic cache invalidation on write, remove, unset, rename, and reload operations
  • Special handling for localized fields (never cached) and lazy fields (evaluated before caching)
  • Class-level optimizations for database_field_name, cleanse_localized_field_names, and traverse_association_tree
  • Eager initialization - accessor strategy determined at document creation time

Advantages of this approach:

  • ✅ Clean separation of concerns (caching isolated in CachingAccessor)
  • ✅ Single Responsibility Principle (each accessor does ONE thing)
  • ✅ Open/Closed Principle (easy to add new strategies without modifying code)
  • ✅ Testable (both strategies can be unit tested independently)
  • ✅ Extensible (can add LRUCachingAccessor, TTLCachingAccessor, etc.)
  • ✅ Zero overhead when caching is disabled
  • ✅ Opt-in approach - safer for production rollout

Trade-offs:

  • ❌ Requires configuration to enable
  • ❌ More files and abstraction layers
  • ❌ Slightly more complex architecture

Safety Guarantees

  • ✅ Always calls demongoize() on first access - no unsafe shortcuts
  • ✅ Time timezone conversions preserved
  • ✅ BSON::Document → Hash conversion works
  • ✅ All type validations applied
  • ✅ Cache invalidation on write/reload/remove/unset/rename
  • ✅ Thread-safe concurrent access
  • ✅ Localized fields not cached - I18n.locale changes work correctly
  • ✅ Lazy-settable fields handled correctly
  • ✅ Accessor strategy locked at document creation time

Test Coverage

tests covering:

  • strategy pattern tests: accessor selection, configuration changes, database-loaded documents
  • performance tests: zero allocations, thread safety, cache invalidation, lazy fields, localized fields
  • Thread safety (concurrent access)
  • Time timezone handling
  • Cache invalidation scenarios
  • Field projections (.only, .without)
  • Correctness verification

Backward Compatibility

100% backward compatible

With cache_attribute_values = false (the default):

  • Identical behavior to current Mongoid
  • Same code paths for attribute reading
  • No memory overhead
  • No performance impact

The implementation adds only a single method call layer with negligible overhead.

Example Usage

class User
  include Mongoid::Document

  field :name, type: String
  field :email, type: String
  field :age, type: Integer
  field :metadata, type: Hash
end

# Enable caching
Mongoid::Config.cache_attribute_values = true

user = User.find(id)

# First read - performs demongoization
user.name  # => "John"

# Subsequent reads - zero allocations (cached)
user.name  # => "John" (from cache, no allocations)
user.name  # => "John" (from cache, no allocations)

# Write invalidates cache
user.name = "Jane"

# Next read re-demongoizes and caches
user.name  # => "Jane" (fresh, then cached)

Memory Considerations

When cache_attribute_values is enabled:

  • Each document instance allocates an accessor object
  • Each CachingAccessor maintains a Concurrent::Map for the cache
  • Each cached field value is stored in the map

For applications that keep many document instances in memory (thousands or more), this additional per-document and per-field state can lead to noticeable memory overhead.
Enable this option only when the performance benefits of avoiding repeated type conversions justify the extra memory use for your workload.

Breaking Changes

None. Internal optimizations only.

Related Issues

Addresses performance concerns with repeated attribute access patterns in high-throughput applications.

Related

See PR #6112 for an alternative direct caching implementation that is always enabled.

Implement a strategy pattern for field access that achieves zero memory
allocations on repeated reads through intelligent caching of demongoized
values.

Key optimizations:
- Introduce Accessor and CachingAccessor classes to encapsulate field
  access logic
- Cache demongoized values to eliminate repeated type conversions
- Invalidate cache only when fields are modified
- Handle edge cases: lazy defaults, localized fields, resizable values
- Add configuration flag cache_attribute_values (default: false)

Performance improvements:
- Zero allocations for cached field reads (String, Integer, Float, etc.)
- Proper change tracking for resizable fields (Arrays, Hashes)
- Thread-safe concurrent access for projections

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

This optimization maintains backward compatibility while providing
significant performance gains when enabled.
@skcc321 skcc321 requested a review from a team as a code owner February 21, 2026 21:39
@skcc321 skcc321 requested review from comandeo-mongo and Copilot and removed request for Copilot February 21, 2026 21:39
@skcc321 skcc321 changed the title perf: Optimize field access with strategy pattern for zero allocations perf: Optimize field access to eliminate memory allocations (Accessor layer) Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant