Open Is Good



YOMM2: Fast, Orthogonal Open (Multi) Methods



Jean-Louis Leroy - jl@leroy.nyc

Case Study: AST

Case Study: AST


struct Node {
    virtual double value() = 0;
};

const, memory management, etc, omitted
code formatted to fit slide

Case Study: AST


struct Number : Node {
  Number(double value) : val(value ) { }

  double value() override {
    return val;
  }

  double val;
};

Case Study: AST


struct Plus : Node {
    Plus(Node& left, Node& right)
    : left(left), right(right) { }

  double value() override {
    return left.value() + right.value();
  }

  Node &left, &right;
};

Case Study: AST


struct Times : Node {
    Times(Node& left, Node& right)
    : left(left), right(right) { }

    double value() override {
        return left.value() * right.value();
    }

    Node &left, &right;
};

Case Study: AST


int main() {
    Node* expr =
        new Times(
            *new Number(2),
            *new Plus(*new Number(3), *new Number(4)));

    cout << expr->value() << "\n"; // 14

    return 0;
}

Case Study: AST


int main() {
    Node* expr =
        new Times(
            *new Number(2),
            *new Plus(*new Number(3), *new Number(4)));

    cout << /* RPN */
         << " = " << expr->value() << "\n";
    // 2 3 4 * + = 14

    return 0;
}

AST: Add a Virtual Function?

struct Node {
    // as before
    virtual string toRPN() = 0;
};

struct Plus : Node {
    // as before
  string toRPN() override {
    return left.toRPN() + " " + right.toRPN() + " +";
  }
};

// same for Number and Times

banana -> gorilla -> jungle

AST: Type Switch?

string toRPN(Node& node) {
    if (auto expr = dynamic_cast<Number*>(&node)) {
        return to_string(expr->value());
    } else if (auto expr = dynamic_cast<Plus*>(&node)) {
        return toRPN(expr->left) + " " +
            toRPN(expr->right) + " +";
    } else if (auto expr = dynamic_cast<Times*>(&node)) {
        return toRPN(expr->left) + " " +
            toRPN(expr->right) + " *";
    }
    throw runtime_error("unknown node type");
}

needs modification each time a new Node subtype is added

AST: Visitor?


struct Node {
    // as before
    struct Visitor {
        virtual void accept(Number& expr) = 0;
        virtual void accept(Plus& expr) = 0;
        virtual void accept(Times& expr) = 0;
    };

    virtual void visit(Visitor& viz) = 0;
};

AST: Visitor?

struct Number : Node {
  // as before
  void visit(Visitor& viz) override { viz.accept(*this); }
};

struct Plus : Node {
  void visit(Visitor& viz) override { viz.accept(*this); }
};

struct Times : Node {
    void visit(Visitor& viz) override { viz.accept(*this); }
};

AST: Visitor?

struct RPNVisitor : Node::Visitor {
  string result;
  void accept(Number& expr) {
    result = to_string(expr.val);
  }
  void accept(Plus& expr) {
    expr.left.visit(*this);
    string l = result;
    expr.right.visit(*this);
    result = l + " " + result + " +";
  }
  void accept(Times& expr) { ... }
};

AST: Visitor?


string toRPN(Node& node) {
  RPNVisitor viz;
  node.visit(viz);
  return viz.result;
}
  • lot of work
  • now it's difficult to add new types

AST: Function Table?

using RPNFormatter = string (*)(Node&);
unordered_map<type_index, RPNFormatter> RPNformatters;

string toRPN(Node& node) {
  return RPNformatters[typeid(node)](node);
}

AST: Function Table?

namespace { struct Init {
  Init() {
    RPNformatters[typeid(Number)] = [](Node& node) {
     return to_string(static_cast<Number&>(node).val); };
    RPNformatters[typeid(Plus)] = [](Node& node) {
     auto expr = static_cast<Plus&>(node);
     return toRPN(expr.left) + " " + toRPN(expr.right)
       + " +"; };
    // same for Time
  }
};
Init init;
} }

not bad, actually

The Expression Problem

The Expression Problem



behaviors += types

types += behaviors

Multi-Layer Architectures



presentation


domain


persistence

Multi-Layer Architectures


  • presentation: PersonDlg, CriminalCaseDlg

  • domain: Person, CriminalCase

  • persistence: persist to database, to json...

  • cross-cutting concerns

Open Methods

Open Methods



  • free virtual functions

  • types += behaviors

AST

#include <yorel/yomm2/cute.hpp>

register_class(Node);
register_class(Plus, Node);
register_class(Times, Node);
register_class(Number, Node);

int main() {
  yorel::yomm2::update_methods();
  // ...
}

(the boring part)

AST

using yorel::yomm2::virtual_;

declare_method(string, toRPN, (virtual_<Node&>));

define_method(string, toRPN, (Number& expr)) {
  return to_string(expr.val);
}

define_method(string, toRPN, (Plus& expr)) {
  return toRPN(expr.left) + " " + toRPN(expr.right) + " +";
}

// same for Times

call it like an ordinary function:

    cout << toRPN(expr) << " = " << expr->value() << "\n";
    // 2 3 4 * + = 14

AST: what about value?

  • value in the node hierarchy => interpreter

  • AST classes should only represent the tree

declare_method(int, value, (virtual_<Node&>));

define_method(int, value, (Number& expr)) {
  return expr.val;
}

define_method(int, value, (Plus& expr)) {
  return value(expr.left) + value(expr.right);
}

define_method(int, value, (Times& expr)) {
  return value(expr.left) * value(expr.right);
}

Multiple Dispatch

Occasionally Useful

add(Matrix, Matrix)                 -> Matrix
                                       add all elements
add(DiagonalMatrix, DiagonalMatrix) -> DiagonalMatrix
                                       just add diagonals

fight(Human, Creature, Axe)    -> not agile enough to wield
fight(Warrior, Creature, Axe)  -> chop it into pieces
fight(Warrior, Dragon, Axe)    -> die a honorable death
fight(Human, Dragon, Hands)    -> you just killed a dragon
                                  with your bare hands!
                                  incredible isn't it?

Syntax

use virtual_<> on multiple arguments:

declare_method(string, fight,
  (virtual_<Character&>, virtual_<Creature&>,
   virtual_<Device&>));

define_method(string, fight,
  (Human& x, Creature& y, Axe& z)) {
  return "not agile enough to wield";
}

define_method(string, fight,
  (Human& x, Dragon& y, Hands& z)) {
  return "you just killed a dragon with your bare hands."
         " Incredible isn't it?";
}

Selecting the right specialization


  • works just like selecting from set of overloads (but at runtime!)

  • or partial template specialization

  • ambiguities can arise

next

calls the next most specific override

register_class(Animal);
register_class(Dog, Animal);
register_class(Bulldog, Dog);

declare_method(std::string, kick, (virtual_<Animal&>));

define_method(string, kick, (Dog& dog)) {
  return "bark";
}

define_method(string, kick, (Bulldog& dog)) {
    return next(dog) + " and bite";
}

next

define_method(void, inspect, (Vehicle& v, Inspector& i)) {
  cout << "Inspect vehicle.\n";
}

define_method(void, inspect, (Car& v, Inspector& i)) {
  next(v, i);
  cout << "Inspect seat belts.\n";
}

define_method(void, inspect, (Car& v, StateInspector& i)) {
  next(v, i);
  cout << "Check road tax.\n";
}

Is This OOP?

Is This OOP?

  • Simula, Smalltalk, C++/Java/D/...:
    message-receiver

  • CLOS: rules

  • algorithms retake the front stage

  • no unnecessary breach of encapsulation

Inside yomm2

A Payroll Application

  • Role
    • Employee
      • Manager
    • Founder
  • Expense (X)
    • Cab, Jet
    • Public
      • Bus, Train

the pay Mono- Method

declare_method(double, pay, (virtual_<Employee&>));

define_method(double, pay, (Employee&)) {
  return 3000;
}

define_method(double, pay, (Manager& manager)) {
  return next(manager) + 2000;
}

declare_method

declare_method(double, pay, (virtual_<Employee&>));
struct _yomm2_method_pay;

namespace {
namespace YoMm2_nS_10 {
using _yOMM2_method =
  method<void, _yomm2_method_pay,
    double(virtual_<Employee &>),
    default_policy>;
_yOMM2_method::init_method init;
}
}

declare_method

declare_method(double, pay, (virtual_<Employee&>));
YoMm2_nS_10::_yOMM2_method
pay(discriminator, Employee & a0);

inline double
pay(Employee & a0) {
  auto pf = reinterpret_cast<double (*)(
    Employee & a0)>(
    YoMm2_nS_10::_yOMM2_method::resolve(a0));
  return pf(a0);
};

define_method

define_method(double, pay, (Employee&)) { return 3000; }
namespace { namespace YoMm2_nS_12 {
template <typename T> struct select;
template <typename... A> struct select<void(A...)> {
  using type = decltype(
    pay(discriminator(), declval<A>()...));
};

using _yOMM2_method =
  select<void(Employee &)>::type;
using _yOMM2_return_t = _yOMM2_method::return_type;
_yOMM2_return_t (*next)(Employee &);

define_method

define_method(double, pay, (Employee&)) { return 3000; }
struct _yOMM2_spec {
  static double body(Employee &);
};

register_spec<_yOMM2_return_t, _yOMM2_method,
                    _yOMM2_spec, void(Employee &)>
_yOMM2_init((void **)&next);
} }

double YoMm2_nS_12::_yOMM2_spec::body(Employee &)
{ return 3000; }

update_methods

  • process the info registered by static ctors

  • build representation of class hierarchies

  • build all the dispatch data inside a single vector

  • find a perfect hash function over relevant type_info's
  • H(x) = (M * x) >> S

Dispatching a Mono-Method

  • pretty much like virtual member functions

  • method table contains a pointer to the effective function

  • only it is not at a fixed offset in the method table

Dispatching a Mono-Method

during update_methods

method<pay>::slots_strides.i = 1;

// method table for Employee
mtbls[ H(&typeid(Employee)) ] = {
  ..., // used by approve,
  wrapper(pay(Employee&))
};

// method table for Manager
mtbls[ H(&typeid(Manager&)) ] = {
  ..., // used by approve,
  wrapper(pay(Manager&))
};

Dispatching a Mono-Method

pay(bill)

=>

mtbls[ H(&typeid(bill)) ]          // mtable for type
  [ method<pay>::slots_strides.i ] // pointer to fun
(bill)                             // call

Performance?

double call_pay(Employee& e) { return pay(e); }
;;; g++-6 -DNDEBUG -O2 ...
movq    mtbls(%rip), %rax                      ; hash table
movb    mtbls+32(%rip), %cl                    ; shift factor
movslq  method<pay>::slots_strides(%rip), %rdx ; slot
movq    (%rdi), %rsi                           ; vptr
movq    -8(%rsi), %rsi                         ; &type_info
imulq   mtbls+24(%rip), %rsi                   ; * M
shrq    %cl, %rsi                              ; >> S
movq    (%rax,%rsi,8), %rax                    ; method table
jmpq    *(%rax,%rdx,8)                         ; call

approve Multi-Method

declare_method(bool, approve,
  (virtual_<Role&>, virtual_<Expense&>, double));

define_method(bool, approve,
  (Role& r, Expense& e, double amount))    { return false; }

define_method(bool, approve,
  (Employee& r, Public& e, double amount)) { return true; }

define_method(bool, approve,
  (Manager& r, Taxi& e, double amount))    { return true; }

define_method(bool, approve,
  (Founder& r, Expense& e, double amount)) { return true; }

Dispatching a Multi-Method

  • it's a little more complicated

  • use a multi-dimensional dispatch table

  • size can grow very quickly

  • the table must be "compressed", devoid of redundancies

  • in fact the "uncompressed" table never exists

  • work in terms of class groups, not classes

Dispatching a Multi-Method

Expense+Jet Public+Bus+Train Cab
Role R,X R,X R,X
Employee R,X E,P R,X
Manager R,X E,P M,C
Founder F,X F,X F,X

(column major)

Building the Compressed Dispatch Table

Dispatching a Multi-Method

method<approve>::.slots_strides.pw = { 0, 4, 0 };

mtbls[ H(&typeid(Employee)) ] = {
  // & of (Employee,Expense+Jet) cell
  // used by pay
};
mtbls[ H(&typeid(Manager)) ] = {
  // & of (Manager,Expense+Jet) cell
  // used by pay
};

mtbls[ H(&typeid(Expense)) ] = { 0 }; // also for Jet
mtbls[ H(&typeid(Public))  ] = { 1 }; // also for Bus, Train
mtbls[ H(&typeid(Cab))     ] = { 2 };

Dispatching a Multi-Method

approve(bill, ticket, 5000)

=>

word* slots_strides = method<approve>::.slots_strides.pw;

mtbls[ H(&typeid(bill)) ]        // method table for bill
  [ slots_strides[0].i ]         // ptr to cell in 1st column
  [ mtbls [ H(&typeid(ticket)) ] // method table for ticket
    [ slots_strides[2].i ]       // column
    * slots_strides[1].i         // stride
  ]                              // pointer to function
(bill, ticket, 5000)             // call

Benchmarks

gcc6 clang6
normal inheritance
virtual function 1-method 16% 17%
double dispatch 2-method 25% 35%
virtual inheritance
virtual function 1-method 19% 17%
double dispatch 2-method 40% 33%

yomm2 vs other systems

  • Pirkelbauer - Solodkyi - Stroustrup (PSS)
  • yomm11
  • Cmm
  • Loki / Modern C++

yomm2 vs PSS

yomm2 vs yomm11

  • no need to instrument classes

  • methods are ordinary functions

QA Time

  • github: https://github.com/jll63/yomm2
  • contact: Jean-Louis Leroy - jl@leroy.nyc