Performance
Open-methods can be as fast as ordinary virtual member functions when compiled with optimization.
First, let’s examine the code generated by clang for an ordinary virtual function call:
void call_virtual_function(const Node& node, std::ostream& os) {
node.postfix(os);
}
Clang compiles this function to the following assembly on the x64 architecture:
mov rax, qword ptr [rdi]
mov rax, qword ptr [rax + 24]
jmp rax # TAILCALL
llvm-mca estimates this code has a throughput of 1 cycle per dispatch.
Let’s look at a method call now:
void call_via_ref(const Node& node, std::ostream& os) {
postfix(node, os);
}
This compiles to (variable names are shortened for readability):
mov rax, rdi
mov rcx, qword ptr [rdi]
mov rdi, qword ptr [rip + mult]
imul rdi, qword ptr [rcx - 8]
movzx ecx, byte ptr [rip + shift]
shr rdi, cl
mov rdx, rsi
mov rcx, qword ptr [rip + vptr_vector_vptrs]
mov rdi, qword ptr [rcx + 8*rdi]
mov rcx, qword ptr [rip + postfix::fn+88]
mov rcx, qword ptr [rdi + 8*rcx]
mov rsi, rax
jmp rcx # TAILCALL
This is quite a few instructions more. Upon closer examination, we see that many are memory reads, independent of one another; they can thus be executed in parallel. For example, the first three instructions can execute simultaneously.
llvm-mca estimates a throughput of 4 cycles per dispatch. However, the difference is amortized by the time spent passing the arguments and returning from the function; plus, of course, executing the body of the function.
Micro- and RDTSC-based benchmarks suggest that dispatching an open-methods with a single virtual argument via a reference is between 30% and 50% slower than calling the equivalent virtual function, with an empty body and no other arguments. In most real programs, the overhead would be unnoticeable.
However, call_via_ref does two things: it constructs a virtual_ptr<Node>
from a const Node&; and then it calls the method.
The construction of the virtual_ptr is the costly part. It performs a lookup
in a perfect hash table, indexed by pointers to std::type_info, to find the
correct vtable. Then it stores a pointer to it in the virtual_ptr object,
along with a pointer to the object.[1]
If we already have a virtual_ptr:
void call_via_virtual_ptr(virtual_ptr<const Node> node, std::ostream& os) {
postfix(node, os);
}
A method call compiles to:
mov rax, qword ptr [rip + postfix::fn+88]
mov rax, qword ptr [rdi + 8*rax]
jmp rax # TAILCALL
virtual_ptr arguments are passed through the method call, to the overrider,
which can use them to make further method calls.
A program designed with open-methods in mind should use virtual_ptrs
in place of plain pointers or references, as much as possible. Here is the Node
example, rewritten to use virtual_ptrs thoughout:
#include <boost/openmethod.hpp>
using boost::openmethod::virtual_ptr;
struct Node {
virtual ~Node() {}
virtual int value() const = 0;
};
struct Variable : Node {
Variable(int value) : v(value) {}
int value() const override { return v; }
int v;
};
struct Plus : Node {
Plus(virtual_ptr<const Node> left, virtual_ptr<const Node> right)
: left(left), right(right) {}
int value() const override { return left->value() + right->value(); }
virtual_ptr<const Node> left, right;
};
struct Times : Node {
Times(virtual_ptr<const Node> left, virtual_ptr<const Node> right)
: left(left), right(right) {}
int value() const override { return left->value() * right->value(); }
virtual_ptr<const Node> left, right;
};
#include <iostream>
using boost::openmethod::virtual_ptr;
BOOST_OPENMETHOD(postfix, (virtual_ptr<const Node> node, std::ostream& os), void);
BOOST_OPENMETHOD_OVERRIDE(
postfix, (virtual_ptr<const Variable> var, std::ostream& os), void) {
os << var->v;
}
BOOST_OPENMETHOD_OVERRIDE(
postfix, (virtual_ptr<const Plus> plus, std::ostream& os), void) {
postfix(plus->left, os);
os << ' ';
postfix(plus->right, os);
os << " +";
}
BOOST_OPENMETHOD_OVERRIDE(
postfix, (virtual_ptr<const Times> times, std::ostream& os), void) {
postfix(times->left, os);
os << ' ';
postfix(times->right, os);
os << " *";
}
BOOST_OPENMETHOD_CLASSES(Node, Variable, Plus, Times);
#include <boost/openmethod/initialize.hpp>
int main() {
boost::openmethod::initialize();
Variable a{2}, b{3}, c{4};
Plus d{a, b}; Times e{d, c};
postfix(e, std::cout);
std::cout << " = " << e.value() << "\n"; // 2 3 + 4 * = 20
}