Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ project /boost/openmethod/config ;

obj has_reflection : has_reflection.cpp : <cxxflags>-freflection ;
explicit has_reflection ;

# The other probe: BMI2's pext, which only policies/minimal_cover_hash.hpp
# needs. Probing beats naming an architecture - a <architecture>x86 conditional
# does not match every toolset spelling, and a compiler that rejects -mbmi2
# outright would take the directory down with it.
obj has_bmi2 : has_bmi2.cpp : <cxxflags>-mbmi2 <include>../include ;
explicit has_bmi2 ;
23 changes: 23 additions & 0 deletions config/has_bmi2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) 2017-2026 Jean-Louis Leroy
// Distributed under the Boost Software License, Version 1.0.
// See accompanying file LICENSE_1_0.txt
// or copy at http://www.boost.org/LICENSE_1_0.txt)

// Probe for BMI2's parallel bit extract, compiled with -mbmi2. See ../Jamfile,
// and boost/openmethod/policies/minimal_cover_hash.hpp, which is the only part
// of the library that needs the instruction.
//
// It tests the header's own feature macro rather than the intrinsic directly:
// what the test suite needs to know is whether that header will let the policy
// be used, which is a slightly narrower question than whether some spelling of
// pext compiles.

#include <boost/openmethod/policies/minimal_cover_hash.hpp>

#include <cstdint>

static_assert(BOOST_OPENMETHOD_HAS_PEXT);

auto probe(std::uint64_t value, std::uint64_t mask) -> std::uint64_t {
return boost::openmethod::detail::pext64(value, mask);
}
7 changes: 7 additions & 0 deletions doc/modules/ROOT/pages/performance.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ correct vtable. Then it stores a pointer to it in the `virtual_ptr` object,
along with a pointer to the object.footnote:[This is how Go and Rust implement
dynamic dispatch.]

The cost of that lookup belongs to the registry's cpp:type_hash[] policy, which
is cpp:fast_perfect_hash[] here as everywhere `default_registry` is used - a
multiply, a shift and a load. The alternatives in
xref:shared_libraries.adoc#type_ids_across_modules[Type Ids Across Modules] buy
a smaller or more predictable table and pay for it on this path, so the figures
below are the best case rather than the only one.

If we already have a `virtual_ptr`:

[source,c++]
Expand Down
18 changes: 18 additions & 0 deletions doc/modules/ROOT/pages/ref_headers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,24 @@ exceptions.
Provides an implementation of the `vptr` policy that stores the v-table pointers
in a map (by default a `std::map`) indexed by type ids.

### link:{headers-url}/boost/openmethod/policies/minimal_perfect_hash.hpp[<boost/openmethod/policies/minimal_perfect_hash.hpp>]

Provides an implementation of the `type_hash` policy that spends one slot per
type id whatever the type ids are, by hash and displace.

### link:{headers-url}/boost/openmethod/policies/two_level_hash.hpp[<boost/openmethod/policies/two_level_hash.hpp>]

Provides an implementation of the `type_hash` policy that indexes a power-of-two
table with a per-bucket multiplier.

### link:{headers-url}/boost/openmethod/policies/minimal_cover_hash.hpp[<boost/openmethod/policies/minimal_cover_hash.hpp>]

Provides an implementation of the `type_hash` policy that indexes by the
smallest set of bit positions that separates the type ids, extracted with
BMI2{apos}s `pext`. Requires that instruction; see
xref:shared_libraries.adoc#type_ids_across_modules[Type Ids Across Modules] for
when to prefer each of the three.

## Headers Included by Other Headers

These are the library's foundations. Every other header includes them, and a
Expand Down
18 changes: 18 additions & 0 deletions doc/modules/ROOT/pages/registries_and_policies.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,24 @@ using the cpp:with[] and cpp:without[] nested templates. For example,
struct indirect_registry : default_registry::with<policies::indirect_vptr> {};
----

cpp:with[] replaces the policy of the same _category_ where it already stands,
and appends only when the registry has no policy of that category yet. That is
what makes a policy swap a one-liner: `default_registry::with<
policies::minimal_perfect_hash<>>` puts the new hash exactly where
`fast_perfect_hash` was, still ahead of `vptr_vector`, so the ordering rule above
is not something a caller has to think about.

The library ships four `type_hash` policies. `fast_perfect_hash` is the default
and the right choice for almost every program. The others exist for the case it
handles least well - type ids spread over several far-apart address ranges, which
is what a program that `dlopen`{empty}s class-registering modules has:
cpp:minimal_perfect_hash[] spends one slot per type id whatever the addresses
are, cpp:two_level_hash[] trades a sawtooth table size for a shorter dispatch
sequence, and cpp:minimal_cover_hash[] indexes by a minimal cover of the ids'
bits but needs BMI2. Each policy's own page has the details;
xref:shared_libraries.adoc#type_ids_across_modules[Type Ids Across Modules]
explains the situation they address and when to pick which.

Policies are implemented as unary
https://www.boost.org/doc/libs/latest/libs/mp11/doc/html/mp11.html[Boost.MP11
quoted metafunctions]. A policy is an ordinary class that contains a nested
Expand Down
109 changes: 109 additions & 0 deletions doc/modules/ROOT/pages/shared_libraries.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,114 @@ against this by putting the applicable macro in a project header that every
translation unit includes, as in the examples, rather than repeating it in
individual `.cpp` files.

[#type_ids_across_modules]
## Type Ids Across Modules

Everything above is about sharing the registry's _state_. There is a second,
quieter question: where the _type ids_ themselves come from, and how far apart
they end up. It decides how well the registry's cpp:type_hash[] policy can do
its job, and it is the one place where `dlopen` behaves differently from
ordinary linking.

Under cpp:std_rtti[], a type id is `&typeid(X)` - the address of a
`std::type_info` object. The Itanium ABI requires that identity to be _pointer_
identity across modules, and the linker delivers it for an implicitly linked
shared library with a copy relocation: the record is copied into the
executable's image, and the library's references are redirected to that copy.
So a program and the libraries it links against present one compact set of type
ids, however many modules there are.

`dlopen` does not get that. A plugin's own classes are not named by the
executable, so nothing unifies them; their records stay in the plugin's own
mapping, which the loader places wherever it likes - and with address-space
randomization, somewhere different on every run. The distance between a
program's type ids and its plugin's is routinely measured in terabytes, and it
moves from run to run.

NOTE: RTTI has to keep default visibility for ids to unify at all. Under
`-fvisibility=hidden` one class can end up with a different `type_info` object
in each module; cpp:initialize[] copes - it treats them as several ids for the
same class - but they are extra ids for the hash to separate. This is why the
library's own shared-library tests mark their classes `BOOST_SYMBOL_VISIBLE`.

### What it costs

cpp:fast_perfect_hash[], the default, searches for a multiplier `M` and a shift
`S` such that `(M * x) >> S` is collision-free over the registered type ids. It
is fast and compact when the ids are evenly spread, and degrades when they are
not - and a program plus a few `dlopen`{empty}ed modules is as uneven as it
gets: several tight clusters, very far apart. Two things follow:

* the search gets dramatically more expensive, and on a large enough set it
fails - it gives up after half a million attempts and the error handler is
called with a `search_error`, which by default terminates the program;
* cpp:vptr_vector[] sizes its table from the hash's range, so a hash that is
working hard costs memory as well as time.

A program that loads plugins and registers more than a few hundred classes is
the one most likely to meet both.

### The alternatives

Three other cpp:type_hash[] policies trade that away, and a fourth option
removes the hash from the picture entirely. They are all drop-in: `with`
replaces a policy with the one of the same category, in place, so the new hash
still precedes cpp:vptr_vector[] in the list.

[cols="1,3"]
|===
| policy | what it does

a| cpp:minimal_perfect_hash[]
a| One slot per type id, whatever the addresses are, and a search whose cost
depends only on how many classes there are. The table size can be stated before
seeing an address. Costs a second dependent load on every dispatch - a
nanosecond or two per call. **The one to reach for in a plugin host.**

a| cpp:two_level_hash[]
a| The same idea with the final reduction replaced by a shift. Cheaper per call
than `minimal_perfect_hash` where the compiler hoists the shift amount out of
the dispatch loop, at the price of a table that rounds up to a power of two -
between one and two slots per type id, depending on the class count.

a| cpp:minimal_cover_hash[]
a| Indexes by the smallest set of bit positions that still separates the type
ids. As fast per call as the default, and it finds its table deterministically
in milliseconds. Needs BMI2, for **every** translation unit of the program -
see its documentation before choosing it.

a| cpp:vptr_map[]
a| Not a hash at all: a map keyed on the type id, so there is no table to size
and no search to fail. Slower per dispatch than any of the above, and the only
option that asks nothing of the type ids.
|===

Switching is one declaration. The registry is then a custom registry, so it
needs the treatment in <<custom_registries>> to be shared across modules:

[source,c++]
----
struct plugin_registry :
boost::openmethod::default_registry::with<
boost::openmethod::policies::minimal_perfect_hash<>> {};
----

`vptr_map` replaces the `vptr` policy rather than the hash, and the hash is then
dead weight, so drop it:

[source,c++]
----
struct plugin_registry :
boost::openmethod::default_registry::with<
boost::openmethod::policies::vptr_map<>>::without<
boost::openmethod::policies::type_hash> {};
----

TIP: none of this arises until a module registers classes of its own. A plugin
that only adds _overriders_ for classes the program already registered
contributes no new type ids, and the default policies are as good there as
anywhere.

## Indirect Vptrs

`initialize` rebuilds the v-tables in the registry. This invalidates all the
Expand Down Expand Up @@ -272,6 +380,7 @@ The shared library it loads includes the same header, so it uses
`indirect_registry` too and imports the state. The complete example is in the
`indirect_vptr` directory.

[#custom_registries]
## Custom Registries

A custom registry is shared exactly the same way - name it instead of
Expand Down
101 changes: 101 additions & 0 deletions doc/modules/ROOT/snippets/policies.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>
#include <boost/openmethod/policies/minimal_cover_hash.hpp>
#include <boost/openmethod/policies/minimal_perfect_hash.hpp>
#include <boost/openmethod/policies/throw_error_handler.hpp>
#include <boost/openmethod/policies/two_level_hash.hpp>
#include <boost/openmethod/policies/vptr_map.hpp>

#include <stdexcept>
Expand Down Expand Up @@ -118,6 +121,75 @@ BOOST_OPENMETHOD_OVERRIDE(

} // namespace fast_perfect_hash_demo

namespace minimal_perfect_hash_demo {

// tag::minimal_perfect_hash[]
// One slot per type id, whatever the addresses are. Swapping the hash is all it
// takes: `with` replaces the policy of the same category, in place, so
// `vptr_vector` still comes after it.
struct compact_registry :
default_registry::with<policies::minimal_perfect_hash<>> {};
// end::minimal_perfect_hash[]

BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog, compact_registry);

BOOST_OPENMETHOD(
trick, (virtual_ptr<Animal, compact_registry>), std::string,
compact_registry);

BOOST_OPENMETHOD_OVERRIDE(
trick, (virtual_ptr<Dog, compact_registry>), std::string) {
return "spin";
}

} // namespace minimal_perfect_hash_demo

namespace two_level_hash_demo {

// tag::two_level_hash[]
struct two_level_registry :
default_registry::with<policies::two_level_hash<>> {};
// end::two_level_hash[]

BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog, two_level_registry);

BOOST_OPENMETHOD(
trick, (virtual_ptr<Animal, two_level_registry>), std::string,
two_level_registry);

BOOST_OPENMETHOD_OVERRIDE(
trick, (virtual_ptr<Dog, two_level_registry>), std::string) {
return "spin";
}

} // namespace two_level_hash_demo

#if BOOST_OPENMETHOD_HAS_PEXT

namespace minimal_cover_hash_demo {

// tag::minimal_cover_hash[]
// Needs BMI2, for every translation unit of the program - hence the guard.
#if BOOST_OPENMETHOD_HAS_PEXT
struct cover_registry :
default_registry::with<policies::minimal_cover_hash<>> {};
#endif
// end::minimal_cover_hash[]

BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog, cover_registry);

BOOST_OPENMETHOD(
trick, (virtual_ptr<Animal, cover_registry>), std::string, cover_registry);

BOOST_OPENMETHOD_OVERRIDE(
trick, (virtual_ptr<Dog, cover_registry>), std::string) {
return "spin";
}

} // namespace minimal_cover_hash_demo

#endif

namespace stderr_output_demo {

// tag::stderr_output[]
Expand Down Expand Up @@ -226,6 +298,35 @@ BOOST_AUTO_TEST_CASE(rtti_and_storage) {
trick(virtual_ptr<Animal, hashed_registry>(snoopy)) == "spin");
}

{
using namespace minimal_perfect_hash_demo;
initialize<compact_registry>();

Dog snoopy;
BOOST_TEST(
trick(virtual_ptr<Animal, compact_registry>(snoopy)) == "spin");
}

{
using namespace two_level_hash_demo;
initialize<two_level_registry>();

Dog snoopy;
BOOST_TEST(
trick(virtual_ptr<Animal, two_level_registry>(snoopy)) == "spin");
}

#if BOOST_OPENMETHOD_HAS_PEXT
{
using namespace minimal_cover_hash_demo;
initialize<cover_registry>();

Dog snoopy;
BOOST_TEST(
trick(virtual_ptr<Animal, cover_registry>(snoopy)) == "spin");
}
#endif

{
using namespace stderr_output_demo;
initialize<noisy_registry>();
Expand Down
12 changes: 2 additions & 10 deletions include/boost/openmethod/policies/fast_perfect_hash.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,8 @@ namespace boost::openmethod {

namespace detail {

#if defined(UINTPTR_MAX)
using uintptr = std::uintptr_t;
constexpr uintptr uintptr_max = UINTPTR_MAX;
#else
static_assert(
sizeof(std::size_t) == sizeof(void*),
"This implementation requires that size_t and void* have the same size.");
using uintptr = std::size_t;
constexpr uintptr uintptr_max = (std::numeric_limits<std::size_t>::max)();
#endif
// detail::uintptr and detail::uintptr_max are in preamble.hpp: every
// `type_hash` policy needs them, not just this one.

struct hash_fn {
std::size_t mult;
Expand Down
Loading
Loading