From 8c7e69b79b63a88a170a9b9004a906db00161a3b Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 8 Jan 2024 18:43:37 +0100 Subject: [PATCH] Optimize Hash#stringify_keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using Symbol#name allows to hit two birds with one stone. First it will return a pre-existing string, so will save one allocation per key. Second, that string will be already interned, so it will save the internal `Hash` implementation the work of looking up the interned strings table to deduplicate the key. ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-darwin21] Warming up -------------------------------------- to_s 17.768k i/100ms cond 23.703k i/100ms Calculating ------------------------------------- to_s 169.830k (±10.4%) i/s - 852.864k in 5.088377s cond 236.803k (± 7.9%) i/s - 1.185M in 5.040945s Comparison: to_s: 169830.3 i/s cond: 236803.4 i/s - 1.39x faster ``` ```ruby require 'bundler/inline' gemfile do source 'https://rubygems.org' gem 'benchmark-ips', require: false end HASH = { first_name: nil, last_name: nil, country: nil, profession: nil, language: nil, hobby: nil, pet: nil, longer_name: nil, occupation: nil, mailing_address: nil, }.freeze require 'benchmark/ips' Benchmark.ips do |x| x.report("to_s") { HASH.transform_keys(&:to_s) } x.report("cond") { HASH.transform_keys { |k| Symbol === k ? k.name : k.to_s } } x.compare!(order: :baseline) end ``` --- activesupport/lib/active_support/core_ext/hash/keys.rb | 8 ++++---- .../lib/active_support/hash_with_indifferent_access.rb | 2 +- activesupport/lib/active_support/json/encoding.rb | 8 +++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index ad5daccd7c9..fd74f030022 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -8,13 +8,13 @@ class Hash # hash.stringify_keys # # => {"name"=>"Rob", "age"=>"28"} def stringify_keys - transform_keys(&:to_s) + transform_keys { |k| Symbol === k ? k.name : k.to_s } end # Destructively converts all keys to strings. Same as # +stringify_keys+, but modifies +self+. def stringify_keys! - transform_keys!(&:to_s) + transform_keys! { |k| Symbol === k ? k.name : k.to_s } end # Returns a new hash with all keys converted to symbols, as long as @@ -82,14 +82,14 @@ class Hash # hash.deep_stringify_keys # # => {"person"=>{"name"=>"Rob", "age"=>"28"}} def deep_stringify_keys - deep_transform_keys(&:to_s) + deep_transform_keys { |k| Symbol === k ? k.name : k.to_s } end # Destructively converts all keys to strings. # This includes the keys from the root hash and from all # nested hashes and arrays. def deep_stringify_keys! - deep_transform_keys!(&:to_s) + deep_transform_keys! { |k| Symbol === k ? k.name : k.to_s } end # Returns a new hash with all keys converted to symbols, as long as diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 521cdf6a438..1402c510e9f 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -393,7 +393,7 @@ module ActiveSupport private def convert_key(key) - key.kind_of?(Symbol) ? key.name : key + Symbol === key ? key.name : key end def convert_value(value, conversion: nil) diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index fd7a9640a46..fc4a0960f29 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -76,7 +76,13 @@ module ActiveSupport when Hash result = {} value.each do |k, v| - k = k.to_s unless String === k + unless String === k + k = if Symbol === k + k.name + else + k.to_s + end + end result[k] = jsonify(v) end result