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

Making "is_a?" Fast

Making "is_a?" Fast

John Hawthorn

November 18, 2022
Tweet

More Decks by John Hawthorn

Other Decks in Technology

Transcript

  1. • Ruby Core Team • Rails Core Team • Sta

    ff Engineer @ GitHub John Hawthorn @jhawthorn @[email protected] he/him
  2. • kind_of? • Module#=== • case statement • rescue statement

    • protected methods What is "is_a?" is_a?("is_a?")
  3. 2% of total Ruby runtime* is_a? * In Rails apps

    I measured, your mileage may vary
  4. $ 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::
  5. $ 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
  6. 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); }
  7. 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
  8. 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
  9. 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
  10. 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
  11. klass klass.ancestors.size Object 11 Hash 19 Array 21 String 19

    ActiveRecord::Base 72 ApplicationRecord::Base 80 ApplicationController 129 Repository 271 Organization 305
  12. What to do? Avoid is_a? - use instance_of? Simplify class

    hierarchy Improve is_a? Avoid is_a? - respond_to?
  13. hash = { Pidgeon => true, Object => true, PP::ObjectMixin

    => true, Kernel => true, BasicObject => true } # to test hash.include?(klass)
  14. def build_class_table(klass) hash = {} while klass hash[klass] = true

    klass = klass.super end hash end # to test hash.include?(klass)
  15. 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)
  16. 🫤 Complex 🫤 Needs cache invalidation 🫤 Slower in best

    case 🫤 Lots of memory used ✅ Consistent performance ✅ Faster in worst case
  17. 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; }
  18. 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
  19. 🫤 Still loops 🫤 Still O(N) 🫤 Still linked list

    ✅ Simple ✅ No cache invalidation
  20. 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
  21. Exception Object BasicObject StandardError TypeError Exception Object BasicObject StandardError NameError

    Exception Object BasicObject StandardError RuntimeError TypeError NameError RuntimeError
  22. 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
  23. def class_search_superclass(klass, search) return true if klass == search depth

    = search.superclasses.size klass.superclasses[depth] == search end
  24. 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
  25. 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