Ruby Racing:

Challenging

Ruby Methods

Danielle Adams / RubyHACK 2017

       adamzdanielle

Ruby Racing:

Challenging

Ruby Methods

code at

runtime

static VALUE
rb_str_strip(VALUE str)
{
    char *start;
    long olen, loffset, roffset;
    rb_encoding *enc = STR_ENC_GET(str);

    RSTRING_GETMEM(str, start, olen);
    loffset = lstrip_offset(str, start, start+olen, enc);
    roffset = rstrip_offset(str, start+loffset, start+olen, enc);

    if (loffset <= 0 && roffset <= 0) return rb_str_dup(str);
    return rb_str_subseq(str, loffset, olen-loffset-roffset);
}
#strip

How do Ruby methods work?

code

# hello_ruby_hack.rb

def say_hello(name)
   puts "Hello, #{name}!"
end

say_hello("Ruby HACK")

tokens

require 'ripper'

file = File.read('hello_ruby_hack.rb')

# tokens are chunks of code
# and don't need to be executable

Ripper.tokenize(file)
=> ["def",
 " ",
 "say_hello",
 "(",
 "name",
 ")",
 "\n",
 "   ",
 "puts",
 " ",
 "\"",
 "Hello, ",
 "\#{",
 "name",
 "}",
 "!",
 "\"",
 "\n",
 "end",
 "\n",
 "\n",
 "say_hello",
 "(",
 "\"",
 "Ruby HACK",
 "\"",
 ")",
 "\n"]

AST

abstract syntax trees

require 'ripper'

file = File.read('hello_ruby_hack.rb')

# sexp is short for symbolic expression

Ripper.sexp(file)
=> [:program,
 [[:def,
   [:@ident, "say_hello", [1, 4]],
   [:paren, [:params, [[:@ident, "name", [1, 14]]], nil, nil, nil, nil, nil, nil]],
   [:bodystmt,
    [[:void_stmt],
     [:command,
      [:@ident, "puts", [1, 21]],
      [:args_add_block,
       [[:string_literal,
         [:string_content,
          [:@tstring_content, "Hello, ", [1, 27]],
          [:string_embexpr, [[:var_ref, [:@ident, "name", [1, 36]]]]],
          [:@tstring_content, "!", [1, 41]]]]],
       false]]],
    nil,
    nil,
    nil]],
  [:method_add_arg,
   [:fcall, [:@ident, "say_hello", [1, 50]]],
   [:arg_paren,
    [:args_add_block,
     [[:string_literal, [:string_content, [:@tstring_content, "Ruby HACK", [1, 61]]]]],
     false]]]]]

bytecode

file = File.read('hello_ruby_hack.rb')
instructions = RubyVM::InstructionSequence.compile(file)

# since Ruby 1.9, bytecode is read by VM
# instead of the traversing the AST

puts instructions.disassemble
== disasm: #<ISeq:<compiled>@<compiled>>================================
0000 trace            1                                               (   1)
0002 putspecialobject 1
0004 putobject        :say_hello
0006 putiseq          say_hello
0008 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0011 pop
0012 trace            1                                               (   5)
0014 putself
0015 putstring        "Ruby HACK"
0017 opt_send_without_block <callinfo!mid:say_hello, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0020 leave
== disasm: #<ISeq:say_hello@<compiled>>=================================
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] name<Arg>
0000 trace            8                                               (   1)
0002 trace            1                                               (   2)
0004 putself
0005 putobject        "Hello, "
0007 getlocal_OP__WC__0 2
0009 tostring
0010 putobject        "!"
0012 concatstrings    3
0014 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0017 trace            16                                              (   3)
0019 leave                                                            (   2)

bytecode

+

YARV

(This is only true for CRuby.)

speeding up

code

story time

What is Ruby Racing?

$ gem install ruby-racer
$
#flatten_race.rb

require 'ruby_racer'
require_relative 'flatten'
input = [ 1, [ 2, 3 ] ]
proc = Proc.new { |input| flatten(input) }
RubyRacer.go(:flatten, input, &proc)

demo

What makes code faster?

primitive

operations

are fast

# max with each enumerable

def max(array)
    # (...)
    array.each do |el|
	max = el if el > max
    # (...)
end
# max with while loop

def max(array)
    # (...)
    while i < arr.length
        if array[i] > max
   	    max = array[i]
    # (...)
end
# benchmark

iterations = 1000000
input = [ 1, 2, 3 ]
operation = el + el

		user	    total	real
with each:      0.2500	    0.2500	(0.251002)
with while:     0.2200	    0.2200	(0.226902)

array mutation

is costly

# array appending vs assignment

[] << 1

[nil][0] = 1

# => [1]
# benchmark

iterations = 1000000

		user	    total        real
with append:    0.1000	    0.1000	 (0.096824)
with assign:    0.0900	    0.0900	 (0.095280)

complex algorithms

are slow

dive into

source code

# ruby/array.c:4631

static VALUE
rb_ary_flatten(int argc, VALUE *argv, VALUE ary)
{
  int mod = 0, level = -1;
  VALUE result, lv;
  # (...)
  result = flatten(ary, level, &mod);
  OBJ_INFECT(result, ary);

  return result;
}
# ruby/array.c:4631

static VALUE
rb_ary_flatten(int argc, VALUE *argv, VALUE ary)
{
  int mod = 0, level = -1;
  VALUE result, lv;
  # (...)
  result = flatten(ary, level, &mod);
  OBJ_INFECT(result, ary);

  return result;
}

  result = flatten(ary, level, &mod);
# ruby/array.c:4508

static VALUE
flatten(VALUE ary, int level, int *modified)
{
    # (...)
    while (i < RARRAY_LEN(ary)) {
	if (level >= 0 &&
            RARRAY_LEN(stack) / 2 >= level) {
	    rb_ary_push(result, elt);
	    continue;
    # (...)
	else {
	    rb_ary_push(stack, ary);

# ruby/array.c:4508

static VALUE
flatten(VALUE ary, int level, int *modified)
{
    # (...)
    while (i < RARRAY_LEN(ary)) {
	if (level >= 0 &&
            RARRAY_LEN(stack) / 2 >= level) {
	    rb_ary_push(result, elt);
	    continue;
    # (...)
	else {
	    rb_ary_push(stack, ary);


if (level >= 0 &&
    RARRAY_LEN(stack) / 2 >= level) {
    rb_ary_push(result, elt);
    rb_ary_push(stack, ary);
# ruby/array.c:4395

static VALUE
rb_ary_uniq(VALUE ary)
{
    VALUE hash, uniq;
    # (...)
        if (RARRAY_LEN(ary) <= 1)
            return rb_ary_dup(ary);
	# (...)
	else {
            hash = ary_make_hash(ary);
            uniq = rb_hash_values(hash);
}
# ruby/array.c:4395

static VALUE
rb_ary_uniq(VALUE ary)
{
    VALUE hash, uniq;
    # (...)
        if (RARRAY_LEN(ary) <= 1)
            return rb_ary_dup(ary);
	# (...)
	else {
            hash = ary_make_hash(ary);
            uniq = rb_hash_values(hash);
}

        if (RARRAY_LEN(ary) <= 1)
            return rb_ary_dup(ary);
            hash = ary_make_hash(ary);
            uniq = rb_hash_values(hash);
# ruby/array.c:2530

VALUE
rb_ary_sort(VALUE ary)
{
    ary = rb_ary_dup(ary);
    rb_ary_sort_bang(ary);
    return ary;
}
# ruby/array.c:2530

VALUE
rb_ary_sort(VALUE ary)
{
    ary = rb_ary_dup(ary);
    rb_ary_sort_bang(ary);
    return ary;
}

    ary = rb_ary_dup(ary);
    rb_ary_sort_bang(ary);

There are a few factors that will affect performance if we are willing to sacrifice other methodologies.

performance != code quality

How do we define code quality?

  • readability
  • testability
  • scalability

What is Ruby best at?

  • good for beginners
  • lots of included libraries
  • lots of OSS libraries
  • easy to debug
  • easy to compile
  • frameworks
  • cool logo

I didn't work hard to make Ruby perfect for everyone, because you feel differently from me. No language is perfect for everyone. [...] The perfect language for Guido van Rossum is probably Python.

 

- Yukihiro "Matz" Matsumoto

(Guido van Rossum wrote Python.)

resources

PyPy

pypy.org

If you want your code to run faster, you should probably just use PyPy.

 

- Guido van Rossum

Ruby Under the Microscope

Pat Shaughnessy

Why?

Ruby is designed to make programmers happy.

 

- Matz

code: github.com/danielleadams/ruby-racer

slides: slides.com/danielleadams/ruby-racing-rh17

 

       adamzdanielle

thank you

Ruby Racing: Challenging Ruby Methods

By Danielle Adams

Ruby Racing: Challenging Ruby Methods

Slides for RubyHACK 2017

  • 1,706