4

Depending on how I generate a string Ruby will release the memory to the OS or it won't. The first test code will take up about 235MB

size = 2**22
string = '!@#$%^&*()-+~`a1234567890abcdefghijklmnopqrstuvwxyz' * size
puts 'Sleeping...'
sleep(5)
string = nil
GC.start
puts 'Just sitting here..'
gets.chomp

After GC.start is called, memory used by the test will shrink back to a few kilobytes. But if I run the same test with string = (0...size).map { (65 + rand(26)).chr }.join, memory will shoot up to 250MB and memory use will actually increase to 290MB after calling GC.start.

EDIT: I'm using Ruby 1.9.3-p448 as the project I'm working on requires it. Although I'll test it on Ruby 2.2 and come back with results.

EDIT 2: Running the test code in Ruby 2.1 (Ruby 2.2 wasn't available in RVM and I just wanted to run the test quickly) gave similar results. Memory still didn't decrease to a reasonable state. It went from 234MB BGCs (before GC.start) to 197MB AGCs. Note: the memory sizes were different because I ran it on a different machine but the specific sizes don't matter just the relative increases and decreases (or non-decreases).

segfault.py
  • 133
  • 1
  • 8
  • Which Ruby are you using? Be sure to try it on current mainstream Ruby (2.2 MRI) and see if you get the same result. – joelparkerhenderson Dec 26 '14 at 20:40
  • The project I'm working on requires Ruby 1.9.3. I'll test it on 2.2 to see if I get the same result though. Increasing the size of the string (to say 2**23) increases the initial overall memory used but in that case Ruby will go from 604MB before `GC.start` to 521MB after `GC.start`. In that case the result is closer to what we expect but still... 521MB of memory taken up with references to nothing. – segfault.py Dec 26 '14 at 21:19
  • If I recall correctly, the Ruby 1.9.3 MRI GC doesn't release RAM back to the OS if it's unrequired. In other words, if you allocate 500MB of strings, then GC, then Ruby will know the RAM is resuasble, but the OS won't. You can try it by looping around your string creation and GC; my guess is the first loop will use the 500MB, then subsequent loops will re-use the same 500MB. Ruby GC improves a lot in MRI 2.0, 2.1, 2.2. – joelparkerhenderson Dec 26 '14 at 21:23
  • Updated post with new test. – segfault.py Dec 26 '14 at 21:52
  • 1
    Tested it on Ruby 2.2.0 and it has the same problem. – segfault.py Dec 26 '14 at 22:02

1 Answers1

2

Ruby MRI does not release memory back to the OS.

Here's what I see with Ruby MRI 2.2 on OSX 10.10, using typical ps -o rss:

  • Allocating the big string by using * uses ~220MB.

  • Allocating the big string by using map uses ~340MB.

On my system, the GC.start doesn't do anything to the RSS. In other words, I see the RAM usage stay the same.

Notably, the map is using a lot of RAM:

  • (0...size).map{ '' } uses ~300MB.

When I loop your examples, something interesting emerges:

  • Allocating the big string by using * continues to use the same RAM, i.e. RSS doesn't change much.

  • Allocating the big string by using map grows by ~40M per loop.

  • Doing just (0...size).map{ '' } grows by ~40M per loop.

This shows me that Ruby map may have a RAM-related issue. It's not exactly a problem, because Ruby isn't raising NoMemoryException, but does seem to be a non-optimal use of RAM.

joelparkerhenderson
  • 34,808
  • 19
  • 98
  • 119