Slide 1

Slide 1 text

Exploring Internal Ruby Through C Extensions Emma Haruka Iwao (@Yuryu)

Slide 2

Slide 2 text

Hello! Emma Haruka Iwao (@Yuryu) Developer Advocate Lives in Tokyo (30%) Loves traveling, games, delicious food Chromium contributor

Slide 3

Slide 3 text

Today's Talk Is About... - Hello, World of C Extension - Reimplementing Ruby Hash - Benchmark! - Conclusion

Slide 4

Slide 4 text

What Are C Extensions? Implementing Ruby interface in C Almost full access to Ruby code and data Full access to OS and hardware (because it's C!) Possible to use C++ And of course, with the ability to shoot yourself in the foot

Slide 5

Slide 5 text

printf("Hello, World\n"); in Ruby

Slide 6

Slide 6 text

Hello, World in C in Ruby #include #include void Init_helloworld() { printf("Hello, World!\n"); } require 'mkmf' extension_name = 'helloworld' dir_config extension_name create_makefile extension_name helloworld.c extconf.rb

Slide 7

Slide 7 text

Hello, World in Action $ ruby extconf.rb creating Makefile $ make compiling helloworld.c linking shared-object helloworld.so $ irb irb(main):001:0> require './helloworld' Hello, World! => true

Slide 8

Slide 8 text

The Init Function Executed when the .so library is loaded Used to register the library to Ruby Not to be confused with #initialize Init is not executed on new require Init_xxx

Slide 9

Slide 9 text

Reimplementing Ruby Hash

Slide 10

Slide 10 text

Reimplementing Hash Hash is a good CS exercise to play with Re-implemented and compared three "Hash" + one Hash - The standard, Ruby hash Hashcxx - Implemented in C++, using unordered_map Hashruby - Implemented in Ruby, using Array Purecxx - Benchmark in C++ for comparison (w/o Ruby)

Slide 11

Slide 11 text

Designing Hashcxx Thin wrapper of C++ unordered_map (C++ hash) Implements fundamental methods: #[], #[]=, #each, #key? Specializes in Integer (Fixnum), Symbol, String But it works with any Object

Slide 12

Slide 12 text

Init_hashcxx void Init_hashcxx() { VALUE cHash = rb_define_class("Hashcxx", rb_cData); rb_include_module(cHash, rb_mEnumerable); rb_define_alloc_func(cHash, allocate); rb_define_method(cHash, "initialize", initialize, 0); rb_define_method(cHash, "[]=", setter, 2); rb_define_method(cHash, "[]", getter, 1); rb_define_method(cHash, "each", each, 0); rb_define_alias(cHash, "each_pair", "each"); rb_define_method(cHash, "key?", has_key, 1); rb_define_alias(cHash, "has_key?", "key?"); rb_define_method(cHash, "empty?", empty, 0); }

Slide 13

Slide 13 text

Pretty Straightforward, heh? C function Ruby notation rb_define_class class rb_include_module include rb_define_method def rb_define_alias alias rb_define_alloc_func ??????

Slide 14

Slide 14 text

hash.c, the CRuby Hash Source void Init_Hash(void) { /* ... */ rb_cHash = rb_define_class("Hash", rb_cObject); rb_include_module(rb_cHash, rb_mEnumerable); rb_define_alloc_func(rb_cHash, empty_hash_alloc); rb_define_singleton_method(rb_cHash, "[]", rb_hash_s_create, -1); rb_define_singleton_method(rb_cHash, "try_convert", rb_hash_s_try_convert, 1); rb_define_method(rb_cHash, "initialize", rb_hash_initialize, -1); rb_define_method(rb_cHash, "initialize_copy", rb_hash_initialize_copy, 1); rb_define_method(rb_cHash, "rehash", rb_hash_rehash, 0); /* .... */

Slide 15

Slide 15 text

rb_define_alloc_func Yes, memory management! Called to new (malloc) in the C world TypedData_Wrap_Struct associates the class and the allocated C memory VALUE allocate(VALUE klass) { ruby_hash_t* value = new ruby_hash_t(); return TypedData_Wrap_Struct(klass, &hashcxx_type, value); }

Slide 16

Slide 16 text

Type Definition Help Ruby understand what the C type is like Importantly, mark and deallocate const rb_data_type_t hashcxx_type = { "Hashcxx_type", {mark, deallocate, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, };

Slide 17

Slide 17 text

Mark Tell garbage collector which objects you use Ruby takes care of object lifecycle. C doesn't If you still need objects, mark them in use void mark(void* p) { auto* hash = reinterpret_cast(p); for (auto& iter: *hash) { rb_gc_mark(iter.first); rb_gc_mark(iter.second); } }

Slide 18

Slide 18 text

Garbage Collection in CRuby Mark and Sweep If you don't mark, Ruby releases the memory Nil starts to appear in random places Might crash in the worst case

Slide 19

Slide 19 text

Ruby GC, Mark and Sweep Root Object Object Object Object Object Object Own Used Marked Root Object Object Object Free

Slide 20

Slide 20 text

Deallocate Release the C memory Again, C doesn't take care of object lifecycle Called by Ruby when the object is released void deallocate(void* hash) { delete reinterpret_cast(hash); }

Slide 21

Slide 21 text

C Class Lifecycle new Class allocate #initialize GC sweep new/malloc initialize deallocate delete/free Ruby C extension C library

Slide 22

Slide 22 text

Get Associated Data in C Use TypedData_Get_Struct In this example, we get a pointer to a C++ std::unordered_map ruby_hash_t* get_hash(VALUE self) { ruby_hash_t* hash; TypedData_Get_Struct(self, ruby_hash_t, &hashcxx_type, hash); return hash; };

Slide 23

Slide 23 text

Basic Types in CRuby VALUE Alias of long, 64 bit integer (on 64 bit Linux) RBasic Struct with flags and class Builtin classes RObject, RString, RArray, RRegexp, etc...

Slide 24

Slide 24 text

Every Ruby Variable in C Is a VALUE VALUE initialize(VALUE self) { return self; } VALUE setter(VALUE self, VALUE key, VALUE value) { auto* hash = get_hash(self); (*hash)[key] = value; return value; } def initialize end def []=(key, value) @cxx_hash[key] = value value end

Slide 25

Slide 25 text

What Is the VALUE Type? A pointer to a Ruby object Special handling for Nil, True, False, Integer, etc Constant / Type Value False 0x00 True 0x14 Nil 0x08 Fixnum 0x01 (mask) …. xxx1 Symbol 0x0c (mask) 00001100

Slide 26

Slide 26 text

VALUE and Objects VALUE Least Significant Bit Fixnum (small Integer) MSB ~ LSB + 1 → FIXNUM RObject - flags - klass - numiv - …… Pointer VALUE Object

Slide 27

Slide 27 text

Special Variables Constant Value False 0x00 True 0x14 Nil 0x08 Heap (user allocated data) is not located at lower addresses Reserved Code Heap (Data) OS Kernel Typical memory space 0 0xffff ffff ffff ffff

Slide 28

Slide 28 text

Calculating Key Hash Values struct ValueHash { size_t operator()(const VALUE& x) const { switch (TYPE(x)) { case T_FIXNUM: return std::hash()(FIX2LONG(x)); case T_SYMBOL: return std::hash()(SYM2ID(x)); case T_STRING: return std::hash() (std::experimental::string_view(RSTRING_PTR(x), RSTRING_LEN(x))); } return std::hash()(FIX2LONG(rb_funcall(x, rb_intern("hash"), 0))); } };

Slide 29

Slide 29 text

Benchmark

Slide 30

Slide 30 text

My Own Hash Implementation in Ruby class Hashruby def initialize @table = Array.new(DEFAULT_CAPACITY) @length = 0 end def []=(key, value) if (@length + 1).to_f / @table.length > MAX_LOAD_FACTOR rehash(@table.length * 2) end @length += insert_(@table, key, value) value end

Slide 31

Slide 31 text

My Own Hash Implementation in Ruby - insert def insert_(table, key, value) h = rhash_(table.length, key) until table[h].nil? if table[h][0].eql? key table[h][1] = value return 0 end h += 1 end table[h] = [key, value] return 1 end

Slide 32

Slide 32 text

Benchmark 1. Initialize Random with a seed 2. Generate random numbers, store them to hash 3. Set the seed to the initial value 4. Use the same numbers, fetch and verify (Also the same with String) Machine n1-standard-8 on Google Compute Engine CPU Intel Xeon, Broadwell Generation OS Debian 9.4 (stretch) Ruby 2.5.1-p57 (r 63029) C Compiler GCC 6.3.0-18+deb9u1

Slide 33

Slide 33 text

Guess Which Is the Fastest? 1. Hash - The standard, Ruby hash 2. Hashcxx - Implemented in C++, using unordered_map 3. Hashruby - Implemented in Ruby, using Array 4. Purecxx - Benchmark in C++ for comparison (not Ruby) Tested with 1,000,000 Integers and 100,000 Strings Each String has 20 characters Memory consumption is measured with Valgrind Ruby memory usage counts CRuby itself

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

Wow, Ruby is so fast! Pure C++ <= Ruby Hash < Ruby C++ Hash < Ruby Pure Hash

Slide 36

Slide 36 text

But Why is Hashcxx Slower Than Hash? Garbage collection is faster with Bultin objects (I think I implemented write barrier correctly) SAMPLES (pct) FRAME 1435 (76.1%) Object#do_hash_bench 451 (23.9%) (garbage collection) 0 (0.0%) block (3 levels) in SAMPLES (pct) FRAME 1008 (90.7%) Object#do_hash_bench 103 (9.3%) (garbage collection) 0 (0.0%) block (3 levels) in ← Hash (Ruby) ← Hashcxx

Slide 37

Slide 37 text

How about String as keys?

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

Whoa, Ruby Is So Slow! 70% of the time is spent on generating random strings Because my implementation sucks? def gen_random_string(random, len) s = String.new(capacity: len) len.times do s << (65 + random.rand(26)) end s end SAMPLES (pct) FRAME 1430 (69.1%) Object#gen_random_string 504 (24.4%) (garbage collection) 134 (6.5%) Object#do_hash_string_bench

Slide 40

Slide 40 text

String Vs std::string String (Ruby) is not the same as std::string (C++) Ruby String has a lot more features C++ std::string is an byte array Ruby String recognizes codepoints The Ruby and C++ programs are not exactly the same

Slide 41

Slide 41 text

Concluding

Slide 42

Slide 42 text

tl;dr Easy enough once you know how to define a class Hello, World is just a few lines of code Significant part of CRuby is actually written in the same way Great to way to read the code as you already know how the classes behave Not necessarily faster than Ruby

Slide 43

Slide 43 text

When to Write C Extensions? Want better performance? - think twice Restrictions with Garbage Collection Left out from Ruby future improvements Use to make an interface for an external library Don't implement everything in a C extension Develop a library and wrap it

Slide 44

Slide 44 text

https://twitter.com/tenderlove/status/905248150323060736

Slide 45

Slide 45 text

Want to Learn More? "Ruby Under a Microscope: An Illustrated Guide to Ruby Internals" is the definitive guide http://patshaughnessy.net/ruby-under-a-microscope "Rubyソースコード完全解説" is great and available online! http://i.loveruby.net/ja/rhg/book/ "Incremental GC for Ruby interpreter" for the latest GC info http://www.atdot.net/~ko1/activities/2014_rubyconf_pub.pdf I couldn't have done this talk without these resources

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

Benchmark - Integer Implementation Time (ms) Memory (MB) Hash 1,056 99.14 Hashcxx 1,420 114.8 Hashruby 3,144 221.9 Purecxx 1,085 99.69