-
Donal McBreen authored
`delegate_missing_to` and `Enumerable#find` both allocate objects. When selecting a large number of encrypted values with can lead to a significant number of allocations. ``` require "bundler/inline" gemfile(true) do source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. gem "activerecord", "~> 7.0.0" gem "sqlite3" gem "benchmark-ips" end require "active_record" require 'benchmark/ips' ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Encryption.configure \ primary_key: "test master key", deterministic_key: "test deterministic key", key_derivation_salt: "testing key derivation salt", support_unencrypted_data: true ActiveRecord::Schema.define do create_table :comments, force: true do |t| t.string :message, length: 1000 end end class Comment < ActiveRecord::Base encrypts :message end srand(123456) 1000.times { Comment.create!(message: 100.times.map { ("A".."Z").to_a.sample }.join) } if ENV['OPTIMIZED'] module ActiveRecord module EncryptionPropertiesAvoidAllocations extend ActiveSupport::Concern ALLOWED_VALUE_CLASSES = [String, ActiveRecord::Encryption::Message, Numeric, TrueClass, FalseClass, Symbol, NilClass, Float, Integer] if %w{delegation all}.include?(ENV['OPTIMIZED']) delegate :each, :[], :key?, to: :data end if %w{find all}.include?(ENV['OPTIMIZED']) # find also involves allocations, so we can cache the results which classes # are valid to avoid the call def validate_value_type(value) unless ALLOWED_VALUE_CLASSES.include?(value.class) || ALLOWED_VALUE_CLASSES.any? { |klass| value.is_a?(klass) } raise ActiveRecord::Encryption::Errors::ForbiddenClass, "Can't store a #{value.class}, only properties of type #{ALLOWED_VALUE_CLASSES.inspect} are allowed" end end end end end ActiveRecord::Encryption::Properties.prepend(ActiveRecord::EncryptionPropertiesAvoidAllocations); end def allocation_count before = GC.stat(:total_allocated_objects) yield GC.stat(:total_allocated_objects) - before end allocated_objects = allocation_count { Comment.pluck(:message) } puts "Optimization: #{ENV['OPTIMIZED'] || "none"}, allocations: #{allocated_objects}" Benchmark.ips do |x| x.config(time: 30) x.report("default") { Comment.pluck(:message) } x.report("delegation_optimization") { Comment.pluck(:message) } x.report("find_optimization") { Comment.pluck(:message) } x.report("all_optimization") { Comment.pluck(:message) } x.hold! "temp_results" x.compare! end ``` Results: ``` $ ruby encryption_properties_benchmark.rb; OPTIMIZED=delegation ruby encryption_properties_benchmark.rb; OPTIMIZED=find ruby encryption_properties_benchmark.rb; OPTIMIZED=all ruby encryption_properties_benchmark.rb ...snip... Optimization: none, allocations: 72238 Warming up -------------------------------------- default 6.000 i/100ms Calculating ------------------------------------- default 70.643 (± 4.2%) i/s - 2.118k in 30.052046s ...snip... Optimization: delegation, allocations: 62313 Warming up -------------------------------------- delegation_optimization 7.000 i/100ms Calculating ------------------------------------- delegation_optimization 75.785 (± 4.0%) i/s - 2.275k in 30.086061s Pausing here -- run Ruby again to measure the next benchmark... Comparison: delegation_optimization: 75.8 i/s default: 70.6 i/s - same-ish: difference falls within error ...snip... Optimization: find, allocations: 60306 Warming up -------------------------------------- find_optimization 7.000 i/100ms Calculating ------------------------------------- find_optimization 74.179 (± 4.0%) i/s - 2.226k in 30.082991s Pausing here -- run Ruby again to measure the next benchmark... Comparison: delegation_optimization: 75.8 i/s find_optimization: 74.2 i/s - same-ish: difference falls within error default: 70.6 i/s - same-ish: difference falls within error ...snip... Optimization: all, allocations: 50315 Warming up -------------------------------------- all_optimization 8.000 i/100ms Calculating ------------------------------------- all_optimization 84.689 (± 5.9%) i/s - 2.528k in 30.008722s Comparison: all_optimization: 84.7 i/s delegation_optimization: 75.8 i/s - 1.12x (± 0.00) slower find_optimization: 74.2 i/s - 1.14x (± 0.00) slower default: 70.6 i/s - 1.20x (± 0.00) slower ``` Overall it's about 15% faster with a 30% reduction in allocations.
Donal McBreen authored`delegate_missing_to` and `Enumerable#find` both allocate objects. When selecting a large number of encrypted values with can lead to a significant number of allocations. ``` require "bundler/inline" gemfile(true) do source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. gem "activerecord", "~> 7.0.0" gem "sqlite3" gem "benchmark-ips" end require "active_record" require 'benchmark/ips' ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Encryption.configure \ primary_key: "test master key", deterministic_key: "test deterministic key", key_derivation_salt: "testing key derivation salt", support_unencrypted_data: true ActiveRecord::Schema.define do create_table :comments, force: true do |t| t.string :message, length: 1000 end end class Comment < ActiveRecord::Base encrypts :message end srand(123456) 1000.times { Comment.create!(message: 100.times.map { ("A".."Z").to_a.sample }.join) } if ENV['OPTIMIZED'] module ActiveRecord module EncryptionPropertiesAvoidAllocations extend ActiveSupport::Concern ALLOWED_VALUE_CLASSES = [String, ActiveRecord::Encryption::Message, Numeric, TrueClass, FalseClass, Symbol, NilClass, Float, Integer] if %w{delegation all}.include?(ENV['OPTIMIZED']) delegate :each, :[], :key?, to: :data end if %w{find all}.include?(ENV['OPTIMIZED']) # find also involves allocations, so we can cache the results which classes # are valid to avoid the call def validate_value_type(value) unless ALLOWED_VALUE_CLASSES.include?(value.class) || ALLOWED_VALUE_CLASSES.any? { |klass| value.is_a?(klass) } raise ActiveRecord::Encryption::Errors::ForbiddenClass, "Can't store a #{value.class}, only properties of type #{ALLOWED_VALUE_CLASSES.inspect} are allowed" end end end end end ActiveRecord::Encryption::Properties.prepend(ActiveRecord::EncryptionPropertiesAvoidAllocations); end def allocation_count before = GC.stat(:total_allocated_objects) yield GC.stat(:total_allocated_objects) - before end allocated_objects = allocation_count { Comment.pluck(:message) } puts "Optimization: #{ENV['OPTIMIZED'] || "none"}, allocations: #{allocated_objects}" Benchmark.ips do |x| x.config(time: 30) x.report("default") { Comment.pluck(:message) } x.report("delegation_optimization") { Comment.pluck(:message) } x.report("find_optimization") { Comment.pluck(:message) } x.report("all_optimization") { Comment.pluck(:message) } x.hold! "temp_results" x.compare! end ``` Results: ``` $ ruby encryption_properties_benchmark.rb; OPTIMIZED=delegation ruby encryption_properties_benchmark.rb; OPTIMIZED=find ruby encryption_properties_benchmark.rb; OPTIMIZED=all ruby encryption_properties_benchmark.rb ...snip... Optimization: none, allocations: 72238 Warming up -------------------------------------- default 6.000 i/100ms Calculating ------------------------------------- default 70.643 (± 4.2%) i/s - 2.118k in 30.052046s ...snip... Optimization: delegation, allocations: 62313 Warming up -------------------------------------- delegation_optimization 7.000 i/100ms Calculating ------------------------------------- delegation_optimization 75.785 (± 4.0%) i/s - 2.275k in 30.086061s Pausing here -- run Ruby again to measure the next benchmark... Comparison: delegation_optimization: 75.8 i/s default: 70.6 i/s - same-ish: difference falls within error ...snip... Optimization: find, allocations: 60306 Warming up -------------------------------------- find_optimization 7.000 i/100ms Calculating ------------------------------------- find_optimization 74.179 (± 4.0%) i/s - 2.226k in 30.082991s Pausing here -- run Ruby again to measure the next benchmark... Comparison: delegation_optimization: 75.8 i/s find_optimization: 74.2 i/s - same-ish: difference falls within error default: 70.6 i/s - same-ish: difference falls within error ...snip... Optimization: all, allocations: 50315 Warming up -------------------------------------- all_optimization 8.000 i/100ms Calculating ------------------------------------- all_optimization 84.689 (± 5.9%) i/s - 2.528k in 30.008722s Comparison: all_optimization: 84.7 i/s delegation_optimization: 75.8 i/s - 1.12x (± 0.00) slower find_optimization: 74.2 i/s - 1.14x (± 0.00) slower default: 70.6 i/s - 1.20x (± 0.00) slower ``` Overall it's about 15% faster with a 30% reduction in allocations.
Loading