Slide 1

Slide 1 text

John Hawthorn - RubyConf mini 2022 Making is_a? Fast

Slide 2

Slide 2 text

• Ruby Core Team • Rails Core Team • Sta ff Engineer @ GitHub John Hawthorn @jhawthorn @[email protected] he/him

Slide 3

Slide 3 text

is_a? "Is this object of this class/module"

Slide 4

Slide 4 text

"foo".is_a?(String) => true "foo".is_a?(Integer) => false 123.is_a?(Integer) => true 123.is_a?(Numeric) => true

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

is_a?(Pigeon) Making is_a? Fast #

Slide 7

Slide 7 text

String === x x.is_a?(String)

Slide 8

Slide 8 text

• kind_of? • Module#=== • case statement • rescue statement • protected methods What is "is_a?" is_a?("is_a?")

Slide 9

Slide 9 text

2% of total Ruby runtime* is_a? * In Rails apps I measured, your mileage may vary

Slide 10

Slide 10 text

Profiling Ruby stackprof or rbspy

Slide 11

Slide 11 text

$ stackprof benchmarks/railsbench/out.bench ================================== Mode: wall(1000) Samples: 32453 (0.00% miss rate) GC: 3699 (11.40%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 1646 (5.1%) 1646 (5.1%) String#gsub! 2295 (7.1%) 698 (2.2%) Class#new 1375 (4.2%) 654 (2.0%) Jbuilder#_set_value 1139 (3.5%) 637 (2.0%) ActiveModel::Type::Hel 565 (1.7%) 565 (1.7%) Module#=== 554 (1.7%) 554 (1.7%) JSON::Ext::Generator::

Slide 12

Slide 12 text

$ stackprof redacted_2022-11-02-14-23-11.stackprof.json ================================== Mode: wall(200) Samples: 64246 (4.57% miss rate) GC: 0 (0.00%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 4725 (7.4%) 4725 (7.4%) IO#read 14203 (22.1%) 3278 (5.1%) Class#new 2513 (3.9%) 2513 (3.9%) Kernel#is_a? 2380 (3.7%) 2307 (3.6%) Trilogy#query

Slide 13

Slide 13 text

Profiling C perf https://www.brendangregg.com/perf.html https://perf.wiki.kernel.org/index.php/Main_Page

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

rb_define_method(rb_mKernel, "kind_of?", rb_obj_is_kind_of, 1); rb_define_method(rb_mKernel, "is_a?", rb_obj_is_kind_of, 1); rb_obj_is_kind_of src/ruby/object.c VALUE rb_obj_is_kind_of(VALUE obj, VALUE c);

Slide 17

Slide 17 text

bool rb_obj_is_kind_of(VALUE obj, VALUE search) { return class_search_ancestor(CLASS_OF(obj), search); }

Slide 18

Slide 18 text

bool class_search_ancestor(VALUE cl, VALUE search) { while (cl) { if (cl == c) return true; cl = RCLASS_SUPER(cl); } return false; } bool rb_obj_is_kind_of(VALUE obj, VALUE search) { return class_search_ancestor(CLASS_OF(obj), search); }

Slide 19

Slide 19 text

def class_search_ancestor(klass, search) while klass return true if klass == search klass = klass.superclass end false end def rb_obj_is_kind_of(obj, search) class_search_ancestor(obj.class, search) end

Slide 20

Slide 20 text

Butter fl y Object BasicObject Pidgeon

Slide 21

Slide 21 text

Butter fl y Object BasicObject Pidgeon

Slide 22

Slide 22 text

Butter fl y Object BasicObject Pidgeon

Slide 23

Slide 23 text

Butter fl y Object BasicObject

Slide 24

Slide 24 text

Butter fl y Object BasicObject

Slide 25

Slide 25 text

Butter fl y Object BasicObject ICLASS ICLASS

Slide 26

Slide 26 text

Butter fl y Object BasicObject ICLASS ICLASS

Slide 27

Slide 27 text

Butter fl y Object PP::ObjectMixin Kernel BasicObject ICLASS ICLASS

Slide 28

Slide 28 text

rb_obj_is_kind_of(obj, klass) def class_search_superclass(klass, search) while klass return true if klass == search klass = klass.superclass end false end def rb_obj_is_kind_of(obj, search) class_search_superclass(obj.class, search) end

Slide 29

Slide 29 text

rb_obj_is_kind_of(obj, klass) def class_search_superclass(klass, search) while klass return true if klass == search klass = klass.superclass end false end def rb_obj_is_kind_of(obj, search) class_search_superclass(obj.class, search) end ObjectSpace.internal_super_of ObjectSpace.internal_class_of

Slide 30

Slide 30 text

rb_obj_is_kind_of(obj, klass) def class_search_superclass(klass, search) while klass return true if klass == search klass = ObjectSpace.internal_super_of(klass) end false end def rb_obj_is_kind_of(obj, search) class_search_superclass(ObjectSpace.internal_class_of(class), sea end

Slide 31

Slide 31 text

Butter fl y Object PP::ObjectMixin Kernel BasicObject ICLASS ICLASS CPU 😢 • cache lines • branch prediction

Slide 32

Slide 32 text

klass klass.ancestors.size Object 11 Hash 19 Array 21 String 19 ActiveRecord::Base 72 ApplicationRecord::Base 80 ApplicationController 129 Repository 271 Organization 305

Slide 33

Slide 33 text

What to do? Avoid is_a? - use instance_of? Simplify class hierarchy Improve is_a? Avoid is_a? - respond_to?

Slide 34

Slide 34 text

Library/Framework Application Where to fix? Language Easy to change Hard to change Narrow fi x Broad fi x

Slide 35

Slide 35 text

Let's fix it in CRuby

Slide 36

Slide 36 text

Hash table Attempt #1

Slide 37

Slide 37 text

hash = { Pidgeon => true, Object => true, PP::ObjectMixin => true, Kernel => true, BasicObject => true } # to test hash.include?(klass)

Slide 38

Slide 38 text

def build_class_table(klass) hash = {} while klass hash[klass] = true klass = klass.super end hash end # to test hash.include?(klass)

Slide 39

Slide 39 text

st_table *build_class_table(VALUE klass) { st_table *hash = st_build_numtable(); while (klass) { st_insert(hash, klass, Qtrue); klass = RCLASS_SUPER(klass); } return hash; } // to test st_lookup(hash, klass, NULL)

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

🫤 Complex 🫤 Needs cache invalidation 🫤 Slower in best case 🫤 Lots of memory used ✅ Consistent performance ✅ Faster in worst case

Slide 44

Slide 44 text

Attempt #2

Slide 45

Slide 45 text

bool class_search_ancestor(VALUE cl, VALUE search) { rb_obj_info_dump(search); while (cl) { if (cl == search) return true; cl = RCLASS_SUPER(cl); } return false; }

Slide 46

Slide 46 text

849127 T_CLASS Hash 466102 T_CLASS Array 270880 T_CLASS String 168829 T_CLASS ActionDispatch::Cookies::AbstractCookieJar 155889 T_CLASS Symbol 124974 T_CLASS Numeric 91594 T_CLASS NilClass 83328 T_CLASS Jbuilder::Blank 76853 T_CLASS TrueClass 76213 T_CLASS FalseClass 69976 T_CLASS URI::Generic 62189 T_CLASS ActionController::Parameters 60719 T_CLASS (annon) 57980 T_CLASS (annon) 50100 T_CLASS Concurrent::Collection::MriMapBackend 49742 T_CLASS ActiveSupport::HashWithIndifferentAccess 45199 T_ICLASS src:ActionDispatch::Routing::UrlFor 39851 T_CLASS ActiveRecord::Relation 39836 T_CLASS Proc 39320 T_CLASS BigDecimal 31648 T_CLASS Range 29861 T_CLASS Pathname 29848 T_CLASS ActiveSupport::OrderedOptions

Slide 47

Slide 47 text

Class only linked list Attempt #2

Slide 48

Slide 48 text

Butter fl y Object PP::ObjectMixin Kernel BasicObject ICLASS ICLASS

Slide 49

Slide 49 text

🫤 Still loops 🫤 Still O(N) 🫤 Still linked list ✅ Simple ✅ No cache invalidation

Slide 50

Slide 50 text

Array Attempt #3

Slide 51

Slide 51 text

Butter fl y Object PP::ObjectMixin Kernel BasicObject ICLASS ICLASS

Slide 52

Slide 52 text

Butter fl y Object PP::ObjectMixin Kernel BasicObject

Slide 53

Slide 53 text

Butter fl y PP::ObjectMixin Kernel BasicObject Object

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

No content

Slide 57

Slide 57 text

Butter fl y PP::ObjectMixin Kernel BasicObject Object

Slide 58

Slide 58 text

Butter fl y BasicObject Object

Slide 59

Slide 59 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception

Slide 60

Slide 60 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception

Slide 61

Slide 61 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception

Slide 62

Slide 62 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception

Slide 63

Slide 63 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception

Slide 64

Slide 64 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception

Slide 65

Slide 65 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception Object BasicObject

Slide 66

Slide 66 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception Object BasicObject

Slide 67

Slide 67 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception Object BasicObject

Slide 68

Slide 68 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Exception Exception Object BasicObject ✅

Slide 69

Slide 69 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Hash Hash Object BasicObject 🚫

Slide 70

Slide 70 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Integer Numeric Object BasicObject Integer 🚫

Slide 71

Slide 71 text

TypeError Exception Object BasicObject StandardError TypeError is_a? StandardErro Exception Object BasicObject StandardError ✅

Slide 72

Slide 72 text

TypeError Exception Object BasicObject StandardError TypeError is_a? Object Object BasicObject ✅

Slide 73

Slide 73 text

def class_search_superclass(klass, search) depth = search.superclasses.size - 1 klass.superclasses[depth] == search end

Slide 74

Slide 74 text

def class_search_superclass(klass, search) depth = search.superclasses.size - 1 klass.superclasses[depth] == search end ✅ O(1) "constant time" ✅ No loops ✅ No cache invalidation

Slide 75

Slide 75 text

Time-memory trade-off GitHub's largest application: 92k Classes 😳 Memory increase: +6.6 MB Before: ~88MB used by classes

Slide 76

Slide 76 text

Exception Object BasicObject StandardError TypeError Exception Object BasicObject StandardError NameError Exception Object BasicObject StandardError RuntimeError TypeError NameError RuntimeError

Slide 77

Slide 77 text

Exception Object BasicObject StandardError Exception Object BasicObject StandardError Exception Object BasicObject StandardError TypeError NameError RuntimeError

Slide 78

Slide 78 text

Exception Object BasicObject StandardError TypeError NameError RuntimeError

Slide 79

Slide 79 text

Time-memory trade-off GitHub's largest application: 92k Classes 😳 Memory increase: +6.6 MB Memory increase: +1.1 MB Before: ~88MB used by classes

Slide 80

Slide 80 text

def class_search_superclass(klass, search) depth = search.superclasses.size klass.superclasses[depth] == search end

Slide 81

Slide 81 text

def class_search_superclass(klass, search) return true if klass == search depth = search.superclasses.size klass.superclasses[depth] == search end

Slide 82

Slide 82 text

def class_search_superclass(klass, search) return true if klass == search depth = search.superclasses_size return false if klass.superclass_size <= depth klass.superclasses[depth] == search end

Slide 83

Slide 83 text

bool class_search_superclass(VALUE klass, VALUE search) if (klass == search) return true; int depth = search.superclasses_size; if (klass.superclass_size <= depth) return false; return klass.superclasses[depth] == search; end

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

Always faster!

Slide 87

Slide 87 text

thanks ❤

Slide 88

Slide 88 text

No content

Slide 89

Slide 89 text

Related PRs • https://github.com/ruby/ruby/pull/5568 (fast class checks) • https://github.com/ruby/ruby/pull/5604 (memory reduction)