Интерполяция Regexp в Ruby приводит к утечке памяти?

У меня есть код с утечкой памяти в приложении Sinatra на Ruby 2.4.4, и я могу воспроизвести его в irb, хотя он не совсем стабилен, и мне интересно, есть ли у других такая же проблема. Это происходит при интерполяции большой строки внутри литерала регулярного выражения:

class Leak
  STR = "RANDOM|STUFF|HERE|UNTIL|YOU|GET|TIRED|OF|TYPING|AND|ARE|SATISFIED|THAT|IT|WILL|LEAK|ENOUGH|MEMORY|TO|NOTICE"*100

  def test
    100.times { /#{STR}/i }
  end
end

t = Leak.new
t.test # If I run this a few times, it will start leaking about 5MB each time

Теперь, если я запускаю GC.start после этого, он обычно очищает последние 5 МБ (или столько, сколько он использовал), а затем t.test будет использовать только несколько КБ, затем почти МБ, затем пару МБ, а затем обратно 5 МБ каждый раз, и снова GC.start соберет только последние 5.

Альтернативный способ получить тот же результат без утечки памяти — заменить /#{STR}/i на RegExp.new(STR, true). Кажется, это хорошо работает для меня.

Является ли это законной утечкой памяти в Ruby или я делаю что-то не так?

ОБНОВЛЕНИЕ: Хорошо, может быть, я неправильно понял это. Я смотрел на использование памяти док-контейнера после запуска GC.start, который иногда падал, но поскольку Ruby не всегда освобождает память, которую он не использует, я думаю, что это может быть просто то, что Ruby использует эту память, а затем, даже если она не сохраняется, она все равно не освобождает память обратно в ОС. Используя гем MemoryProfiler, я вижу, что total_retained, даже после его запуска несколько раз, равен 0.

Основная проблема здесь заключалась в том, что у нас были сбои контейнеров, теоретически из-за использования памяти, но, возможно, это не утечка памяти, а просто нехватка памяти, чтобы позволить Ruby потреблять то, что он хочет? Существуют ли настройки для сборщика мусора, которые помогут ему решить, когда пора очиститься до того, как Ruby исчерпает память и выйдет из строя?

ОБНОВЛЕНИЕ 2: это по-прежнему не имеет смысла, потому что, почему Ruby продолжает выделять все больше и больше памяти только из-за многократного запуска одного и того же процесса (почему он не использует ранее выделенную память) ? Насколько я понимаю, GC предназначен для запуска хотя бы один раз, прежде чем выделять больше памяти из ОС, так почему же Ruby просто выделяет все больше и больше памяти, когда я запускаю это несколько раз?

ОБНОВЛЕНИЕ 3: в моем изолированном тесте Ruby, кажется, приближается к пределу, при котором он перестает выделять дополнительную память, независимо от того, сколько раз я запускаю тест (обычно это около 120 МБ), но в моем производстве код, я еще не достиг такого предела (он превышает 500 МБ без замедления - возможно, потому, что по классу разбросано больше случаев такого использования памяти). Может быть ограничение на объем памяти, который он будет использовать, но, похоже, он во много раз выше, чем можно было бы ожидать для запуска этого кода (который на самом деле использует только дюжину или около того МБ для одного запуска)

Обновление 4: я сузил тестовый пример до того, что действительно дает утечки! Чтение многобайтового символа из файла было ключом к воспроизведению реальной проблемы:

str = "String that doesn't fit into a single RVALUE, with a multibyte char:" + 160.chr(Encoding::UTF_8)
File.write('weirdstring.txt', str)

class Leak
  PATTERN = File.read("weirdstring.txt").freeze

  def test
    10000.times { /#{PATTERN}/i }
  end
end

t = Leak.new

loop do
  print "Running... "

  t.test


  # If this doesn't work on your system, just comment these lines out and watch the memory usage of the process with top or something
  mem = %x[echo 0 $(awk '/Private/ {print "+", $2}' /proc/`pidof ruby`/smaps) | bc].chomp.to_i
  puts "process memory: #{mem}"
end

Итак... это настоящая утечка, верно?


person mltsy    schedule 05.06.2019    source источник
comment
В версии 2.4.4 могут быть ошибки, они сохраняются в версии 2.6.3?   -  person tadman    schedule 05.06.2019
comment
Похоже, та же проблема, хотя, если я использую MemoryProfiler, память не сохраняется ... обновление сообщения ....   -  person mltsy    schedule 05.06.2019
comment
Сборщик мусора срабатывает только тогда, когда считает, что это необходимо. Может быть, он не считает эти распределения достаточно серьезными, чтобы их можно было очистить.   -  person tadman    schedule 05.06.2019


Ответы (2)


Сборщик мусора уничтожает неиспользуемые объекты и освобождает память для процесса Ruby, но процесс Ruby никогда не освобождает эту память для ОС. Но это не то же самое, что утечка памяти (поскольку при нормальных обстоятельствах в какой-то момент процесс Ruby имеет достаточно выделенной памяти и больше не растет - очень грубо говоря). Утечки памяти происходят, когда сборщик мусора не может освободить память (из-за ошибок, плохого кода и т. д.), и процессу Ruby приходится занимать все больше и больше памяти.

Это не относится к вашему коду — он не содержит утечек памяти, но есть проблемы с эффективностью.

Что происходит, когда вы делаете 100.times { /#{STR}/i }, так это то, что вы

  1. Создайте 100 очень длинных строк (при интерполяции константы в литерал шаблона)...

  2. ... а затем создайте 100 регулярных выражений из этих строк.

Все это требует ненужных выделений, из-за чего процесс Ruby использует больше памяти (и также снижает производительность - GC довольно дорог). Изменение определения класса на

class Leak
  PAT = /"RANDOM|STUFF|HERE|UNTIL|YOU|GET|TIRED|OF|TYPING|AND|ARE|SATISFIED|THAT|IT|WILL|LEAK|ENOUGH|MEMORY|TO|NOTICE"*100/i

  def test
    100.times { PAT }
  end
end

(например, запоминать не саму строку, а шаблон, созданный из нее как константу, а затем повторно использовать его) уменьшает выделение памяти во время одного и того же вызова test как классом String, так и классом Regexp по порядку (согласно отчету memory_profilers).

person Konstantin Strukov    schedule 06.06.2019
comment
Итак... Я понимаю, что Ruby не освобождает память для ОС, но это не объясняет, почему запуск 100.times { /#{STR}/i } снова и снова продолжает использовать все больше и больше памяти, верно? Даже после первой интерполяции интерполированная строка отбрасывается, что должно освободить достаточно памяти для второй. Или, по крайней мере, до того, как из ОС будет выделено больше памяти, Ruby должен освободить все эти старые неиспользуемые слоты, освободив место для следующих 100.times, верно?? Но запуск его снова и снова потребляет все больше и больше и больше памяти, что для меня не имеет смысла ...? - person mltsy; 07.06.2019

Это была утечка памяти!

https://bugs.ruby-lang.org/issues/15916

Должно быть исправлено в одном из следующих выпусков Ruby (2.6.4 или 2.6.5?)

person mltsy    schedule 17.06.2019