No commit activity in last 3 years
No release in over 3 years
Bring the speed back to your Active Record models!
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

 Project Readme

lightweight_attributes

lightweight_attributes is a tiny monkey-patch for making Active Record model objects ultimately lightweight.

Benchmarks

Here's a result of a simple benchmark measuring memory usage, number of method calls, and execution time with and without this gem, for fetching 10,000 records from a MySQL database and iterating over each of them and reading each attribute. The benchmark is included in this repo so everyone can try.

Allocated Memory and Allocated Objects

lightweight_attributes halves the memory allocation and reduces the object creation to be 25%!

$ bundle e ruby benchmark.rb memory
****************************** ActiveModel::AttributeSet ******************************
Total allocated: 26969712 bytes (270001 objects)
Total retained:  24649712 bytes (260001 objects)

allocated memory by class
-----------------------------------
  12800000  Hash
   8000000  ActiveModel::Attribute::FromDatabase
   3600000  String
   1200000  Model
    880000  ActiveModel::LazyAttributeHash
    400000  ActiveModel::AttributeSet
     89712  Array

allocated objects by class
-----------------------------------
    100000  ActiveModel::Attribute::FromDatabase
     90000  String
     50000  Hash
     10000  ActiveModel::AttributeSet
     10000  ActiveModel::LazyAttributeHash
     10000  Model
         1  Array


****************************** LightweightAttributes ******************************
Total allocated: 14889712 bytes (70001 objects)
Total retained:  12569712 bytes (60001 objects)

allocated memory by class
-----------------------------------
  12800000  Hash
   1200000  Model
    800000  LightweightAttributes::AttributeSet
     89712  Array

allocated objects by class
-----------------------------------
     50000  Hash
     10000  LightweightAttributes::AttributeSet
     10000  Model
         1  Array

Number of Method Calls

lightweight_attributes reduces method calls to be less than 40%!

$ bundle e ruby benchmark.rb methods
****************************** ActiveModel::AttributeSet ******************************
{"ActiveRecord::Result#each"=>1,
 "ActiveRecord::Result#hash_rows"=>1,
 "ActiveRecord::Persistence::ClassMethods#instantiate"=>10000,
 "ActiveRecord::Inheritance::ClassMethods#discriminate_class_for_record"=>10000,
 "ActiveRecord::Inheritance::ClassMethods#using_single_table_inheritance?"=>10000,
 "ActiveRecord::ModelSchema::ClassMethods#inheritance_column"=>20000,
 "Object#present?"=>10000,
 "NilClass#blank?"=>10000,
 "ActiveRecord::Persistence::ClassMethods#discriminate_class_for_record"=>10000,
 "ActiveRecord::ModelSchema::ClassMethods#attributes_builder"=>10000,
 "ActiveModel::AttributeSet::Builder#build_from_database"=>10000,
 "ActiveModel::LazyAttributeHash#initialize"=>10000,
 "ActiveModel::AttributeSet#initialize"=>10000,
 "ActiveRecord::Core::ClassMethods#allocate"=>10000,
 "ActiveRecord::AttributeMethods::ClassMethods#define_attribute_methods"=>20000,
 "ActiveRecord::Core#init_with"=>10000,
 "#<Class:ActiveRecord::LegacyYamlAdapter>#convert"=>10000,
 "ActiveRecord::ModelSchema::ClassMethods#yaml_encoder"=>10000,
 "ActiveModel::AttributeSet::YAMLEncoder#decode"=>10000,
 "ActiveRecord::Aggregations#init_internals"=>10000,
 "ActiveRecord::Associations#init_internals"=>10000,
 "ActiveRecord::Core#init_internals"=>10000,
 "ActiveRecord::Base#_run_find_callbacks"=>10000,
 "ActiveSupport::Callbacks#run_callbacks"=>20000,
 "ActiveRecord::Base#__callbacks"=>20000,
 "#<Class:ActiveRecord::Base>#__callbacks"=>20000,
 "ActiveSupport::Callbacks::CallbackChain#empty?"=>20000,
 "ActiveRecord::Base#_run_initialize_callbacks"=>10000,
 "ActiveRecord::AttributeMethods::PrimaryKey#id"=>10000,
 "ActiveRecord::Transactions#sync_with_transaction_state"=>10000,
 "ActiveRecord::Transactions#update_attributes_from_transaction_state"=>10000,
 "ActiveRecord::AttributeMethods::PrimaryKey::ClassMethods#primary_key"=>10000,
 "ActiveRecord::AttributeMethods::Read#_read_attribute"=>100000,
 "ActiveModel::AttributeSet#fetch_value"=>100000,
 "ActiveModel::AttributeSet#[]"=>100000,
 "ActiveModel::LazyAttributeHash#[]"=>100000,
 "ActiveModel::LazyAttributeHash#assign_default_value"=>100000,
 "#<Class:ActiveModel::Attribute>#from_database"=>100000,
 "ActiveModel::Attribute#initialize"=>100000,
 "ActiveModel::Attribute#value"=>100000,
 "ActiveModel::Attribute::FromDatabase#type_cast"=>100000,
 "ActiveModel::Type::Integer#deserialize"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c613"=>10000,
 "ActiveModel::Type::Value#deserialize"=>90000,
 "ActiveModel::Type::Value#cast"=>90000,
 "ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlString#cast_value"=>90000,
 "ActiveModel::Type::String#cast_value"=>90000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c623"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c633"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c643"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c653"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c663"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c673"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c683"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c693"=>10000}


****************************** LightweightAttributes ******************************
{"ActiveRecord::Result#each"=>1,
 "ActiveRecord::Result#hash_rows"=>1,
 "ActiveRecord::Persistence::ClassMethods#instantiate"=>10000,
 "ActiveRecord::Inheritance::ClassMethods#discriminate_class_for_record"=>10000,
 "ActiveRecord::Inheritance::ClassMethods#using_single_table_inheritance?"=>10000,
 "ActiveRecord::ModelSchema::ClassMethods#inheritance_column"=>20000,
 "Object#present?"=>10000,
 "NilClass#blank?"=>10000,
 "ActiveRecord::Persistence::ClassMethods#discriminate_class_for_record"=>10000,
 "ActiveRecord::ModelSchema::ClassMethods#attributes_builder"=>10000,
 "LightweightAttributes::AttributeSet::Builder#build_from_database"=>10000,
 "LightweightAttributes::AttributeSet#initialize"=>10000,
 "ActiveRecord::Core::ClassMethods#allocate"=>10000,
 "ActiveRecord::AttributeMethods::ClassMethods#define_attribute_methods"=>20000,
 "ActiveRecord::Core#init_with"=>10000,
 "#<Class:ActiveRecord::LegacyYamlAdapter>#convert"=>10000,
 "ActiveRecord::ModelSchema::ClassMethods#yaml_encoder"=>10000,
 "ActiveModel::AttributeSet::YAMLEncoder#decode"=>10000,
 "ActiveRecord::Aggregations#init_internals"=>10000,
 "ActiveRecord::Associations#init_internals"=>10000,
 "ActiveRecord::Core#init_internals"=>10000,
 "ActiveRecord::Base#_run_find_callbacks"=>10000,
 "ActiveSupport::Callbacks#run_callbacks"=>20000,
 "ActiveRecord::Base#__callbacks"=>20000,
 "#<Class:ActiveRecord::Base>#__callbacks"=>20000,
 "ActiveSupport::Callbacks::CallbackChain#empty?"=>20000,
 "ActiveRecord::Base#_run_initialize_callbacks"=>10000,
 "ActiveRecord::AttributeMethods::PrimaryKey#id"=>10000,
 "ActiveRecord::Transactions#sync_with_transaction_state"=>10000,
 "ActiveRecord::Transactions#update_attributes_from_transaction_state"=>10000,
 "ActiveRecord::AttributeMethods::PrimaryKey::ClassMethods#primary_key"=>10000,
 "ActiveRecord::AttributeMethods::Read#_read_attribute"=>100000,
 "LightweightAttributes::AttributeSet#fetch_value"=>100000,
 "ActiveModel::Type::Integer#deserialize"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c613"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c623"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c633"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c643"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c653"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c663"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c673"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c683"=>10000,
 "#<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x00007fcb5c4e06b8>#__temp__36f6c693"=>10000}

Elapsed Time

lightweight_attributes makes it 4x faster!

$ bundle e ruby benchmark.rb time
****************************** ActiveModel::AttributeSet ******************************
1.590478

****************************** LightweightAttributes ******************************
0.384524

Installation

Just bundle 'lightweight_attributes' gem on your Rails apps, and you're all set. No configurations!

Usage

It just works. I said, no configurations. This gem changes Active Record's data structure without changing any public API, so you don't need to change any single line of code in your application. If bundling this gem introduces any incompatibility besides making things fast and less memory-consuming, that should be a bug.

Why ActiveRecord::Base Is Slow and Heavy, and How This Gem Makes It Fast and Lightweight?

The Active Record Object

Active Record model object is a super hero that plays multiple roles. One is "form object". For this use case, we usually create just one object per request, so we don't have to care about performance of this object.

Another use case is "data transfer object". For this purpose, we would create hundreds of objects for rendering one HTML page. We would even create millions of model objects for APIs or batch systems. In this case, size of each object deeply impacts the whole system performance.

The Attribute API

As a version 4.0 feature, Active Record eqiupped a new API named "attributes API" that handles type casting between user, application, and database. However, this new feature caused a significant performance regression. For that purpose, each attribute per each model instance holds an instance of Attribute object that handles all the heavy lifting works around type casting.

Because of that data structure, ActiveRecord::Base became a super fat and heavy object. For example, when instantiating 1,000 records having 10 columns, it internally creates 10,000 instances ofAttribute objects. This of course causes a massive GC pressure.

The Solution

This gem defers the creation of attribute objects in a particular use case.

For the "data transfer object" use case, what we really need is a read-only Struct like object. We don't need interactive type casting feature. We don't need dirty tracking feature. lightweight_attributes focuses on this use case. It's a set of monkey-patches that holds the set of data from DB in a single Hash object rather than an Array of Attribute objects.

This behavior is base on an assumption that AR model objects from database are "read only" in most cases. There could still be some cases where a model from database receives attribute writes (e.g. for update action). And for such cases, our attributes object takes "deoptimization" approach. When this Hash-based model object receives any attribute write, it metamorphses to be a normal attribute-based model object, then falls back to the original Active Record attributes API.

Contributing

Pull requests are welcome on GitHub at https://github.com/amatsuda/lightweight_attributes.

License

The gem is available as open source under the terms of the MIT License.

Sponsor

This gem has been developed under total financial support from Akatsuki Inc..