Ruby,
concurrencia y GC
explicado


RubyConf Argentina 2011
  • Investigación y desarrollo
  • MacRuby
  • Aprender cosas nuevas

yo

Matt Aimonetti

Lo siento

This presentation will be in English

Goal of this presentation:

Understand some of Ruby's design decisions and their consequences

Ruby ha evolucionado bastante desde 1993

muchos "sabores" de Ruby:

  • "C" Ruby (referencia)
  • JRuby (Java)
  • MacRuby (Objective-C)
  • Rubinius (~Ruby)
  • Maglev (GemStone)
  • IronRuby (.NET)
  • ...

Organización bien definida

Partes de la implementation de Ruby

concurrencia

Ejemplo:
simple client/server

cliente

class Client
  def query(id)
    Server.dispatch(self, id)
  end
  def reply(id, response)
    print "Response: #{id} | "
  end
end

Server implementation

module Server
  module_function
  def dispatch(client, id)
    client.reply(id, fake_response(id))
  end
  def fake_response(id)
    response = ""
    n = id.even? ? id+1 : id*999
    n.times do
      response << ("a".."z").to_a[rand(26)]
    end
    response
  end
end
10.times{|n| Client.new.query(n) }

# Response: 0 | Response: 1 | Response: 2 | Response: 3 | Response: 4 |
# Response: 5 | Response: 6 | Response: 7 | Response: 8 | Response: 9 |

Sequential responses

What's a thread?

Threaded server

Switch the Server#dispatch method from

def dispatch(client, id)
  client.reply(id, fake_response(id))
end

to:

def dispatch(client, id)
  Thread.new { client.reply(id, fake_response(id)) }
end

Client responses:

10.times{|n| Client.new.query(n) }
Thread.list.last.join

# Response: 0 | Response: 2 | Response: 1 | Response: 4 | Response: 6 | 
# Response: 8 | Response: 3 | Response: 7 | Response: 9 | Response: 5 |

Threaded responses

Threads aren't magical

A cpu can only execute 1 instruction at a time

Context switching

Ruby fair scheduler (1 CPU)

Green threads
vs
native threads

Cons of green threads

Pros of native threads

Cons of threads (in general)

Fibers/Continuations

"Light threads" with own stack and manual scheduling

Why aren't threads more popular with Ruby developers?

Reasons:

Why?

With a GIL

@array, threads = [], []
4.times do
  threads << Thread.new {
    (1..100_000).each {|n| @array << n}
  }
end
threads.each{|t| t.join }
puts @array.size
# => 400000

Without a GIL

Basic structures aren't safe

@array, threads = [], []
4.times do
  threads << Thread.new {
    (1..100_000).each {|n| @array << n}
  }
end
threads.each{|t| t.join }
puts @array.size
# => 335467
# running the code again will give you a different result

Removing GIL: unexpected behavior

Why a gil?

C extensions

Caso más común:

El uso de bibliotecas existentes (libxml, DB driver..)

Define objects

#include <ruby.h>
VALUE mNokogiri ;
VALUE mNokogiriXml ;
VALUE mNokogiriXmlSax ;

mNokogiri         = rb_define_module("Nokogiri");
mNokogiriXml      = rb_define_module_under(mNokogiri, "XML");
mNokogiriXmlSax   = rb_define_module_under(mNokogiriXml, "SAX");

rb_const_set(mNokogiri, rb_intern("LIBXML_ICONV_ENABLED"), Qfalse);
            

Expose C functions as Ruby methods

#include <ruby.h>
VALUE rConf = rb_define_module("RubyConf");
rb_define_singleton_method(rConf, "bonjour", c_bonjour, 0);

static VALUE c_bonjour(VALUE self) {
  return rb_str_new2("bonjour RubyConf!");
}
            

Tell Ruby how to manage memory

//boring C code example removed so you don't fall asleep.
// basically you set "GC hooks" called to clean C extension
// used memory.
            

C ext challenges made easier by the GIL

Why a gil?

Should we remove the GIL?

CPU bound
vs
IO bound

Does your app spend most of its time waiting for CPU cycles or IO?

Where is most of the time spent within a request?

If it is in the code execution (CPU bound)

If it is in IO land

Other ways to achieve concurrency

memory management

Object declaration
==
object allocation


100.times{ "RubyConf" } # 100.times{ String.new("RubyConf" }

Allocates 100 string objects

{"location" => "New Orleans"}

Allocates 1 hash and 3 strings

class Foo; end; Foo.new

Allocates 1 node, 2 classes, 1 object

Garbage Collection prior to Ruby 1.9.3

What if:
there are no available slots?

What if:
the Garbage Collector can't free slots?

Garbage Collector in 1.9.3

Lazy sweeping

What if:
the freelist is empty?

What if:
all slots are marked after a full scan?

Different types of GCs, in the case of C Ruby's:

Optimization tricks

Concrete effect on daily code

Generating the RDoc documentation takes about 80 seconds on my machine. 30% of that time is spent on GC.
Narihiro Nakamura
We burn 20% of our front-end CPU on garbage collection.
Evan Weaver, Twitter, Oct 2009

See for yourself using GC::Profiler & ObjectSpace.count_objects

GC::Profiler.enable
# your code
puts GC::Profiler.result
Index    Invoke Time(sec)       Use Size(byte)     Total Size(byte)         Total Object                    GC Time(ms)
    1               0.005               110600               393216                 9816         0.24300000000000016032
    2               0.006               110560               393216                 9816         0.40999999999999975353
    3               0.008               110680               393216                 9816         1.00400000000000133582
Total Object                    GC Time(ms)
        9816         0.24300000000000016032
        9816         0.40999999999999975353
        9816
GC.disable
ref = ObjectSpace.count_objects[:T_STRING]
10_000.times{|n| 'test' }
count = ObjectSpace.count_objects[:T_STRING] - ref
puts "#{count} new strings added to memory"
puts ObjectSpace.count_objects.inspect
"10026 new strings added to memory"
{:TOTAL=>24526, :FREE=>308, :T_OBJECT=>8, :T_CLASS=>478, 
:T_MODULE=>21, :T_FLOAT=>7, :T_STRING=>16295, :T_REGEXP=>24, 
:T_ARRAY=>985, :T_HASH=>16, :T_BIGNUM=>3, :T_FILE=>9, 
:T_DATA=>398, :T_MATCH=>108, :T_COMPLEX=>1, :T_NODE=>5846, 
:T_ICLASS=>19}

GC Stats Rack middleware

https://github.com/mattetti/GC-stats-middleware

GC run, previous cycle was 255 requests ago.
GC 40 invokes.
Index    Total Object                    GC Time(ms)
    1          101432        14.47700000000007314327
    2          101432        13.95699999999999718625
    3          101432        13.84699999999994268762
    4          101432        14.65799999999983782573
    5          101432        15.47099999999979047516
    6          101432        14.96900000000001007550
    7          101432        17.90399999999969793407
    8          101432        15.38599999999989975663
    9          101432        15.29500000000005854872
   10          101432        16.75899999999996836664
   11          101432        14.70199999999977080734

[60%] 14414 freed strings.
[12%] 2927 freed arrays.
[9%] 2268 freed big numbers.
[2%] 564 freed hashes.
[1%] 373 freed objects.
[5%] 1351 freed parser nodes (eval usage).

Lessons:

Preguntas?


Slides: http://rubyconfar.merbist.com/

Twitter: @merbist

Blog: http://merbist.com