Skip to content

Commit db27b67

Browse files
committed
Optimize ActiveSupport::SafeBuffer
For normal objects, eagerly defining ivars is good to avoid trashing inline caches. But for subclasses of core types like String, ivars are stored in a global hash table, hence initializing and accessing them is rather costly. That cost is even higher if a ractor has been spawned, because looking up the table will require synchronizing the entire VM. Based on the assumption that the overwhelming majority of `SafeBuffer` instances are never mutated, hence never end up unsafe, we can optimize by marking the unsafe status rather than the opposite. This way we save on eagerly allocating the external buffer in the global ivar table, save from having to free it when the buffer is collected, and also save from having to lookup the table when accessing the ivar because the VM is smart enough to see the object has the default shape, hence doesn't have any ivar. ```ruby require "bundler/inline" gemfile do gem "rails", path: "." gem "benchmark-ips" end require "active_support/all" Benchmark.ips do |x| x.report("String#html_safe") { "test".html_safe } end ``` Before: ``` ruby 3.5.0dev (2025-07-17T14:01:57Z master a46309d19a) +YJIT +PRISM [arm64-darwin24] Warming up -------------------------------------- String#html_safe 575.727k i/100ms Calculating ------------------------------------- String#html_safe 6.421M (± 1.6%) i/s (155.75 ns/i) - 32.241M in 5.022802s ``` After: ``` ruby 3.5.0dev (2025-07-17T14:01:57Z master a46309d19a) +YJIT +PRISM [arm64-darwin24] Warming up -------------------------------------- String#html_safe 1.070M i/100ms Calculating ------------------------------------- String#html_safe 12.470M (± 0.8%) i/s (80.19 ns/i) - 63.140M in 5.063698s ```
1 parent 2bb6b67 commit db27b67

File tree

1 file changed

+13
-10
lines changed

1 file changed

+13
-10
lines changed

activesupport/lib/active_support/core_ext/string/output_safety.rb

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,13 @@ def safe_concat(value)
6767
original_concat(value)
6868
end
6969

70-
def initialize(str = "")
71-
@html_safe = true
70+
def initialize(_str = "")
7271
super
7372
end
7473

7574
def initialize_copy(other)
7675
super
77-
@html_safe = other.html_safe?
76+
@html_unsafe = true unless other.html_safe?
7877
end
7978

8079
def concat(value)
@@ -116,7 +115,9 @@ def +(other)
116115
def *(_)
117116
new_string = super
118117
new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
119-
new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
118+
if @html_unsafe
119+
new_safe_buffer.instance_variable_set(:@html_unsafe, true)
120+
end
120121
new_safe_buffer
121122
end
122123

@@ -131,9 +132,9 @@ def %(args)
131132
self.class.new(super(escaped_args))
132133
end
133134

134-
attr_reader :html_safe
135-
alias_method :html_safe?, :html_safe
136-
remove_method :html_safe
135+
def html_safe?
136+
@html_unsafe.nil?
137+
end
137138

138139
def to_s
139140
self
@@ -159,7 +160,7 @@ def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
159160
end # end
160161
161162
def #{unsafe_method}!(*args) # def capitalize!(*args)
162-
@html_safe = false # @html_safe = false
163+
@html_unsafe = true # @html_unsafe = true
163164
super # super
164165
end # end
165166
EOT
@@ -180,7 +181,7 @@ def #{unsafe_method}(*args, &block) # def gsub(*args, &block)
180181
end # end
181182
182183
def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
183-
@html_safe = false # @html_safe = false
184+
@html_unsafe = true # @html_unsafe = false
184185
if block # if block
185186
super(*args) { |*params| # super(*args) { |*params|
186187
set_block_back_references(block, $~) # set_block_back_references(block, $~)
@@ -214,7 +215,9 @@ def set_block_back_references(block, match_data)
214215

215216
def string_into_safe_buffer(new_string, is_html_safe)
216217
new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
217-
new_safe_buffer.instance_variable_set :@html_safe, is_html_safe
218+
unless is_html_safe
219+
new_safe_buffer.instance_variable_set :@html_unsafe, true
220+
end
218221
new_safe_buffer
219222
end
220223
end

0 commit comments

Comments
 (0)