Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Exploring Internal Ruby Through C Extensions

Exploring Internal Ruby Through C Extensions

You may have wondered how Ruby objects are represented in the CRuby code. Not really? I would say writing a C extension is a great way to explore and learn how CRuby handles different object types. This session will re-implement our own Hash class, explain basic types in the CRuby, compare performance between native Hash, pure C++ implementation, and the C extension version, and discuss memory layouts and consumption in Ruby. The audience will also become more comfortable with the CRuby code through this session. Experience with C is not required.

Emma Haruka Iwao

May 31, 2018
Tweet

More Decks by Emma Haruka Iwao

Other Decks in Technology

Transcript

  1. Hello! Emma Haruka Iwao (@Yuryu) Developer Advocate Lives in Tokyo

    (30%) Loves traveling, games, delicious food Chromium contributor
  2. Today's Talk Is About... - Hello, World of C Extension

    - Reimplementing Ruby Hash - Benchmark! - Conclusion
  3. 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
  4. Hello, World in C in Ruby #include <stdio.h> #include <ruby.h>

    void Init_helloworld() { printf("Hello, World!\n"); } require 'mkmf' extension_name = 'helloworld' dir_config extension_name create_makefile extension_name helloworld.c extconf.rb
  5. 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
  6. 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
  7. 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)
  8. 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
  9. 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); }
  10. 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 ??????
  11. 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); /* .... */
  12. 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); }
  13. 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, };
  14. 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<ruby_hash_t*>(p); for (auto& iter: *hash) { rb_gc_mark(iter.first); rb_gc_mark(iter.second); } }
  15. 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
  16. Ruby GC, Mark and Sweep Root Object Object Object Object

    Object Object Own Used Marked Root Object Object Object Free
  17. 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<ruby_hash_t*>(hash); }
  18. C Class Lifecycle new Class allocate #initialize GC sweep new/malloc

    initialize deallocate delete/free Ruby C extension C library
  19. 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; };
  20. 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...
  21. 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
  22. 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
  23. VALUE and Objects VALUE Least Significant Bit Fixnum (small Integer)

    MSB ~ LSB + 1 → FIXNUM RObject - flags - klass - numiv - …… Pointer VALUE Object
  24. 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
  25. Calculating Key Hash Values struct ValueHash { size_t operator()(const VALUE&

    x) const { switch (TYPE(x)) { case T_FIXNUM: return std::hash<long>()(FIX2LONG(x)); case T_SYMBOL: return std::hash<ID>()(SYM2ID(x)); case T_STRING: return std::hash<std::experimental::string_view>() (std::experimental::string_view(RSTRING_PTR(x), RSTRING_LEN(x))); } return std::hash<long>()(FIX2LONG(rb_funcall(x, rb_intern("hash"), 0))); } };
  26. 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
  27. 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
  28. 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
  29. 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
  30. Wow, Ruby is so fast! Pure C++ <= Ruby Hash

    < Ruby C++ Hash < Ruby Pure Hash
  31. 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 <main> SAMPLES (pct) FRAME 1008 (90.7%) Object#do_hash_bench 103 (9.3%) (garbage collection) 0 (0.0%) block (3 levels) in <main> ← Hash (Ruby) ← Hashcxx
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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