В Ruby Regexp происходит утечка памяти? - PullRequest
0 голосов
/ 05 июня 2019

У меня есть код с утечкой памяти в приложении 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 или я что-то не так делаю?

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

Основная проблема здесь заключалась в том, что у нас произошел сбой контейнеров, теоретически из-за использования памяти, но, возможно, это не утечка памяти, а простоне хватает памяти, чтобы позволить Ruby потреблять то, что он хочет?Есть ли настройки для GC, чтобы помочь ему решить, когда пора очищать, прежде чем 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

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

Ответы [ 2 ]

1 голос
/ 17 июня 2019

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

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

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

1 голос
/ 06 июня 2019

GC уничтожает неиспользуемые объекты и освобождает память для процесса Ruby, но процесс Ruby никогда не освобождает эту память для ОС . Но это , а не , то же самое, что утечка памяти (потому что в обычных условиях в какой-то момент процессу Ruby выделяется достаточно памяти, и он больше не растет - очень грубо говоря). Утечки памяти происходят , когда GC не может освободить память (из-за ошибок, плохого кода и т. Д.), А процесс 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_profiler).

...