Skip to content

Commit 340f024

Browse files
authored
PCRE2: use thread local for jit stack and match data (#16175)
We can reuse the same JIT stack and matchdata for all instances. There's no need for a specific match data per `Regex`: We just allocate one and make sure it's not used by other threads. The drawback is that we must allocate each matchdata with a maximum number of ovectors (65535). That might increase memory usage, though I failed to notice it in practice. Maybe not allocating separate memory for every regular expression is helping?
1 parent 5427e98 commit 340f024

File tree

3 files changed

+42
-27
lines changed

3 files changed

+42
-27
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# :nodoc:
2+
class Crystal::ValueWithFinalizer(T)
3+
getter value : T
4+
5+
def initialize(@value : T, @finalizer : T ->)
6+
end
7+
8+
def finalize
9+
@finalizer.call(@value)
10+
end
11+
end

src/regex/lib_pcre2.cr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,12 @@ lib LibPCRE2
264264

265265
fun jit_stack_create = pcre2_jit_stack_create_8(startsize : LibC::SizeT, maxsize : LibC::SizeT, gcontext : GeneralContext*) : JITStack*
266266
fun jit_stack_assign = pcre2_jit_stack_assign_8(mcontext : MatchContext*, callable_function : Void* -> JITStack*, callable_data : Void*) : Void
267+
fun jit_stack_free = pcre2_jit_stack_free_8(jit_stack : JITStack*) : Void
267268

268269
fun pattern_info = pcre2_pattern_info_8(code : Code*, what : UInt32, where : Void*) : Int
269270

270271
fun match = pcre2_match_8(code : Code*, subject : UInt8*, length : LibC::SizeT, startoffset : LibC::SizeT, options : UInt32, match_data : MatchData*, mcontext : MatchContext*) : Int
272+
fun match_data_create = pcre2_match_data_create_8(ovecsize : UInt32, gcontext : GeneralContext*) : MatchData*
271273
fun match_data_create_from_pattern = pcre2_match_data_create_from_pattern_8(code : Code*, gcontext : GeneralContext*) : MatchData*
272274
fun match_data_free = pcre2_match_data_free_8(match_data : MatchData*) : Void
273275

src/regex/pcre2.cr

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require "./lib_pcre2"
2-
require "crystal/thread_local_value"
2+
require "crystal/value_with_finalizer"
33

44
# :nodoc:
55
module Regex::PCRE2
@@ -207,12 +207,14 @@ module Regex::PCRE2
207207
private def match_impl(str, byte_index, options)
208208
match_data = match_data(str, byte_index, options) || return
209209

210-
ovector_count = LibPCRE2.get_ovector_count(match_data)
210+
# We reuse the same `match_data` allocation, so we must reimplement the
211+
# behavior of pcre2_match_data_create_from_pattern (get_ovector_count always
212+
# returns 65535, aka the maximum).
213+
ovector_count = capture_count_impl &+ 1
211214
ovector = Slice.new(LibPCRE2.get_ovector_pointer(match_data), ovector_count &* 2)
212215

213216
# We need to dup the ovector because `match_data` is re-used for subsequent
214-
# matches (see `@match_data`).
215-
# Dup brings the ovector data into the realm of the GC.
217+
# matches. We only dup the match data (not everything).
216218
ovector = ovector.dup
217219

218220
::Regex::MatchData.new(self, @re, str, byte_index, ovector.to_unsafe, ovector_count.to_i32 &- 1)
@@ -228,43 +230,43 @@ module Regex::PCRE2
228230

229231
class_getter match_context : LibPCRE2::MatchContext* do
230232
match_context = LibPCRE2.match_context_create(nil)
231-
LibPCRE2.jit_stack_assign(match_context, ->(_data) { Regex::PCRE2.jit_stack }, nil)
233+
LibPCRE2.jit_stack_assign(match_context, ->(_data) { current_jit_stack.value }, nil)
232234
match_context
233235
end
234236

235-
# Returns a JIT stack that's shared in the current thread.
237+
# JIT stack is unique per thread.
236238
#
237-
# Only a single `match` function can run per thread at any given time, so there
238-
# can't be any concurrent access to the JIT stack.
239-
@@jit_stack = Crystal::ThreadLocalValue(LibPCRE2::JITStack*).new
240-
241-
def self.jit_stack
242-
@@jit_stack.get do
243-
LibPCRE2.jit_stack_create(32_768, 1_048_576, nil) || raise "Error allocating JIT stack"
244-
end
239+
# Only a single `match` function can run per thread at any given time, so
240+
# there can't be any concurrent access to the JIT stack.
241+
thread_local(current_jit_stack : ::Crystal::ValueWithFinalizer(::LibPCRE2::JITStack*)) do
242+
ptr = LibPCRE2.jit_stack_create(32_768, 1_048_576, nil)
243+
raise RuntimeError.new("Error allocating JIT stack") if ptr.null?
244+
::Crystal::ValueWithFinalizer.new(ptr, ->(value : ::LibPCRE2::JITStack*) { LibPCRE2.jit_stack_free(value) })
245245
end
246246

247-
# Match data is shared per instance and thread.
247+
# Match data is unique per thread.
248248
#
249-
# Match data contains a buffer for backtracking when matching in interpreted mode (non-JIT).
250-
# This buffer is heap-allocated and should be re-used for subsequent matches.
251-
@match_data = Crystal::ThreadLocalValue(LibPCRE2::MatchData*).new
252-
253-
private def match_data
254-
@match_data.get do
255-
LibPCRE2.match_data_create_from_pattern(@re, nil)
256-
end
249+
# Match data contains a buffer for backtracking when matching in interpreted
250+
# mode (non-JIT). This buffer is heap-allocated and should be re-used for
251+
# subsequent matches.
252+
#
253+
# Only a single `match` function can run per thread at any given time, so
254+
# there can't be any concurrent access to the match data buffer.
255+
thread_local(current_match_data : ::Crystal::ValueWithFinalizer(::LibPCRE2::MatchData*)) do
256+
# The ovector size is clamped to 65535 pairs; we declare the maximum because
257+
# we allocate the match data buffer once for the thread and need to adapt to
258+
# any regular expression.
259+
ptr = LibPCRE2.match_data_create(65_535, nil)
260+
raise RuntimeError.new("Error allocating match data") if ptr.null?
261+
::Crystal::ValueWithFinalizer.new(ptr, ->(value : LibPCRE2::MatchData*) { LibPCRE2.match_data_free(value) })
257262
end
258263

259264
def finalize
260-
@match_data.consume_each do |match_data|
261-
LibPCRE2.match_data_free(match_data)
262-
end
263265
LibPCRE2.code_free @re
264266
end
265267

266268
private def match_data(str, byte_index, options)
267-
match_data = self.match_data
269+
match_data = Regex::PCRE2.current_match_data.value
268270
match_count = LibPCRE2.match(@re, str, str.bytesize, byte_index, pcre2_match_options(options), match_data, PCRE2.match_context)
269271

270272
if match_count < 0

0 commit comments

Comments
 (0)