-
-
Notifications
You must be signed in to change notification settings - Fork 31.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Memory leak in Modules/sre_lib.h #67877
Comments
In Modules/sre_lib.h on line 882 [1], a block of memory is allocated. If SRE(match) function later terminates abruptly, either because of a signal or because subsequent memory allocation fails, this block is never released. [1] https://hg.python.org/cpython/file/c89f7c34e356/Modules/sre_lib.h#l882 |
There is maybe a bug. Can you show an example of regex and a text where the memory leak occurs? You can use the tracemalloc module to check if there is a memory leak. Or use sys.getcounts() if you compiled Python in debug mode. sre_lib.h is very complex, it uses the C instruction "goto" with regex bytecodes. "DO_JUMP(JUMP_REPEAT, jump_repeat, ctx->pattern+ctx->pattern[0]);" calls "goto entrace" to execute following bytecodes, but later it comes back after DO_JUMP() with the "jump_repeat:" label: https://hg.python.org/cpython/file/c89f7c34e356/Modules/sre_lib.h#l1180 |
Memory leak only happens if match operation terminates abruptly, e.g. because of SIGINT. In this case, DO_JUMP doesn't come back. |
Tracemalloc code: import re
import signal
import tracemalloc
class AlarmError(Exception):
pass
def handle_alarm(signal, frame):
raise AlarmError
signal.signal(signal.SIGALRM, handle_alarm)
s1 = tracemalloc.take_snapshot()
for _ in range(20):
try:
signal.alarm(1)
re.match('(?:a|a|(?=b)){1000}', 'a'*999)
raise RuntimeError
except AlarmError:
pass
s2 = tracemalloc.take_snapshot()
res = s2.compare_to(s1, 'lineno')
for e in res[:10]:
print(e) For me, it shows almost 3 MiB allocated in re.py. |
May be this patch helps. |
Oh cool, you wrote a script to reproduce the issue! And Serhiy wrote a patch, great! Great job guys. sre_clean_repeat_data.patch looks good to me. @serhiy: Can you try the example to ensure that it fixes the issue? If yes, go ahead! |
This patch doesn't fix the issue. The problem is that the list starting with state->repeat doesn't necessarily contains all repeat contexts that are allocated. Indeed, here [1] and here [2] repeat contexts are temporarily removed from the list. If the match procedure terminates abruptly, they are not added back. [1] https://hg.python.org/cpython/file/c89f7c34e356/Modules/sre_lib.h#l963 |
Try to allocate SRE_REPEAT on state's stack, the performance has not changed significantly. It passes the other tests, except this one (test_stack_overflow): |
PR11926 (closed) tried to allocate SRE_REPEAT on state's stack. PR12160 uses a memory pool, this solution doesn't mess up the code. 🔸For infrequent alloc/free scenes, it adds a small overhead: s = 'a'
p = re.compile(r'(a)?')
p.match(s) # <- measure this statement before patch: 316 ns +- 19 ns 🔸For very frequent alloc/free scenes, it brings a speedup: s = 200_000_000 * 'a'
p = re.compile(r'.*?(?:bb)+')
p.match(s) # <- measure this statement before patch: 7.16 sec 🔸I tested in a real case that use 17 patterns to process 100MB data: before patch: 27.09 sec |
My PR methods are suboptimal, so I closed them. The number of REPEAT can be counted when compiling a pattern, and allocate a It seem at any time, a REPEAT will only have one in active, so a Can the number of REPEAT be placed in |
This looks promising. Please, go ahead! You are free to add any fields to any opcodes. It may break some third-party code which generates compiled patterns from a sequence of opcodes, it the stability of this interface was not promised. And they will be broken in any case due to reorganizing of internal code (bpo-47152). |
Thank you Ma Lin for all your work. The fix changes interfaces of some internal functions which can be used in third-party code, and the bug occurs only in special circumstances, so it is not practical to backport it. |
If SRE(match) function terminates abruptly, either because of a signal or because memory allocation fails, allocated SRE_REPEAT blocks might be never released.
If SRE(match) function terminates abruptly, either because of a signal or because memory allocation fails, allocated SRE_REPEAT blocks might be never released.
#126840 and #126843 are two alternate PRs. They are based on my earlier patch, but extend it in different ways. #126840 maintains a double-linked list of all repeat contexts. #126843 maintains two single-linked lists of repeat contexts. #126843 looks simpler, but I am not sure about performance. Both PRs also add private method Example from #91404 (comment) which could not finish in a hour with previous attempt (#32283), now return the result instantly. re.findall(r'(?:(?:/[^\n]*)?\n)*/', '../utils/common":3}],15:[function(t,e,a){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],"/":[function(t,e,a){"use strict";var i={};(0,t("./lib/utils/common").assign)(i,t("./lib/deflate"),t("./lib/inflate"),t("./lib/zlib/constants")),e.exports=i},{"./lib/deflate":1,"./lib/inflate":2,"./lib/utils/common":3,"./lib/zlib/constants":6}]},{},[])("/")});\n') Example from #91404 (comment) also returns an expected result. m = re.match(r'((a)?)*', 'aaaaa')
assert m.span(0) == (0, 5)
assert m.span(1) == (5, 5) @wjssz, could you please take a look at these PRs? |
Maybe #126843 works. But it requires a deeper understanding of the hiberarchy of #126840 should works. But Below is my patch, it uses a doubly-linked list for used pool, a singly-linked list for unused pool. It puts released structs to unused pool, so that these structs can be reused. It's faster when malloc/free structs very frequently. My patch compare to #126840 (On Windows, MSVC2022, non-PGO release build):
IMO This GitHub account can't upload file, you can save the patch, and apply it to click to see my patch
|
Yes, the issue can be reproduced with signals, but it is less reliable. We need to use long running regexps to be sure that signal will be raised before the matching finished, and we cannot predict the place of interruption. With I was going to add a pool for freed SRE_REPEAT blocks later, as optimization. But it will not harm to add it now. On my computer, with optimized build, #126843 is ~10% slower than #126840 in regex_dna and your patch is ~20% faster than #126840 in regex_effbot. So I chose your patch. |
If SRE(match) function terminates abruptly, either because of a signal or because memory allocation fails, allocated SRE_REPEAT blocks might be never released. Co-authored-by: <wjssz@users.noreply.github.com>
…thonGH-126840) If SRE(match) function terminates abruptly, either because of a signal or because memory allocation fails, allocated SRE_REPEAT blocks might be never released. (cherry picked from commit 7538e7f) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: <wjssz@users.noreply.github.com>
Thank you @wjssz (aka @animalize) for fixing this old and complex issue. |
…126840) If SRE(match) function terminates abruptly, either because of a signal or because memory allocation fails, allocated SRE_REPEAT blocks might be never released. Co-authored-by: <wjssz@users.noreply.github.com>
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
Linked PRs
The text was updated successfully, but these errors were encountered: