<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://chico.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="http://chico.dev/" rel="alternate" type="text/html" /><updated>2026-06-06T05:10:46+00:00</updated><id>http://chico.dev/feed.xml</id><title type="html">Francisco Geiman Thiesen</title><subtitle>Passionate Algorithmist, Compiler Engineer @ Microsoft</subtitle><entry><title type="html">Porting Open3D with mirror_bridge: 25,262 hand-written binding lines → 71 auto-generated</title><link href="http://chico.dev/Mirror-Bridge-Open3D-71-Lines/" rel="alternate" type="text/html" title="Porting Open3D with mirror_bridge: 25,262 hand-written binding lines → 71 auto-generated" /><published>2026-04-24T00:00:00+00:00</published><updated>2026-04-24T00:00:00+00:00</updated><id>http://chico.dev/Mirror-Bridge-Open3D-71-Lines</id><content type="html" xml:base="http://chico.dev/Mirror-Bridge-Open3D-71-Lines/"><![CDATA[<p><img src="/images/mirror-bridge-open3d-demo.png" alt="Real C++ point clouds, processed via mirror_bridge" /></p>

<p>Every point cloud above was computed in plain C++ and reached Python
through a binding I didn’t write. No <code class="language-plaintext highlighter-rouge">.def()</code>, no trampoline classes,
no keyword-arg boilerplate. Just <code class="language-plaintext highlighter-rouge">bind_class&lt;PointCloud&gt;(m, "PointCloud")</code>.
Reflection fills in the rest at compile time.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th style="text-align: right">pybind11</th>
      <th>mirror_bridge</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Hand-written binding lines (Open3D geom)</td>
      <td style="text-align: right">3,610</td>
      <td><strong>71</strong>  (51× less, auto-generated)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PointCloud(list_of_1M_points)</code> ctor</td>
      <td style="text-align: right">585 ms</td>
      <td><strong>19.6 ms</strong>  (30× faster, §3)</td>
    </tr>
  </tbody>
</table>

<p><strong>TL;DR</strong></p>

<ul>
  <li>47 Open3D geometry classes bind with <strong>0 hand-written lines</strong>.
<code class="language-plaintext highlighter-rouge">mirror_bridge generate</code> emits the whole 71-line module.</li>
  <li>On the list-to-<code class="language-plaintext highlighter-rouge">std::vector&lt;Eigen::Vector3d&gt;</code> ingest primitive,
mirror_bridge is <strong>30-35× faster than pybind11</strong>, stable across
10K-1M points. On compute-heavy calls the binding layer rounds to
zero and the two frameworks are at parity. Full case study in §3.</li>
  <li>Apache 2.0. One <code class="language-plaintext highlighter-rouge">docker run</code> reproduces every number in under ten
minutes.</li>
</ul>

<hr />

<h2 id="1-pybind11-write-every-binding-twice">1. pybind11: write every binding twice</h2>

<p>Here’s how Open3D binds one method, <code class="language-plaintext highlighter-rouge">voxel_down_sample_and_trace</code>
(<a href="https://github.com/isl-org/Open3D/blob/main/cpp/pybind/geometry/pointcloud.cpp">pointcloud.cpp L70-L75</a>):</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="n">def</span><span class="p">(</span><span class="s">"voxel_down_sample_and_trace"</span><span class="p">,</span>
     <span class="o">&amp;</span><span class="n">PointCloud</span><span class="o">::</span><span class="n">VoxelDownSampleAndTrace</span><span class="p">,</span>
     <span class="s">"Function to downsample using PointCloud::VoxelDownSample. Also "</span>
     <span class="s">"records point cloud index before downsampling"</span><span class="p">,</span>
     <span class="s">"voxel_size"</span><span class="n">_a</span><span class="p">,</span> <span class="s">"min_bound"</span><span class="n">_a</span><span class="p">,</span> <span class="s">"max_bound"</span><span class="n">_a</span><span class="p">,</span>
     <span class="s">"approximate_class"</span><span class="n">_a</span> <span class="o">=</span> <span class="nb">false</span><span class="p">)</span>
</code></pre></div></div>

<p>Every method lives twice: once in the class declaration, again here.
Every parameter name is re-typed. Every default is re-typed. Every
overload gets its own <code class="language-plaintext highlighter-rouge">.def()</code>.</p>

<p>The pybind11 layer for Open3D’s geometry module alone is <strong>3,610
lines</strong>. The whole Python binding spans <strong>25,262 lines across 124
files</strong>.</p>

<h2 id="2-mirror_bridge-write-nothing">2. mirror_bridge: write nothing</h2>

<p>One command. Zero hand-written binding code:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ mirror_bridge generate Open3D/cpp/open3d/geometry \
      --module open3d_full --lang python \
      -I Open3D/cpp -I /usr/include/eigen3

  ✓ Found: OrientedBoundingBox in BoundingVolume.h
  ✓ Found: AxisAlignedBoundingBox in BoundingVolume.h
  ✓ Found: HalfEdgeTriangleMesh::HalfEdge in HalfEdgeTriangleMesh.h
  ✓ Found: KDTreeSearchParam::SearchType in KDTreeSearchParam.h
  ✓ Found: TriangleMesh::Material::MaterialParameter in TriangleMesh.h
  ✓ Found: VoxelGrid::VoxelPoolingMode in VoxelGrid.h
     ... 47 classes in all ...
  ✓ Built: build/open3d_full.so
</code></pre></div></div>

<p>A brace-depth parser finds every class, struct and enum. Nested types
get qualified correctly (<code class="language-plaintext highlighter-rouge">HalfEdgeTriangleMesh::HalfEdge</code>, not a bare
<code class="language-plaintext highlighter-rouge">HalfEdge</code> that would collide with another file’s <code class="language-plaintext highlighter-rouge">HalfEdge</code>).</p>

<p>Dependencies are resolved by reflection, not by the parser.
<code class="language-plaintext highlighter-rouge">std::meta::bases_of(T)</code> gives inheritance edges. The enclosing scope
gives nesting. <code class="language-plaintext highlighter-rouge">parameters_of</code> and the reflected return type catch
every class referenced from a method signature. The generator
topologically sorts the result, so <code class="language-plaintext highlighter-rouge">AxisAlignedBoundingBox</code> lands
before <code class="language-plaintext highlighter-rouge">PointCloud</code> (which returns one from <code class="language-plaintext highlighter-rouge">get_aabb()</code>), and every
nested type lands after its enclosing scope.</p>

<p>The emitted module is short enough to read:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">MIRROR_BRIDGE_MODULE</span><span class="p">(</span><span class="n">open3d_full</span><span class="p">,</span>
    <span class="n">mirror_bridge</span><span class="o">::</span><span class="n">bind_class</span><span class="o">&lt;</span><span class="n">OrientedBoundingBox</span><span class="o">&gt;</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="s">"OrientedBoundingBox"</span><span class="p">);</span>
    <span class="n">mirror_bridge</span><span class="o">::</span><span class="n">bind_class</span><span class="o">&lt;</span><span class="n">AxisAlignedBoundingBox</span><span class="o">&gt;</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="s">"AxisAlignedBoundingBox"</span><span class="p">);</span>
    <span class="n">mirror_bridge</span><span class="o">::</span><span class="n">bind_class</span><span class="o">&lt;</span><span class="n">PointCloud</span><span class="o">&gt;</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="s">"PointCloud"</span><span class="p">);</span>
    <span class="c1">// ... 43 more bind_class lines ...</span>
    <span class="n">mirror_bridge</span><span class="o">::</span><span class="n">bind_class</span><span class="o">&lt;</span><span class="n">AggColorVoxel</span><span class="o">&gt;</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="s">"AggColorVoxel"</span><span class="p">);</span>
<span class="p">)</span>
</code></pre></div></div>

<p><strong>71 lines total.</strong> Each <code class="language-plaintext highlighter-rouge">bind_class&lt;T&gt;</code> pulls constructors, methods,
fields, operators, kwargs, defaults, <code class="language-plaintext highlighter-rouge">__repr__</code>, subclassing,
inheritance and polymorphic returns straight from the header.</p>

<h2 id="3-deep-dive-30-faster-on-list--vectoreigenvector3d">3. Deep dive: 30× faster on list → vector&lt;Eigen::Vector3d&gt;</h2>

<p>The binding layer only matters when it <em>is</em> the whole cost. Computing
a centroid on a million points is SIMD math; the dispatcher rounds to
zero. But constructing the point cloud from a Python list is pure
binding-layer work, and that’s where mirror_bridge is dramatically
faster than pybind11.</p>

<h3 id="the-measurement">The measurement</h3>

<p>Both bindings are compiled against the same <code class="language-plaintext highlighter-rouge">demo::PointCloud</code> source
in <code class="language-plaintext highlighter-rouge">asm_study/geometry_min.hpp</code>, with the same clang-p2996 and the
same <code class="language-plaintext highlighter-rouge">-O3 -stdlib=libc++ -fPIC -shared</code> flags. The only thing that
changes between <code class="language-plaintext highlighter-rouge">pc_pybind.so</code> and <code class="language-plaintext highlighter-rouge">pc_mb.so</code> is the binding
framework.</p>

<p>Constructor scaling on a list of 3-element float sublists, median of
3 runs with one warmup (<a href="https://github.com/FranciscoThiesen/mirror_bridge/tree/main/asm_study">source</a>):</p>

<table>
  <thead>
    <tr>
      <th>N points</th>
      <th style="text-align: right">pybind11</th>
      <th style="text-align: right">mirror_bridge</th>
      <th style="text-align: right">ratio</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>10,000</td>
      <td style="text-align: right">5.3 ms</td>
      <td style="text-align: right">0.15 ms</td>
      <td style="text-align: right"><strong>35.2×</strong></td>
    </tr>
    <tr>
      <td>100,000</td>
      <td style="text-align: right">60.5 ms</td>
      <td style="text-align: right">1.8 ms</td>
      <td style="text-align: right"><strong>33.4×</strong></td>
    </tr>
    <tr>
      <td>1,000,000</td>
      <td style="text-align: right">585 ms</td>
      <td style="text-align: right">19.6 ms</td>
      <td style="text-align: right"><strong>29.9×</strong></td>
    </tr>
  </tbody>
</table>

<p>The ratio is stable. This is a constant-factor binding-layer
difference, not a scaling artefact.</p>

<p>For context, at N = 1M on the same build:</p>

<table>
  <thead>
    <tr>
      <th>operation</th>
      <th style="text-align: right">pybind11</th>
      <th style="text-align: right">mirror_bridge</th>
      <th style="text-align: right">ratio</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">get_center</code> (compute-heavy)</td>
      <td style="text-align: right">0.83 ms</td>
      <td style="text-align: right">0.84 ms</td>
      <td style="text-align: right">0.99×</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">size()</code> (pure dispatch overhead)</td>
      <td style="text-align: right">0.08 µs</td>
      <td style="text-align: right">0.03 µs</td>
      <td style="text-align: right">2.96×</td>
    </tr>
  </tbody>
</table>

<p>Parity on compute. A clean 3× on dispatch. <strong>30× on the ingest
primitive.</strong></p>

<h3 id="why-pybind11-pays-that-cost">Why pybind11 pays that cost</h3>

<p>pybind11’s <code class="language-plaintext highlighter-rouge">type_caster&lt;Eigen::Matrix&lt;double, 3, 1&gt;&gt;</code> is designed
first for numpy compatibility and second for Python lists. On every
element, it:</p>

<ol>
  <li>Constructs a caster object.</li>
  <li>Checks whether the element is a numpy array of the right dtype
and shape.</li>
  <li>Falls through to the Python buffer protocol.</li>
  <li>Falls through to the “length-3 sequence of floats” path.</li>
  <li>Constructs a fresh <code class="language-plaintext highlighter-rouge">Eigen::Vector3d</code> and emits a <code class="language-plaintext highlighter-rouge">move</code> into the
destination vector.</li>
</ol>

<p>Every one of a million elements pays steps 1-5. For list input, the
numpy and buffer-protocol checks are pure overhead.</p>

<h3 id="why-mirror_bridge-doesnt">Why mirror_bridge doesn’t</h3>

<p>mirror_bridge’s <code class="language-plaintext highlighter-rouge">eigen_vector_from_python</code> is specialised for the
list shape. No numpy branches, no caster objects, no detours:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// from python/mirror_bridge_eigen.hpp (abridged)</span>
<span class="k">template</span><span class="o">&lt;</span><span class="k">typename</span> <span class="nc">Scalar</span><span class="p">,</span> <span class="kt">int</span> <span class="n">N</span><span class="p">&gt;</span>
<span class="kr">inline</span> <span class="kt">bool</span> <span class="nf">eigen_vector_from_python</span><span class="p">(</span><span class="n">PyObject</span><span class="o">*</span> <span class="n">obj</span><span class="p">,</span>
                                     <span class="n">Eigen</span><span class="o">::</span><span class="n">Matrix</span><span class="o">&lt;</span><span class="n">Scalar</span><span class="p">,</span> <span class="n">N</span><span class="p">,</span> <span class="mi">1</span><span class="o">&gt;&amp;</span> <span class="n">out</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">PySequence_Check</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span> <span class="o">||</span> <span class="n">PySequence_Size</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span> <span class="o">!=</span> <span class="n">N</span><span class="p">)</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">N</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">PyObject</span><span class="o">*</span> <span class="n">item</span> <span class="o">=</span> <span class="n">PySequence_GetItem</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">i</span><span class="p">);</span>
        <span class="n">out</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="n">Scalar</span><span class="o">&gt;</span><span class="p">(</span><span class="n">PyFloat_AsDouble</span><span class="p">(</span><span class="n">item</span><span class="p">));</span>
        <span class="n">Py_DECREF</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The outer list→vector loop (in <code class="language-plaintext highlighter-rouge">mirror_bridge_python.hpp</code>) calls this
per element. Three <code class="language-plaintext highlighter-rouge">PyFloat_AsDouble</code> calls and a direct write into
the Eigen object. No intermediate ceremony.</p>

<p>Reflection is what makes this specialisation possible: at binding-
generation time, mirror_bridge knows the parameter type is
<code class="language-plaintext highlighter-rouge">std::vector&lt;Eigen::Vector3d&gt;</code>, so it can emit the specialised
converter in the call site. pybind11’s type caster has no comparable
compile-time information and has to handle the general case every
time.</p>

<h3 id="reproduce">Reproduce</h3>

<p>One-time: <code class="language-plaintext highlighter-rouge">./docker_build.sh</code> compiles clang-p2996 and libc++ from
source (~18 minutes on a modern laptop, measured). Every run after
the image is built is ~15 seconds.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/FranciscoThiesen/mirror_bridge
<span class="nb">cd </span>mirror_bridge
./docker_build.sh                   <span class="c"># one-time, ~18 min</span>

docker run <span class="nt">--rm</span> <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>:/workspace <span class="nt">-w</span> /workspace/asm_study <span class="se">\</span>
    mirror_bridge:latest bash <span class="nt">-c</span> <span class="s1">'\
        ./build_fair.sh &amp;&amp; \
        python3 bench_fair.py'</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">build_fair.sh</code> compiles both <code class="language-plaintext highlighter-rouge">bind_mb.cpp</code> and <code class="language-plaintext highlighter-rouge">bind_pybind.cpp</code>
with identical flags, printing the exact compiler line.
<code class="language-plaintext highlighter-rouge">bench_fair.py</code> prints the scaling table above plus the context
rows. Expect constants in the 25-40× range depending on hardware;
the scaling pattern holds.</p>

<h3 id="when-it-matters-in-practice">When it matters in practice</h3>

<ul>
  <li><strong>Data pipelines that parse JSON / CSV / dicts</strong> into C++ types hit
this ingest primitive. The 30× compounds across a batch job.</li>
  <li><strong>Compute-dominated workflows</strong> where you load a million points
once and then run a dozen C++ methods on them: ingest amortises,
the rest is at parity. No win, no loss.</li>
  <li><strong>Numpy already</strong>: both frameworks can use the buffer protocol for
near-zero-copy access. If your data is already a numpy array, use
that. mirror_bridge has an open item to expose
<code class="language-plaintext highlighter-rouge">PointCloud.points</code> through the buffer protocol directly, which
would collapse this entire cost to a pointer handoff.</li>
</ul>

<h2 id="4-what-the-ffi-still-costs">4. What the FFI still costs</h2>

<p>Even on the fast side, crossing the FFI isn’t free:</p>

<table>
  <thead>
    <tr>
      <th>Operation</th>
      <th style="text-align: right">mirror_bridge cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PointCloud(list_of_1M_points)</code></td>
      <td style="text-align: right">~18 ms</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">len(pcd.points)</code> (reads 1M points out)</td>
      <td style="text-align: right">~270 ms</td>
    </tr>
  </tbody>
</table>

<p>Handing a million points into C++ is a real 1M-vector copy. Reading
them back as a Python list re-materialises a million Python objects.</p>

<p><strong>Rule of thumb: keep data inside C++ as long as possible, and only
extract what you display.</strong> Every parity number above holds because
the points live in the C++ <code class="language-plaintext highlighter-rouge">PointCloud</code> and each method call returns
a small scalar.</p>

<h2 id="5-everything-reflection-can-see-is-free">5. Everything reflection can see is free</h2>

<p>Auto-generated per class, no user input:</p>

<ul>
  <li><strong>Constructors</strong> as Python <code class="language-plaintext highlighter-rouge">__init__</code> overloads, resolved by arg
count and type.</li>
  <li><strong>Methods</strong>, direct and inherited via <code class="language-plaintext highlighter-rouge">bases_of</code> BFS walk.</li>
  <li><strong>Fields</strong> as Python attributes (getter/setter).</li>
  <li><strong>Operators</strong>: <code class="language-plaintext highlighter-rouge">+ - * / %  == != &lt; &gt; &lt;= &gt;=  += -= *= /= []  ()</code> and
unary <code class="language-plaintext highlighter-rouge">+/-</code>. Routed via <code class="language-plaintext highlighter-rouge">std::meta::operator_of</code> to the matching
Python slot.</li>
  <li><strong>Keyword arguments</strong> from <code class="language-plaintext highlighter-rouge">std::meta::identifier_of</code> on each
parameter info.</li>
  <li><strong>Default argument values</strong> detected via <code class="language-plaintext highlighter-rouge">has_default_argument</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">__repr__</code></strong> from visible members.</li>
  <li><strong>Exception mapping</strong>: <code class="language-plaintext highlighter-rouge">std::out_of_range → IndexError</code>,
<code class="language-plaintext highlighter-rouge">std::invalid_argument → ValueError</code>, <code class="language-plaintext highlighter-rouge">std::runtime_error →
RuntimeError</code>, via <code class="language-plaintext highlighter-rouge">dynamic_cast</code>.</li>
  <li><strong>Polymorphic returns</strong> resolved through <code class="language-plaintext highlighter-rouge">typeid</code> lookup.</li>
  <li><strong>Nested classes</strong> qualified as <code class="language-plaintext highlighter-rouge">Parent::Child</code>.</li>
</ul>

<h3 id="auto-trampoline-python-subclasses-override-c-virtuals-for-free">Auto-trampoline: Python subclasses override C++ virtuals for free</h3>

<p>pybind11 lets Python subclasses override C++ virtuals, but the
plumbing isn’t free. You have to write a <em>trampoline class</em>: a
hand-forwarded layer with one entry per virtual, each using
<code class="language-plaintext highlighter-rouge">PYBIND11_OVERRIDE(...)</code> to route back to Python if the subclass
defined an override. One class, one trampoline. Ten classes, ten.</p>

<p>mirror_bridge generates the trampoline from reflection.
<code class="language-plaintext highlighter-rouge">bind_class_auto&lt;T&gt;</code> enumerates T’s virtual slots, synthesises a
per-slot dispatcher that routes into Python when the instance has an
override, and swaps T’s vtable with a custom one at construction.
You write zero glue.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TaggedPointCloud</span><span class="p">(</span><span class="n">o3d</span><span class="p">.</span><span class="n">PointCloud</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">GetCenter</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">[</span><span class="mi">999</span><span class="p">,</span> <span class="mi">999</span><span class="p">,</span> <span class="mi">999</span><span class="p">]</span>   <span class="c1"># reached by C++ too, via the vtable swap
</span></code></pre></div></div>

<p>No <code class="language-plaintext highlighter-rouge">PYBIND11_OVERRIDE</code>, no dispatch helper, no trampoline
boilerplate. (Caveat: works on Linux and macOS today; see §8.)</p>

<h2 id="6-running-against-real-libopen3dso">6. Running against real libOpen3D.so</h2>

<p>This isn’t a theoretical exercise. The binding loads and calls into
a real <code class="language-plaintext highlighter-rouge">libOpen3D.so</code> built from source. Getting there took <strong>two
small CMake patches</strong> to a single file
(<code class="language-plaintext highlighter-rouge">3rdparty/find_dependencies.cmake</code>):</p>

<ol>
  <li>
    <p><strong>Forward <code class="language-plaintext highlighter-rouge">CMAKE_CXX_FLAGS</code> to ExternalProjects</strong> so 3rdparty
libraries (VTK, embree, zmq, …) inherit <code class="language-plaintext highlighter-rouge">-stdlib=libc++</code> from
the top-level build. Without it, the final <code class="language-plaintext highlighter-rouge">.so</code> mixes
<code class="language-plaintext highlighter-rouge">std::__1::*</code> (libc++) and <code class="language-plaintext highlighter-rouge">std::__cxx11::*</code> (libstdc++) symbols
and fails to dlopen.</p>
  </li>
  <li>
    <p><strong>Wrap BoringSSL archives with <code class="language-plaintext highlighter-rouge">-Wl,--whole-archive</code></strong> when
linking <code class="language-plaintext highlighter-rouge">libOpen3D.so</code>, or dlopen fails on <code class="language-plaintext highlighter-rouge">X509_INFO_free</code> (curl
and zmq reach SSL through transitive calls the linker otherwise
drops).</p>
  </li>
</ol>

<p>The diff lives at <a href="https://github.com/FranciscoThiesen/mirror_bridge/tree/main/examples/open3d-full-port/patches"><code class="language-plaintext highlighter-rouge">examples/open3d-full-port/patches</code></a> as a
single <code class="language-plaintext highlighter-rouge">.patch</code> file. macOS and Windows builds are unchanged.</p>

<p>With the patched fork, a real Open3D Python session
(<a href="https://github.com/FranciscoThiesen/mirror_bridge/tree/main/examples/open3d-runtime">source</a>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">real_open3d</span> <span class="k">as</span> <span class="n">o3d</span>

<span class="n">pcd</span> <span class="o">=</span> <span class="n">o3d</span><span class="p">.</span><span class="n">PointCloud</span><span class="p">([[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">]])</span>
<span class="k">print</span><span class="p">(</span><span class="n">pcd</span><span class="p">.</span><span class="n">GetCenter</span><span class="p">())</span>             <span class="c1"># [0.25, 0.25, 0.25]
</span><span class="k">print</span><span class="p">(</span><span class="n">pcd</span><span class="p">.</span><span class="n">HasPoints</span><span class="p">())</span>             <span class="c1"># True
</span>
<span class="n">smaller</span> <span class="o">=</span> <span class="n">pcd</span><span class="p">.</span><span class="n">VoxelDownSample</span><span class="p">(</span><span class="n">voxel_size</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>   <span class="c1"># kwargs + defaults
</span><span class="n">aabb</span>    <span class="o">=</span> <span class="n">pcd</span><span class="p">.</span><span class="n">GetAxisAlignedBoundingBox</span><span class="p">()</span>        <span class="c1"># polymorphic return
</span><span class="k">print</span><span class="p">(</span><span class="n">aabb</span><span class="p">.</span><span class="n">Volume</span><span class="p">())</span>                              <span class="c1"># 1.0
</span>
<span class="c1"># Subclass Open3D with a Python override. No trampoline.
</span><span class="k">class</span> <span class="nc">TaggedPointCloud</span><span class="p">(</span><span class="n">o3d</span><span class="p">.</span><span class="n">PointCloud</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">GetCenter</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">[</span><span class="mi">999</span><span class="p">,</span> <span class="mi">999</span><span class="p">,</span> <span class="mi">999</span><span class="p">]</span>
</code></pre></div></div>

<p>Zero <code class="language-plaintext highlighter-rouge">.def</code> calls.</p>

<h2 id="7-try-it-yourself">7. Try it yourself</h2>

<p>The one-time toolchain build takes ~18 minutes (compiling
clang-p2996 + libc++ from source). After that, the reproduction
steps below run in well under a minute.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/FranciscoThiesen/mirror_bridge
<span class="nb">cd </span>mirror_bridge
./docker_build.sh                   <span class="c"># one-time, ~18 min</span>
</code></pre></div></div>

<h3 id="quick-reproduction-20-seconds-in-a-built-image">Quick reproduction (~20 seconds in a built image)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>:/workspace <span class="nt">-w</span> /workspace mirror_bridge:latest bash <span class="nt">-c</span> <span class="s1">'
    # The §3 ingest benchmark:
    cd asm_study &amp;&amp; ./build_fair.sh &amp;&amp; python3 bench_fair.py &amp;&amp; \
    # The 6-panel visual at the top of this post:
    cd ../examples/open3d-comprehensive &amp;&amp; \
    ./build_and_test.sh &amp;&amp; python3 visual_demo.py
'</span>
</code></pre></div></div>

<p>Measured end-to-end: ~18 seconds on an Apple Silicon laptop.
<code class="language-plaintext highlighter-rouge">open3d-comprehensive/build_and_test.sh</code> verifies 10 feature
groups; <code class="language-plaintext highlighter-rouge">visual_demo.py</code> writes <code class="language-plaintext highlighter-rouge">mirror_bridge_open3d_demo.png</code>. No
external downloads required beyond what <code class="language-plaintext highlighter-rouge">./docker_build.sh</code> already
pulled.</p>

<h3 id="extended-link-against-real-libopen3dso-15-minutes-extra">Extended: link against real libOpen3D.so (~15 minutes extra)</h3>

<p>The <code class="language-plaintext highlighter-rouge">examples/open3d-runtime/</code> demo links against a real
libOpen3D.so you build once from the patched fork. The full recipe
(inside the Docker container, from the repo root):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Build libOpen3D.so. ~5 minutes on a modern laptop; the 3rdparty</span>
<span class="c"># tarballs are already cached in examples/open3d-full-port/Open3D/</span>
<span class="c"># 3rdparty_downloads so no internet round trips are needed.</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> /tmp/o3d_fork_build <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /tmp/o3d_fork_build
cmake <span class="nt">-G</span> Ninja <span class="se">\</span>
  <span class="nt">-DCMAKE_BUILD_TYPE</span><span class="o">=</span>Release <span class="se">\</span>
  <span class="nt">-DCMAKE_CXX_FLAGS</span><span class="o">=</span><span class="s2">"-stdlib=libc++"</span> <span class="se">\</span>
  <span class="nt">-DBUILD_SHARED_LIBS</span><span class="o">=</span>ON <span class="se">\</span>
  <span class="nt">-DBUILD_GUI</span><span class="o">=</span>OFF <span class="nt">-DBUILD_PYTHON_MODULE</span><span class="o">=</span>OFF <span class="nt">-DBUILD_EXAMPLES</span><span class="o">=</span>OFF <span class="se">\</span>
  <span class="nt">-DBUILD_TESTS</span><span class="o">=</span>OFF <span class="nt">-DBUILD_BENCHMARKS</span><span class="o">=</span>OFF <span class="nt">-DBUILD_UNIT_TESTS</span><span class="o">=</span>OFF <span class="se">\</span>
  <span class="nt">-DBUILD_WEBRTC</span><span class="o">=</span>OFF <span class="nt">-DBUILD_LIBREALSENSE</span><span class="o">=</span>OFF <span class="nt">-DBUILD_AZURE_KINECT</span><span class="o">=</span>OFF <span class="se">\</span>
  <span class="nt">-DUSE_SYSTEM_EIGEN3</span><span class="o">=</span>ON <span class="se">\</span>
  /workspace/examples/open3d-full-port/Open3D
ninja ext_openblas   <span class="c"># openblas first; Open3D's ExternalProject</span>
                     <span class="c"># wiring otherwise asks for libopenblas.a</span>
                     <span class="c"># before it's been staged.</span>
ninja                <span class="c"># the rest (~3-4 minutes)</span>

<span class="c"># Build and run the mirror_bridge binding against it (~17 seconds):</span>
<span class="nb">cd</span> /workspace/examples/open3d-runtime
<span class="nb">export </span><span class="nv">O3D_BUILD</span><span class="o">=</span>/tmp/o3d_fork_build
./build_and_test.sh                 <span class="c"># 5 feature groups pass</span>
</code></pre></div></div>

<p>A note on <code class="language-plaintext highlighter-rouge">LD_PRELOAD</code>. Open3D’s vendored 3rdparty deps (curl, zmq,
parts of VTK) compile with libstdc++ even when the top-level build
requests libc++, so the resulting <code class="language-plaintext highlighter-rouge">libOpen3D.so</code> carries unresolved
<code class="language-plaintext highlighter-rouge">std::__cxx11::*</code> symbols. libc++ and libstdc++ use different symbol
namespaces (<code class="language-plaintext highlighter-rouge">std::__1::</code> vs <code class="language-plaintext highlighter-rouge">std::__cxx11::</code>), so the two can
coexist in one process; <code class="language-plaintext highlighter-rouge">build_and_test.sh</code> preloads libstdc++ so
the loader finds those symbols at dlopen time. This does not affect
the mirror_bridge binding’s ABI, which stays pure libc++.</p>

<h2 id="8-what-it-isnt-yet">8. What it isn’t yet</h2>

<ul>
  <li>
    <p><strong>Compiler support.</strong> Needs C++26 reflection (P2996). That means
clang-p2996 today (pinned in the Docker image) or GCC 15 trunk.
<strong>MSVC hasn’t implemented P2996.</strong> That’s the real portability
ceiling right now.</p>
  </li>
  <li>
    <p><strong>Auto-trampoline is Linux/macOS only</strong> (Itanium ABI). On MSVC the
portable <code class="language-plaintext highlighter-rouge">bind_class&lt;T, Trampoline&gt;</code> path still works: you write a
small trampoline class that forwards each virtual to
<code class="language-plaintext highlighter-rouge">dispatch_python&lt;Ret&gt;("name")</code>. Same behaviour, a few lines per
virtual of hand-written glue.</p>
  </li>
  <li>
    <p><strong>Default argument <em>values</em></strong> aren’t exposed by P2996R13, only
their <em>presence</em>. You can skip trailing defaulted args in a kwargs
call; you can’t skip a middle one and supply a later one.</p>
  </li>
  <li>
    <p><strong>Docstrings.</strong> P2996 doesn’t expose Doxygen comments. Workarounds
exist (<code class="language-plaintext highlighter-rouge">[[=mb::doc("...")]]</code> via P3394, or a parallel parser). Not
shipping today.</p>
  </li>
</ul>

<h2 id="star-the-repo-tell-us-what-to-bind-next">Star the repo, tell us what to bind next</h2>

<p>mirror_bridge is Apache 2.0 at <a href="https://github.com/FranciscoThiesen/mirror_bridge">FranciscoThiesen/mirror_bridge</a>.
It emits Python, Lua and JavaScript bindings today from the same C++
reflection input.</p>

<p><strong>If you’ve ever written a pybind11 <code class="language-plaintext highlighter-rouge">.def()</code> chain for a
hundred-method class and wished there was a better way, this is it.</strong></p>

<p>If the numbers surprised you, run the reproduction and try to break
them. If you hit something that doesn’t work, file an issue. The
repo is small and feedback drives priorities.</p>

<p>⭐ <strong>GitHub</strong>: <a href="https://github.com/FranciscoThiesen/mirror_bridge">github.com/FranciscoThiesen/mirror_bridge</a></p>

<hr />

<p><em>Thanks to the clang-p2996 team at Bloomberg and the P2996 author
community. None of this is possible without their reflection work.</em></p>]]></content><author><name></name></author><category term="Modern C++" /><category term="Python" /><category term="Bindings" /><category term="Reflection" /><category term="C++26" /><category term="Open3D" /><category term="Performance" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">std::hash Boilerplate Is Dead</title><link href="http://chico.dev/Mirror-Hash/" rel="alternate" type="text/html" title="std::hash Boilerplate Is Dead" /><published>2025-12-20T00:00:00+00:00</published><updated>2025-12-20T00:00:00+00:00</updated><id>http://chico.dev/Mirror-Hash</id><content type="html" xml:base="http://chico.dev/Mirror-Hash/"><![CDATA[<h1 id="i-love-to-specialize-hash-functions-for-my-c-classes-no-one-ever">“I Love to Specialize Hash Functions for My C++ Classes” (No One, Ever)</h1>

<p><strong>TL;DR:</strong> <a href="https://github.com/FranciscoThiesen/mirror_hash">mirror_hash</a> uses C++26 reflection to automatically generate hash functions for any class/struct. No macros, no annotations, no manual member lists. A trivially copyable <code class="language-plaintext highlighter-rouge">Point{int, int}</code> hashes in under 2ns. Passes all <a href="https://github.com/rurban/smhasher">SMHasher</a> tests. Works today with <a href="https://github.com/bloomberg/clang-p2996">clang-p2996</a>.</p>

<hr />

<p>Writing <code class="language-plaintext highlighter-rouge">std::hash</code> specializations is one of those C++ rituals that nobody enjoys. You’ve probably done something like:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Point</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">;</span>
    <span class="kt">bool</span> <span class="k">operator</span><span class="o">==</span><span class="p">(</span><span class="k">const</span> <span class="n">Point</span><span class="o">&amp;</span><span class="p">)</span> <span class="k">const</span> <span class="o">=</span> <span class="k">default</span><span class="p">;</span>
<span class="p">};</span>

<span class="c1">// Now you have to write this boilerplate:</span>
<span class="k">template</span><span class="o">&lt;</span><span class="p">&gt;</span>
<span class="k">struct</span> <span class="nc">std</span><span class="o">::</span><span class="n">hash</span><span class="o">&lt;</span><span class="n">Point</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="k">operator</span><span class="p">()(</span><span class="k">const</span> <span class="n">Point</span><span class="o">&amp;</span> <span class="n">p</span><span class="p">)</span> <span class="k">const</span> <span class="k">noexcept</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">seed</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="c1">// boost::hash_combine pattern</span>
        <span class="n">seed</span> <span class="o">^=</span> <span class="n">std</span><span class="o">::</span><span class="n">hash</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">{}(</span><span class="n">p</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">+</span> <span class="mh">0x9e3779b9</span> <span class="o">+</span> <span class="p">(</span><span class="n">seed</span> <span class="o">&lt;&lt;</span> <span class="mi">6</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">seed</span> <span class="o">&gt;&gt;</span> <span class="mi">2</span><span class="p">);</span>
        <span class="n">seed</span> <span class="o">^=</span> <span class="n">std</span><span class="o">::</span><span class="n">hash</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">{}(</span><span class="n">p</span><span class="p">.</span><span class="n">y</span><span class="p">)</span> <span class="o">+</span> <span class="mh">0x9e3779b9</span> <span class="o">+</span> <span class="p">(</span><span class="n">seed</span> <span class="o">&lt;&lt;</span> <span class="mi">6</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">seed</span> <span class="o">&gt;&gt;</span> <span class="mi">2</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">seed</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>This pattern is based on <a href="https://www.boost.org/doc/libs/1_86_0/libs/container_hash/doc/html/hash.html#combine">boost::hash_combine</a>.</p>

<p>For <em>every single type</em> you want to use in an <code class="language-plaintext highlighter-rouge">unordered_map</code> (or any hash table). And when your struct changes? Update the hash function. Forget a member? Silent bugs. Add a pointer to a vector? Now you need to think about whether to hash the pointer or the contents.</p>

<p><strong>What if I told you C++26 can do this automatically?</strong></p>

<h2 id="enter-c26-reflection">Enter C++26 Reflection</h2>

<p>C++26’s reflection proposal (P2996) gives us the ability to introspect types at compile time. We can iterate over struct members, get their names, types, and values, all without macros or code generation.</p>

<p>Here’s the core idea behind <a href="https://github.com/FranciscoThiesen/mirror_hash">mirror_hash</a>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"mirror_hash/mirror_hash.hpp"</span><span class="cp">
</span>
<span class="k">struct</span> <span class="nc">Point</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">;</span>
    <span class="kt">bool</span> <span class="k">operator</span><span class="o">==</span><span class="p">(</span><span class="k">const</span> <span class="n">Point</span><span class="o">&amp;</span><span class="p">)</span> <span class="k">const</span> <span class="o">=</span> <span class="k">default</span><span class="p">;</span>
<span class="p">};</span>

<span class="kt">int</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">Point</span> <span class="n">p</span><span class="p">{</span><span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="mi">30</span><span class="p">};</span>

    <span class="c1">// Just works. No specialization needed.</span>
    <span class="k">auto</span> <span class="n">h</span> <span class="o">=</span> <span class="n">mirror_hash</span><span class="o">::</span><span class="n">hash</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>

    <span class="c1">// Works with standard containers too</span>
    <span class="n">std</span><span class="o">::</span><span class="n">unordered_set</span><span class="o">&lt;</span><span class="n">Point</span><span class="p">,</span> <span class="n">mirror_hash</span><span class="o">::</span><span class="n">hasher</span><span class="o">&lt;&gt;&gt;</span> <span class="n">points</span><span class="p">;</span>
    <span class="n">points</span><span class="p">.</span><span class="n">insert</span><span class="p">({</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>No boilerplate. No manual member enumeration. It just works.</p>

<p>And it’s not limited to simple types. Complex nested structures work automatically:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">User</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">name</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">age</span><span class="p">;</span>
    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">tags</span><span class="p">;</span>
    <span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">email</span><span class="p">;</span>
<span class="p">};</span>

<span class="n">std</span><span class="o">::</span><span class="n">unordered_map</span><span class="o">&lt;</span><span class="n">User</span><span class="p">,</span> <span class="n">Data</span><span class="p">,</span> <span class="n">mirror_hash</span><span class="o">::</span><span class="n">hasher</span><span class="o">&lt;&gt;&gt;</span> <span class="n">users</span><span class="p">;</span>
<span class="n">users</span><span class="p">[{</span><span class="s">"Alice"</span><span class="p">,</span> <span class="mi">30</span><span class="p">,</span> <span class="p">{</span><span class="s">"admin"</span><span class="p">,</span> <span class="s">"dev"</span><span class="p">},</span> <span class="s">"alice@example.com"</span><span class="p">}]</span> <span class="o">=</span> <span class="n">data</span><span class="p">;</span>
</code></pre></div></div>

<p>Strings, vectors, optionals, nested structs: they all hash correctly. No manual enumeration required.</p>

<p>Automatic hashing is actually one of the motivating examples in <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html">P2996 itself</a>, listed as “member-wise hash_append.” The proposal authors clearly saw this use case coming. If you want to take a look at some other projects leveraging reflection you can take a look at <a href="https://chico.dev/Mirror-Bridge">mirror_bridge</a> for automatic binding generation and <a href="https://github.com/simdjson/simdjson">simdjson</a> for <a href="https://github.com/simdjson/simdjson/blob/master/doc/basics.md#c26-static-reflection">reflection-based JSON parsing and serialization</a>.</p>

<p>As Munger warned in <a href="https://www.youtube.com/watch?v=pqzcCfUglws&amp;t=10s">one of my favorite talks</a>: “To a man with a hammer, everything looks like a nail.” At least compile-time reflection is a really cool hammer.</p>

<h2 id="how-it-works-reflection-at-compile-time">How It Works: Reflection at Compile Time</h2>

<p>The magic happens with two new C++26 features working together:</p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">nonstatic_data_members_of(^^T)</code></strong>: Returns reflections of all data members</li>
  <li><strong><code class="language-plaintext highlighter-rouge">template for</code></strong>: Iterates over reflections at compile time (P1306 expansion statements)</li>
</ol>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span><span class="o">&lt;</span><span class="k">typename</span> <span class="nc">Policy</span><span class="p">,</span> <span class="n">Reflectable</span> <span class="n">T</span><span class="p">&gt;</span>
<span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">hash_impl</span><span class="p">(</span><span class="k">const</span> <span class="n">T</span><span class="o">&amp;</span> <span class="n">value</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">result</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="c1">// template for: compile-time iteration over all members</span>
    <span class="k">template</span> <span class="k">for</span> <span class="p">(</span><span class="k">constexpr</span> <span class="k">auto</span> <span class="n">member</span> <span class="o">:</span>
        <span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">nonstatic_data_members_of</span><span class="p">(</span><span class="o">^^</span><span class="n">T</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">access_context</span><span class="o">::</span><span class="n">unchecked</span><span class="p">()))</span>
    <span class="p">{</span>
        <span class="c1">// value.[:member:] splices the reflection to access the actual member</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">Policy</span><span class="o">::</span><span class="n">combine</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">hash_impl</span><span class="o">&lt;</span><span class="n">Policy</span><span class="p">&gt;(</span><span class="n">value</span><span class="p">.[</span><span class="o">:</span><span class="n">member</span><span class="o">:</span><span class="p">]));</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s break down the syntax:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">^^T</code></strong>: The <em>reflection operator</em>. Returns a compile-time handle to type <code class="language-plaintext highlighter-rouge">T</code>’s metadata.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">nonstatic_data_members_of(...)</code></strong>: Returns a range of reflections, one per data member.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">template for</code></strong>: Like a regular <code class="language-plaintext highlighter-rouge">for</code>, but unrolls at compile time. Each iteration is a separate instantiation.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">value.[:member:]</code></strong>: The <em>splice operator</em>. Converts a reflection back into code (in this case, member access).</li>
</ul>

<h2 id="what-about-existing-solutions">What About Existing Solutions?</h2>

<p>Fair question: do we really need C++26 reflection for this? There are existing approaches:</p>

<p><strong><a href="https://www.boost.org/doc/libs/1_87_0/libs/describe/doc/html/describe.html">Boost.Describe</a>:</strong> Requires annotating each struct with a macro:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Point</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">;</span> <span class="p">};</span>
<span class="n">BOOST_DESCRIBE_STRUCT</span><span class="p">(</span><span class="n">Point</span><span class="p">,</span> <span class="p">(),</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span>  <span class="c1">// Manual member list</span>
</code></pre></div></div>
<p>Works well, but you must remember to update the macro when adding members. Miss one, and you get silent bugs.</p>

<p><strong><a href="https://github.com/felixguendling/cista">Cista</a>:</strong> Requires a <code class="language-plaintext highlighter-rouge">cista_members()</code> function in each type:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Point</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">;</span>
    <span class="k">auto</span> <span class="n">cista_members</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">tie</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span> <span class="p">}</span>  <span class="c1">// Manual</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Same issue: manual enumeration that can drift out of sync.</p>

<p><strong><a href="https://abseil.io/docs/cpp/guides/hash">Abseil Hash</a>:</strong> Requires a friend function:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Point</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">;</span>
    <span class="k">template</span><span class="o">&lt;</span><span class="k">typename</span> <span class="nc">H</span><span class="p">&gt;</span>
    <span class="k">friend</span> <span class="n">H</span> <span class="n">AbslHashValue</span><span class="p">(</span><span class="n">H</span> <span class="n">h</span><span class="p">,</span> <span class="k">const</span> <span class="n">Point</span><span class="o">&amp;</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">H</span><span class="o">::</span><span class="n">combine</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">move</span><span class="p">(</span><span class="n">h</span><span class="p">),</span> <span class="n">p</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="n">y</span><span class="p">);</span>  <span class="c1">// Manual</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The pattern is clear: all existing solutions require <strong>manual member enumeration</strong>. You’re still writing boilerplate, just in a different form. Add a member, forget to update the hash, get subtle bugs.</p>

<p><strong>mirror_hash is different.</strong> With C++26 reflection, the compiler provides the member list. There’s nothing to keep in sync:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Point</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">;</span> <span class="p">};</span>  <span class="c1">// Add z, hash automatically includes it</span>
<span class="k">auto</span> <span class="n">h</span> <span class="o">=</span> <span class="n">mirror_hash</span><span class="o">::</span><span class="n">hash</span><span class="p">(</span><span class="n">Point</span><span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">});</span>  <span class="c1">// Just works</span>
</code></pre></div></div>

<p>This is the fundamental advantage: <strong>true automatic generation</strong> with zero annotation overhead.</p>

<h2 id="the-key-insight-hash-only-what-you-use">The Key Insight: Hash Only What You Use</h2>

<p>One concern with automatic hashing: “Won’t this generate code for every type in my program?”</p>

<p><strong>No.</strong> C++ templates are lazy. Hash functions are only instantiated when you actually use them:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">NeverHashed</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">name</span><span class="p">;</span>
    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">data</span><span class="p">;</span>
<span class="p">};</span>

<span class="k">struct</span> <span class="nc">ActuallyHashed</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">id</span><span class="p">;</span>
    <span class="kt">double</span> <span class="n">value</span><span class="p">;</span>
<span class="p">};</span>

<span class="kt">int</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// This instantiates hash code for ActuallyHashed only</span>
    <span class="n">std</span><span class="o">::</span><span class="n">unordered_set</span><span class="o">&lt;</span><span class="n">ActuallyHashed</span><span class="p">,</span> <span class="n">mirror_hash</span><span class="o">::</span><span class="n">hasher</span><span class="o">&lt;&gt;&gt;</span> <span class="n">set</span><span class="p">;</span>

    <span class="c1">// NeverHashed gets no hash code generated</span>
    <span class="n">NeverHashed</span> <span class="n">unused</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The compiler only generates <code class="language-plaintext highlighter-rouge">hash_impl&lt;Policy, ActuallyHashed&gt;</code>. The <code class="language-plaintext highlighter-rouge">NeverHashed</code> struct incurs zero overhead because we never hash it.</p>

<p>This is the beauty of C++ templates: you don’t pay for what you don’t use. Reflection extends this principle: metadata is queried at compile time, and only for types that actually need it. See <a href="https://github.com/FranciscoThiesen/mirror_hash/blob/main/tests/test_mirror_hash.cpp"><code class="language-plaintext highlighter-rouge">lazy_instantiation</code> in test_mirror_hash.cpp</a> for the test that verifies this behavior.</p>

<h2 id="handling-non-trivially-copyable-types">Handling Non-Trivially Copyable Types</h2>

<p>A critical design decision in any hashing library: how do you handle types like <code class="language-plaintext highlighter-rouge">std::string</code>, <code class="language-plaintext highlighter-rouge">std::vector</code>, or structs containing them?</p>

<p>mirror_hash handles this through <strong>concept-based dispatch</strong>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Non-trivially copyable types get per-member hashing</span>
<span class="k">template</span><span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="k">concept</span> <span class="n">Reflectable</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">is_class_v</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&amp;&amp;</span>
    <span class="o">!</span><span class="n">StringLike</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">Container</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">Optional</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">Variant</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">;</span>

<span class="c1">// Strings hash their contents, not their internal pointers</span>
<span class="k">template</span><span class="o">&lt;</span><span class="k">typename</span> <span class="nc">Policy</span><span class="p">,</span> <span class="n">StringLike</span> <span class="n">T</span><span class="p">&gt;</span>
<span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="nf">hash_impl</span><span class="p">(</span><span class="k">const</span> <span class="n">T</span><span class="o">&amp;</span> <span class="n">value</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">hash</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">{}(</span><span class="n">value</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Containers iterate over elements</span>
<span class="k">template</span><span class="o">&lt;</span><span class="k">typename</span> <span class="nc">Policy</span><span class="p">,</span> <span class="n">Container</span> <span class="n">T</span><span class="p">&gt;</span>
<span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="nf">hash_impl</span><span class="p">(</span><span class="k">const</span> <span class="n">T</span><span class="o">&amp;</span> <span class="n">value</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">result</span> <span class="o">=</span> <span class="n">hash_impl</span><span class="o">&lt;</span><span class="n">Policy</span><span class="o">&gt;</span><span class="p">(</span><span class="n">value</span><span class="p">.</span><span class="n">size</span><span class="p">());</span>
    <span class="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">elem</span> <span class="o">:</span> <span class="n">value</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">Policy</span><span class="o">::</span><span class="n">combine</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">hash_impl</span><span class="o">&lt;</span><span class="n">Policy</span><span class="o">&gt;</span><span class="p">(</span><span class="n">elem</span><span class="p">));</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When you hash a struct containing a <code class="language-plaintext highlighter-rouge">std::string</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Person</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">name</span><span class="p">;</span>  <span class="c1">// Non-trivially copyable!</span>
    <span class="kt">int</span> <span class="n">age</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>mirror_hash recursively hashes each member. For <code class="language-plaintext highlighter-rouge">name</code>, it hashes the string <em>contents</em>. For <code class="language-plaintext highlighter-rouge">age</code>, it hashes the integer directly. This is safe and correct for complex types.</p>

<h2 id="the-fast-path-trivially-copyable-types">The Fast Path: Trivially Copyable Types</h2>

<p>For simple structs like <code class="language-plaintext highlighter-rouge">Point</code>, we can do much better. If all members are trivially copyable (ints, floats, no pointers), we can hash the entire struct as raw bytes:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Check if all members are trivially copyable using template for</span>
<span class="k">template</span><span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="k">consteval</span> <span class="kt">bool</span> <span class="nf">all_members_trivially_copyable</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">template</span> <span class="k">for</span> <span class="p">(</span><span class="k">constexpr</span> <span class="k">auto</span> <span class="n">member</span> <span class="o">:</span>
        <span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">nonstatic_data_members_of</span><span class="p">(</span><span class="o">^^</span><span class="n">T</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">access_context</span><span class="o">::</span><span class="n">unchecked</span><span class="p">()))</span>
    <span class="p">{</span>
        <span class="c1">// [:type_of(member):] splices the member's type</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">std</span><span class="o">::</span><span class="n">is_trivially_copyable_v</span><span class="o">&lt;</span><span class="p">[</span><span class="o">:</span><span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">type_of</span><span class="p">(</span><span class="n">member</span><span class="p">)</span><span class="o">:</span><span class="p">]&gt;)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">template</span><span class="o">&lt;</span><span class="k">typename</span> <span class="nc">Policy</span><span class="p">,</span> <span class="n">Reflectable</span> <span class="n">T</span><span class="p">&gt;</span>
<span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">hash_impl</span><span class="p">(</span><span class="k">const</span> <span class="n">T</span><span class="o">&amp;</span> <span class="n">value</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
    <span class="c1">// Fast path: if all members are trivially copyable, hash raw bytes</span>
    <span class="k">if</span> <span class="k">constexpr</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">is_trivially_copyable_v</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&amp;&amp;</span> <span class="n">all_members_trivially_copyable</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">())</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">hash_bytes_fixed</span><span class="o">&lt;</span><span class="n">Policy</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">T</span><span class="p">)</span><span class="o">&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="n">value</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// Slow path: iterate over members with template for</span>
    <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">result</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">template</span> <span class="k">for</span> <span class="p">(</span><span class="k">constexpr</span> <span class="k">auto</span> <span class="n">member</span> <span class="o">:</span> <span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">nonstatic_data_members_of</span><span class="p">(</span><span class="o">^^</span><span class="n">T</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">Policy</span><span class="o">::</span><span class="n">combine</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">hash_impl</span><span class="o">&lt;</span><span class="n">Policy</span><span class="p">&gt;(</span><span class="n">value</span><span class="p">.[</span><span class="o">:</span><span class="n">member</span><span class="o">:</span><span class="p">]));</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This compile-time optimization means a <code class="language-plaintext highlighter-rouge">struct { int x, y, z; }</code> gets hashed as a single 12-byte block, not three separate hash operations combined.</p>

<h2 id="under-the-hood-the-byte-hashing-layer">Under the Hood: The Byte Hashing Layer</h2>

<p>This library has two distinct parts:</p>

<ol>
  <li><strong>The reflection layer</strong> (novel): Automatically discovers struct members at compile time</li>
  <li><strong>The byte hashing layer</strong> (borrowed): Converts bytes to hash values using proven algorithms</li>
</ol>

<blockquote>
  <p><strong>Disclaimer:</strong> I’m not a cryptography expert. The byte hashing in mirror_hash comes from people who actually know what they’re doing. I make no cryptographic claims. For hash tables and checksums, this should be fine. For security-sensitive applications, please use a proper cryptographic hash.</p>
</blockquote>

<p><img src="/images/i-have-no-idea-what-im-doing.jpg" alt="I have no idea what I'm doing" /></p>

<p>The byte hashing is built on:</p>

<ul>
  <li><strong><a href="https://github.com/Nicoshev/rapidhash">rapidhash</a></strong> by Nicolas De Carli: What mirror_hash uses under the hood</li>
  <li><strong><a href="https://github.com/wangyi-fudan/wyhash">wyhash</a></strong> by Wang Yi: The foundation that rapidhash builds on</li>
  <li><strong><a href="https://github.com/ogxd/gxhash">GxHash</a></strong> by ogxd: Inspiration for the ARM64 AES optimization</li>
</ul>

<p><strong>Note on architecture:</strong> The reflection layer and byte hashing layer are completely independent. The reflection machinery discovers struct members and serializes them; the byte hasher converts those bytes to a hash value. You could swap rapidhash for xxHash, CityHash, or even SHA-256 if you needed different properties (portability, cryptographic strength, etc.). The AES optimization is just one choice we made for ARM64 performance - it’s not fundamental to the approach. The main contribution here is the <em>integration layer</em>: using C++26 reflection to automatically feed your struct’s bytes into these proven algorithms.</p>

<p><strong>Using a different hash function:</strong> mirror_hash uses a policy-based design. You can plug in any hash function by defining a policy with <code class="language-plaintext highlighter-rouge">mix</code> and <code class="language-plaintext highlighter-rouge">combine</code> methods:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Define a custom policy wrapping your preferred hash</span>
<span class="k">struct</span> <span class="nc">my_xxhash_policy</span> <span class="p">{</span>
    <span class="k">static</span> <span class="k">constexpr</span> <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">mix</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">k</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
        <span class="c1">// Your finalization logic (e.g., XXH3_64bits avalanche)</span>
        <span class="n">k</span> <span class="o">^=</span> <span class="n">k</span> <span class="o">&gt;&gt;</span> <span class="mi">33</span><span class="p">;</span>
        <span class="n">k</span> <span class="o">*=</span> <span class="mh">0xff51afd7ed558ccdULL</span><span class="p">;</span>
        <span class="n">k</span> <span class="o">^=</span> <span class="n">k</span> <span class="o">&gt;&gt;</span> <span class="mi">33</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">k</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">static</span> <span class="k">constexpr</span> <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">combine</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">seed</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">value</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
        <span class="c1">// Your combining logic</span>
        <span class="k">return</span> <span class="n">seed</span> <span class="o">^</span> <span class="p">(</span><span class="n">value</span> <span class="o">+</span> <span class="mh">0x9e3779b9</span> <span class="o">+</span> <span class="p">(</span><span class="n">seed</span> <span class="o">&lt;&lt;</span> <span class="mi">6</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">seed</span> <span class="o">&gt;&gt;</span> <span class="mi">2</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="c1">// Use it with any struct</span>
<span class="k">struct</span> <span class="nc">Point</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">;</span> <span class="p">};</span>
<span class="k">auto</span> <span class="n">h</span> <span class="o">=</span> <span class="n">mirror_hash</span><span class="o">::</span><span class="n">hash</span><span class="o">&lt;</span><span class="n">my_xxhash_policy</span><span class="o">&gt;</span><span class="p">(</span><span class="n">Point</span><span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">});</span>

<span class="c1">// Or with containers</span>
<span class="n">std</span><span class="o">::</span><span class="n">unordered_set</span><span class="o">&lt;</span><span class="n">Point</span><span class="p">,</span> <span class="n">mirror_hash</span><span class="o">::</span><span class="n">hasher</span><span class="o">&lt;</span><span class="n">my_xxhash_policy</span><span class="o">&gt;&gt;</span> <span class="n">points</span><span class="p">;</span>
</code></pre></div></div>

<p>mirror_hash ships with several built-in policies: <code class="language-plaintext highlighter-rouge">mirror_hash_policy</code> (default, with AES acceleration on ARM64), <code class="language-plaintext highlighter-rouge">folly_policy</code>, <code class="language-plaintext highlighter-rouge">wyhash_policy</code>, <code class="language-plaintext highlighter-rouge">murmur3_policy</code>, <code class="language-plaintext highlighter-rouge">xxhash3_policy</code>, <code class="language-plaintext highlighter-rouge">fnv1a_policy</code>, and <code class="language-plaintext highlighter-rouge">aes_policy</code>. For bulk byte hashing, you can also specialize <code class="language-plaintext highlighter-rouge">hash_bytes&lt;YourPolicy&gt;</code> to use your preferred algorithm for raw memory.</p>

<p><strong>The Core Mixing Primitive:</strong></p>

<p>This is the one thing I didn’t touch. It’s the heart of rapidhash/wyhash:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">inline</span> <span class="kt">void</span> <span class="nf">mum</span><span class="p">(</span><span class="kt">uint64_t</span><span class="o">*</span> <span class="n">A</span><span class="p">,</span> <span class="kt">uint64_t</span><span class="o">*</span> <span class="n">B</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
    <span class="n">__uint128_t</span> <span class="n">r</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="n">__uint128_t</span><span class="o">&gt;</span><span class="p">(</span><span class="o">*</span><span class="n">A</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="o">*</span><span class="n">B</span><span class="p">);</span>
    <span class="o">*</span><span class="n">A</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">r</span><span class="p">);</span>       <span class="c1">// Low 64 bits</span>
    <span class="o">*</span><span class="n">B</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">r</span> <span class="o">&gt;&gt;</span> <span class="mi">64</span><span class="p">);</span> <span class="c1">// High 64 bits</span>
<span class="p">}</span>

<span class="kr">inline</span> <span class="kt">uint64_t</span> <span class="nf">mix</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="n">A</span><span class="p">,</span> <span class="kt">uint64_t</span> <span class="n">B</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
    <span class="n">mum</span><span class="p">(</span><span class="o">&amp;</span><span class="n">A</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">B</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">A</span> <span class="o">^</span> <span class="n">B</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A 64×64→128 bit multiply has excellent avalanche properties: each input bit affects many output bits. The XOR of both 64-bit halves is crucial - it ensures entropy from the high bits (which contain information about large-magnitude products) mixes with the low bits (which preserve fine-grained patterns). Without this recombination, you’d lose half the information. This is well-studied math. I just use it.</p>

<p><img src="/images/mirror-hash-instructions.png" alt="Instruction efficiency: multiply vs AES" />
<em>Per 16 bytes of mixing: AES uses fewer instructions and cycles than 128-bit multiply. On Apple Silicon, AESE+AESMC fuse into a single ~2-cycle operation.</em></p>

<h3 id="arm64-with-hardware-aes">ARM64 with Hardware AES</h3>

<p>On ARM64 processors with AES crypto extensions (Apple Silicon, AWS Graviton), mirror_hash uses a hybrid strategy:</p>

<table>
  <thead>
    <tr>
      <th>Size</th>
      <th>rapidhash</th>
      <th>mirror_hash</th>
      <th>vs rapidhash</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>8B</td>
      <td>1.34 ns</td>
      <td>1.73 ns</td>
      <td>-22% (dispatch overhead)</td>
    </tr>
    <tr>
      <td>32B</td>
      <td>1.87 ns</td>
      <td>1.88 ns</td>
      <td>~even (uses rapidhashNano)</td>
    </tr>
    <tr>
      <td>64B</td>
      <td>2.51 ns</td>
      <td><strong>2.41 ns</strong></td>
      <td>+4%</td>
    </tr>
    <tr>
      <td>128B</td>
      <td>4.17 ns</td>
      <td><strong>3.47 ns</strong></td>
      <td>+20%</td>
    </tr>
    <tr>
      <td>512B</td>
      <td>11.87 ns</td>
      <td><strong>5.89 ns</strong></td>
      <td>+101%</td>
    </tr>
    <tr>
      <td>1KB</td>
      <td>20.55 ns</td>
      <td><strong>9.69 ns</strong></td>
      <td>+112%</td>
    </tr>
    <tr>
      <td>4KB</td>
      <td>79.88 ns</td>
      <td><strong>32.59 ns</strong></td>
      <td>+145%</td>
    </tr>
    <tr>
      <td>8KB</td>
      <td>151.39 ns</td>
      <td><strong>61.58 ns</strong></td>
      <td>+146%</td>
    </tr>
  </tbody>
</table>

<p><img src="/images/mirror-hash-throughput.png" alt="Throughput comparison: mirror_hash vs rapidhash vs GxHash" />
<em>Throughput across input sizes. Small inputs (8-16B) have ~22% overhead from dispatch logic, while 24-32B is roughly even. From 33-128B, mirror_hash wins ~93% of sizes with ~20% average speedup. Above 128B, 8-way AES kicks in (67-146% faster, with 100%+ at 512B and above).</em></p>

<p><strong>Why is AES faster for medium inputs?</strong> ARM64’s AESE+AESMC instructions can mix 16 bytes in ~2 cycles using dedicated silicon. By comparison, the 128-bit multiply at the heart of rapidhash has data dependencies that limit throughput - each multiply must wait for the previous one. AES operations are independent, so we can run 4 or 8 of them in parallel on separate data lanes while the CPU’s execution units stay busy.</p>

<p><strong>Why not use AES for everything?</strong> AES has setup overhead (loading keys, initializing state vectors) that dominates when you’re only hashing 8 bytes. For tiny inputs, the dispatch logic to choose between code paths adds overhead - rapidhashNano is fast, but calling it through mirror_hash’s size-checking dispatch costs ~22% at 8-16 bytes.</p>

<p>The hybrid approach:</p>
<ul>
  <li><strong>0-32 bytes</strong>: rapidhashNano (AES setup overhead not amortized for tiny inputs)</li>
  <li><strong>33-128 bytes</strong>: Single-state AES with 32-byte unrolling and overlapping read (~20% faster than rapidhash on average, wins 93% of sizes)</li>
  <li><strong>129-512B</strong>: 8-way unrolled AES (67-100% faster as parallelism amortizes)</li>
  <li><strong>512B-8KB</strong>: Full 8-way AES acceleration (100-146% faster than rapidhash)</li>
  <li><strong>&gt;8KB</strong>: rapidhash (memory bandwidth dominates)</li>
</ul>

<p><strong>The 33-128 byte range uses an optimized single-state approach.</strong> Instead of 4-way parallel processing (which has setup overhead), mirror_hash v2.1 uses 32-byte unrolled processing with a single accumulator.</p>

<p>For handling the 1-15 byte remainder after aligned blocks, mirror_hash uses an <strong>overlapping read</strong>: instead of copying the remaining bytes to a zeroed buffer, it reads the last 16 bytes of the input—which may overlap with already-processed data:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Traditional approach (slower):</span>
<span class="k">alignas</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span> <span class="kt">uint8_t</span> <span class="n">buf</span><span class="p">[</span><span class="mi">16</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>  <span class="c1">// stack alloc + zero init</span>
<span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">ptr</span><span class="p">,</span> <span class="n">remainder_len</span><span class="p">);</span>     <span class="c1">// copy 1-15 bytes</span>
<span class="n">buf</span><span class="p">[</span><span class="mi">15</span><span class="p">]</span> <span class="o">=</span> <span class="n">remainder_len</span><span class="p">;</span>             <span class="c1">// encode length</span>

<span class="c1">// Overlapping read (faster):</span>
<span class="n">uint8x16_t</span> <span class="n">data</span> <span class="o">=</span> <span class="n">vld1q_u8</span><span class="p">(</span><span class="n">end</span> <span class="o">-</span> <span class="mi">16</span><span class="p">);</span>           <span class="c1">// single 16-byte load</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">veorq_u8</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">vdupq_n_u8</span><span class="p">(</span><span class="n">remainder_len</span><span class="p">));</span> <span class="c1">// XOR length into every byte</span>
</code></pre></div></div>

<p>The XOR with remainder length ensures different-length inputs hash differently, even when they share the overlapping bytes. Benchmarking shows this is <strong>2x faster</strong> than the memcpy approach for handling partial blocks—the stack allocation, zero-initialization, and variable-length copy add significant overhead compared to a single 16-byte load. Measured results:</p>

<table>
  <thead>
    <tr>
      <th>Size</th>
      <th>Winner</th>
      <th>Speedup</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>48B</td>
      <td>mirror_hash</td>
      <td>+9%</td>
    </tr>
    <tr>
      <td>64B</td>
      <td>mirror_hash</td>
      <td>+4%</td>
    </tr>
    <tr>
      <td>80B</td>
      <td>mirror_hash</td>
      <td>+24%</td>
    </tr>
    <tr>
      <td>96B</td>
      <td>mirror_hash</td>
      <td>+12%</td>
    </tr>
    <tr>
      <td>112B</td>
      <td>mirror_hash</td>
      <td>+31%</td>
    </tr>
    <tr>
      <td>128B</td>
      <td>mirror_hash</td>
      <td>+20%</td>
    </tr>
  </tbody>
</table>

<p>mirror_hash wins consistently across the 33-128 byte range regardless of alignment.</p>

<p><img src="/images/mirror-hash-speedup.png" alt="Speedup of mirror_hash vs rapidhash" />
<em>mirror_hash speedup at key sizes. Green = mirror_hash wins, Blue = rapidhash wins. Small inputs (8-16B) show dispatch overhead. From 48B onward, AES acceleration provides consistent wins.</em></p>

<p><img src="/images/mirror-hash-latency.png" alt="Latency comparison: small vs large inputs" />
<em>Left: Small inputs show dispatch overhead at 8-16B, roughly even at 24-32B. Right: Large inputs (128B-8KB) show AES acceleration dominating with 18-146% speedups.</em></p>

<h3 id="why-mirror_hash-beats-gxhash-at-large-inputs">Why mirror_hash Beats GxHash at Large Inputs</h3>

<p>Wait, mirror_hash beats GxHash by 23% at 8KB? They both use hardware AES. What’s going on?</p>

<p>The difference comes down to <strong>instruction count per compression step</strong>. Looking at the actual assembly:</p>

<p><strong>GxHash’s fast compression (4 instructions):</strong></p>
<pre><code class="language-asm">movi  v2.2d, #0           ; create zero vector
aese  v0.16b, v2.16b      ; AESE with zero key
aesmc v0.16b, v0.16b      ; MixColumns
eor   v0.16b, v0.16b, v1  ; XOR with input b
</code></pre>

<p><strong>mirror_hash’s fast compression (2 instructions):</strong></p>
<pre><code class="language-asm">aese  v0.16b, v1.16b      ; AESE with b as key
aesmc v0.16b, v0.16b      ; MixColumns
</code></pre>

<p>The key insight: ARM64’s <code class="language-plaintext highlighter-rouge">AESE</code> instruction incorporates the key XOR into the operation. GxHash does <code class="language-plaintext highlighter-rouge">AESE(a, 0)</code> then <code class="language-plaintext highlighter-rouge">XOR(result, b)</code> as separate steps. mirror_hash does <code class="language-plaintext highlighter-rouge">AESE(a, b)</code> directly, saving two instructions per compression.</p>

<p>At 8KB, we do 64 iterations of 128-byte blocks, with 7 fast compressions per iteration:</p>
<ul>
  <li><strong>GxHash</strong>: 64 × 7 × 4 = <strong>1792 instructions</strong></li>
  <li><strong>mirror_hash</strong>: 64 × 7 × 2 = <strong>896 instructions</strong></li>
</ul>

<p>That’s 50% fewer instructions for the fast compression step alone.</p>

<p>There’s also a secondary factor: <strong>key loading</strong>. GxHash loads keys inside its compression function on every call. mirror_hash loads keys once before the loop and keeps them in NEON registers:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// GxHash: loads keys every time</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="n">state</span> <span class="nf">compress</span><span class="p">(</span><span class="n">state</span> <span class="n">a</span><span class="p">,</span> <span class="n">state</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">uint8x16_t</span> <span class="n">k1</span> <span class="o">=</span> <span class="n">vld1q_u32</span><span class="p">(</span><span class="n">keys_1</span><span class="p">);</span>  <span class="c1">// Load from memory</span>
    <span class="n">uint8x16_t</span> <span class="n">k2</span> <span class="o">=</span> <span class="n">vld1q_u32</span><span class="p">(</span><span class="n">keys_2</span><span class="p">);</span>  <span class="c1">// Load from memory</span>
    <span class="c1">// ... use k1, k2</span>
<span class="p">}</span>

<span class="c1">// mirror_hash: keys loaded once, passed as parameters</span>
<span class="n">uint8x16_t</span> <span class="n">k1</span> <span class="o">=</span> <span class="n">vld1q_u8</span><span class="p">(</span><span class="n">KEY1</span><span class="p">);</span>  <span class="c1">// Once before loop</span>
<span class="n">uint8x16_t</span> <span class="n">k2</span> <span class="o">=</span> <span class="n">vld1q_u8</span><span class="p">(</span><span class="n">KEY2</span><span class="p">);</span>
<span class="k">while</span> <span class="p">(</span><span class="n">len</span> <span class="o">&gt;=</span> <span class="mi">128</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">hash</span> <span class="o">=</span> <span class="n">compress_full</span><span class="p">(</span><span class="n">hash</span><span class="p">,</span> <span class="n">v0</span><span class="p">,</span> <span class="n">k1</span><span class="p">,</span> <span class="n">k2</span><span class="p">);</span>  <span class="c1">// k1, k2 in registers</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For 8KB, that’s 64 × 2 = 128 extra memory loads avoided.</p>

<p><strong>Measured results</strong> (ARM64, native Docker):</p>
<ul>
  <li>GxHash-style: 96.84 ns</li>
  <li>mirror_hash-style: 79.08 ns</li>
  <li><strong>Speedup: +22.5%</strong></li>
</ul>

<p>The full profiling is in <a href="https://github.com/FranciscoThiesen/mirror_hash/blob/main/profiling/aes_deep_analysis.cpp">profiling/aes_deep_analysis.cpp</a>.</p>

<p>See <a href="https://github.com/FranciscoThiesen/mirror_hash/blob/main/benchmarks/gxhash_comparison.cpp">benchmarks/gxhash_comparison.cpp</a> for the full benchmark. All measurements from an M3 Max MacBook Pro.</p>

<h2 id="quality-validation-smhasher">Quality Validation: SMHasher</h2>

<p>Quality matters as much as speed. A fast hash that produces collisions is useless.</p>

<p>mirror_hash passes the full <a href="https://github.com/rurban/smhasher">SMHasher</a> test suite - the canonical hash function quality benchmark. This isn’t a subset or simplified version; it’s the real thing, taking 30+ minutes to run all tests:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>smhasher_upstream/build
./SMHasher mirror_hash_unified
</code></pre></div></div>

<p><strong>Key test results:</strong></p>

<table>
  <thead>
    <tr>
      <th>Test Category</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Sanity checks</strong></td>
      <td>PASS</td>
    </tr>
    <tr>
      <td><strong>Avalanche</strong></td>
      <td>All bit sizes show ~50% flip rate</td>
    </tr>
    <tr>
      <td><strong>Sparse keys</strong></td>
      <td>No collisions across all key sizes</td>
    </tr>
    <tr>
      <td><strong>Permutation</strong></td>
      <td>No collisions in combination tests</td>
    </tr>
    <tr>
      <td><strong>Cyclic collisions</strong></td>
      <td>PASS</td>
    </tr>
    <tr>
      <td><strong>TwoBytes</strong></td>
      <td>PASS</td>
    </tr>
    <tr>
      <td><strong>Text</strong></td>
      <td>PASS</td>
    </tr>
    <tr>
      <td><strong>Zeroes</strong></td>
      <td>PASS</td>
    </tr>
    <tr>
      <td><strong>Seed</strong></td>
      <td>PASS</td>
    </tr>
    <tr>
      <td><strong>MomentChi2</strong></td>
      <td>“Great”</td>
    </tr>
    <tr>
      <td><strong>BadSeeds</strong></td>
      <td>0x0 PASS</td>
    </tr>
    <tr>
      <td><strong>Prng</strong></td>
      <td>PASS</td>
    </tr>
  </tbody>
</table>

<p>The hash is integrated directly into SMHasher’s source tree (<code class="language-plaintext highlighter-rouge">smhasher_upstream/</code>), so you can run the tests yourself.</p>

<h2 id="benchmarks">Benchmarks</h2>

<p>How fast is the reflection-based approach? Here are measured times on ARM64 (M3 Max, Clang 21, <code class="language-plaintext highlighter-rouge">-O3</code>):</p>

<table>
  <thead>
    <tr>
      <th>Struct Type</th>
      <th>Time</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Point{int, int}</code></td>
      <td>~1.7 ns</td>
      <td>Trivially copyable, raw bytes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Person{string(5), int}</code></td>
      <td>~4.6 ns</td>
      <td>Short string (SSO, inline storage)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Person{string(32), int}</code></td>
      <td>~6.1 ns</td>
      <td>Longer string (heap-allocated, 32% slower)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Data{vector(5), string}</code></td>
      <td>~8 ns</td>
      <td>Must iterate 5 elements</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Data{vector(100), string}</code></td>
      <td>~20 ns</td>
      <td>Must iterate 100 elements</td>
    </tr>
  </tbody>
</table>

<p>The time scales with the actual work required:</p>
<ul>
  <li><strong>String hashing</strong> depends on string length (though <a href="https://devblogs.microsoft.com/oldnewthing/20240510-00/?p=109742">SSO, or Small String Optimization</a>, helps for short strings by storing them inline)</li>
  <li><strong>Container hashing</strong> iterates over all elements; there’s no shortcut</li>
  <li><strong>Member count</strong> adds overhead for the combine operations</li>
</ul>

<p>This isn’t “overhead” in the sense of wasted work. It’s the inherent cost of correctly hashing dynamic data. A hand-written hash function would have the same cost. The value of mirror_hash is that you don’t have to write it yourself.</p>

<p>Run the benchmark yourself:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./build/benchmark
</code></pre></div></div>

<h2 id="the-catch-you-need-a-special-compiler-for-now">The Catch: You Need a Special Compiler (for now)</h2>

<p>C++26 reflection isn’t in any released compiler yet. You need Bloomberg’s <a href="https://github.com/bloomberg/clang-p2996">clang-p2996</a> branch:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Compile flags</span>
clang++ <span class="nt">-std</span><span class="o">=</span>c++2c <span class="nt">-freflection</span> <span class="nt">-freflection-latest</span> mycode.cpp
</code></pre></div></div>

<p>It’s experimental. It might break. But it’s a glimpse of C++’s future, and that future involves a lot less boilerplate.</p>

<h2 id="when-not-to-use-this">When NOT to Use This</h2>

<p>mirror_hash isn’t always the right choice:</p>

<ul>
  <li><strong>Cryptographic hashing</strong>: Use SHA-256, BLAKE3, etc. mirror_hash is for hash tables, not security.</li>
  <li><strong>Custom hash semantics</strong>: If only some fields should contribute to the hash (e.g., skip cached values), you need manual control.</li>
  <li><strong>Cross-platform determinism</strong>: Hash values may differ between platforms or compiler versions. Don’t persist them.</li>
  <li><strong>Pre-C++26 codebases</strong>: Requires experimental compiler support that may not be production-ready.</li>
</ul>

<p>For these cases, stick with hand-written <code class="language-plaintext highlighter-rouge">std::hash</code> specializations or established libraries.</p>

<h2 id="try-it-out">Try It Out</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/FranciscoThiesen/mirror_hash
<span class="nb">cd </span>mirror_hash
./start_dev_container.sh  <span class="c"># Uses Docker with clang-p2996</span>
./build.sh
./run_tests.sh
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>What started as “let’s avoid writing <code class="language-plaintext highlighter-rouge">std::hash</code> boilerplate” turned into a journey through:</p>

<ol>
  <li>C++26 reflection and <code class="language-plaintext highlighter-rouge">template for</code></li>
  <li>Trivially copyable optimizations</li>
  <li>128-bit multiply mixing (rapidhash)</li>
  <li>Hardware AES acceleration</li>
  <li>Memory bandwidth analysis</li>
</ol>

<p>The days of manually writing <code class="language-plaintext highlighter-rouge">std::hash</code> specializations are numbered. C++26 reflection gives us the tools to generate them automatically, and with the right platform-specific optimizations, performance can be excellent too.</p>

<p>For years, we’ve written code that the compiler could infer. We’ve manually enumerated members that the compiler already knows about. P2996 finally bridges that gap.</p>

<p>The future of C++ is less boilerplate and more automation. And I, for one, can’t wait.</p>

<hr />

<p><em>mirror_hash is MIT licensed. The underlying hash algorithms (wyhash, rapidhash, xxHash) have their own licenses. Please check their respective repositories.</em></p>

<h2 id="acknowledgments">Acknowledgments</h2>

<p>Thanks to Wyatt Childers, Peter Dimov, Dan Katz, Barry Revzin, Andrew Sutton, Faisal Vali, and Daveed Vandevoorde for authoring and championing P2996, the reflection proposal that makes this all possible.</p>

<p>Special thanks to Dan Katz for maintaining the clang-p2996 branch. Without a working compiler, this would all be theoretical.</p>

<p>On the hashing side: thanks to Wang Yi for creating wyhash, which has become the foundation of modern fast hashing. Thanks to Nicolas De Carli for rapidhash, the fastest portable hash function I’ve tested. Thanks to ogxd for GxHash, which showed me how to use hardware AES for hashing and whose 8-way unrolling strategy mirror_hash adapts. And thanks to Yann Collet for xxHash, which has been battle-tested for over a decade and set the standard for what a high-quality hash function should be.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html">P2996R13 - Reflection for C++26</a>: The main reflection proposal</li>
  <li><a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p1306r5.html">P1306R5 - Expansion Statements</a>: The <code class="language-plaintext highlighter-rouge">template for</code> feature</li>
  <li><a href="https://github.com/bloomberg/clang-p2996">Bloomberg clang-p2996</a>: Experimental compiler with reflection support</li>
  <li><a href="https://herbsutter.com/2025/06/21/trip-report-june-2025-iso-c-standards-meeting-sofia-bulgaria/">Trip Report: June 2025 ISO C++ Meeting</a>: When reflection was voted into C++26</li>
  <li><a href="https://github.com/rurban/smhasher">SMHasher</a>: The canonical hash quality test suite</li>
  <li><a href="https://github.com/Nicoshev/rapidhash">rapidhash</a>: The hash algorithm that mirror_hash builds upon</li>
  <li><a href="https://github.com/wangyi-fudan/wyhash">wyhash</a>: The foundation of modern fast hashing</li>
  <li><a href="https://github.com/ogxd/gxhash">GxHash</a>: Hardware AES-accelerated hashing that inspired the ARM64 optimization</li>
</ul>]]></content><author><name></name></author><category term="Modern" /><category term="C++," /><category term="Hashing," /><category term="Reflection," /><category term="C++26," /><category term="Performance" /><summary type="html"><![CDATA[“I Love to Specialize Hash Functions for My C++ Classes” (No One, Ever)]]></summary></entry><entry><title type="html">mirror_bridge: Outrunning V8, PyPy, and LuaJIT with C++26 Reflection</title><link href="http://chico.dev/Mirror-Bridge-Multi-Language/" rel="alternate" type="text/html" title="mirror_bridge: Outrunning V8, PyPy, and LuaJIT with C++26 Reflection" /><published>2025-12-18T00:00:00+00:00</published><updated>2025-12-18T00:00:00+00:00</updated><id>http://chico.dev/Mirror-Bridge-Multi-Language</id><content type="html" xml:base="http://chico.dev/Mirror-Bridge-Multi-Language/"><![CDATA[<p><em>We know Mirror Bridge generated bindings beat the standard Python interpreter. But how do they fare against the big guns?</em></p>

<hr />

<video autoplay="" loop="" muted="" playsinline="" style="max-width:100%">
  <source src="/images/mandelbrot_zoom.mp4" type="video/mp4" />
</video>
<p><em>The Mandelbrot set, rendered identically by Python, Lua, and JavaScript calling the same C++ engine</em></p>

<hr />

<h2 id="the-benchmark">The Benchmark</h2>

<p>PyPy. LuaJIT. V8. These are serious JIT compilers with years of optimization work behind them. Let’s see how they compare.</p>

<p>800×600 Mandelbrot, 256 max iterations. Lower is better.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PYTHON
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Pure CPython:    2.51s  ████████████████████████████████████████
Pure PyPy:       0.14s  ██▎
C++ Binding:     0.06s  █

LUA
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Pure Lua 5.4:    0.77s  █████████████
Pure LuaJIT:     0.24s  ████
C++ Binding:     0.07s  █▏

JAVASCRIPT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Pure V8:         0.10s  █▌
C++ Binding:     0.06s  █
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Runtime</th>
      <th>Time</th>
      <th>vs C++ Binding</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CPython</td>
      <td>2.51s</td>
      <td>39× slower</td>
    </tr>
    <tr>
      <td>PyPy</td>
      <td>0.14s</td>
      <td>2.2× slower</td>
    </tr>
    <tr>
      <td>Lua 5.4</td>
      <td>0.77s</td>
      <td>10× slower</td>
    </tr>
    <tr>
      <td>LuaJIT</td>
      <td>0.24s</td>
      <td>3.2× slower</td>
    </tr>
    <tr>
      <td>V8</td>
      <td>0.10s</td>
      <td>1.5× slower</td>
    </tr>
    <tr>
      <td><strong>C++ Binding</strong></td>
      <td><strong>0.06s</strong></td>
      <td>—</td>
    </tr>
  </tbody>
</table>

<p>V8 is a serious piece of engineering: speculative optimization, inline caching, hidden classes. And we can still beat it.</p>

<hr />

<h2 id="how-it-works">How It Works</h2>

<p>If you’re not familiar with C++26 reflection, check out my <a href="/Mirror-Bridge">previous post on Mirror Bridge</a> for the fundamentals.</p>

<p>The short version: C++26 reflection lets you iterate over struct members at compile time:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span> <span class="nf">for</span> <span class="p">(</span><span class="k">constexpr</span> <span class="k">auto</span> <span class="n">member</span> <span class="o">:</span> <span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">nonstatic_data_members_of</span><span class="p">(</span><span class="o">^^</span><span class="n">T</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">auto</span><span class="o">&amp;</span> <span class="n">value</span> <span class="o">=</span> <span class="n">obj</span><span class="p">.[</span><span class="o">:</span><span class="n">member</span><span class="o">:</span><span class="p">];</span>  <span class="c1">// splice member directly into code</span>
    <span class="c1">// generate binding for this member...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">[:member:]</code> splice compiles to a direct memory offset, exactly like hand-written <code class="language-plaintext highlighter-rouge">obj.width</code>. No string lookup, no virtual dispatch.</p>

<p><strong>The entire binding for Mandelbrot:</strong></p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">MIRROR_BRIDGE_MODULE</span><span class="p">(</span><span class="n">mandelbrot</span><span class="p">,</span>
    <span class="n">mirror_bridge</span><span class="o">::</span><span class="n">bind_class</span><span class="o">&lt;</span><span class="n">Mandelbrot</span><span class="o">&gt;</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="s">"Mandelbrot"</span><span class="p">);</span>
<span class="p">)</span>
</code></pre></div></div>

<p>One line. Every constructor, method, and property discovered and bound automatically.</p>

<hr />

<h2 id="why-was-c-losing-to-v8">Why Was C++ Losing to V8?</h2>

<p>Early benchmarks showed something unexpected: the C++ binding was running at 0.7× V8’s speed. What was going on?</p>

<p>The culprit wasn’t the computation. It was the data transfer.</p>

<p><code class="language-plaintext highlighter-rouge">render_rgb()</code> returns 1.44 million bytes (800×600×3). A naive binding converts element-by-element:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// The bottleneck: 1.44 million calls to napi_set_element</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">data</span><span class="p">.</span><span class="n">size</span><span class="p">();</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">napi_set_element</span><span class="p">(</span><span class="n">env</span><span class="p">,</span> <span class="n">array</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">to_js</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">]));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The fix: detect byte containers and use bulk transfer:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span><span class="o">&lt;</span><span class="n">ByteContainer</span> <span class="n">T</span><span class="p">&gt;</span>
<span class="n">napi_value</span> <span class="nf">to_javascript</span><span class="p">(</span><span class="n">napi_env</span> <span class="n">env</span><span class="p">,</span> <span class="k">const</span> <span class="n">T</span><span class="o">&amp;</span> <span class="n">container</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">void</span><span class="o">*</span> <span class="n">buffer_data</span><span class="p">;</span>
    <span class="n">napi_create_arraybuffer</span><span class="p">(</span><span class="n">env</span><span class="p">,</span> <span class="n">container</span><span class="p">.</span><span class="n">size</span><span class="p">(),</span> <span class="o">&amp;</span><span class="n">buffer_data</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">buffer</span><span class="p">);</span>
    <span class="n">std</span><span class="o">::</span><span class="n">memcpy</span><span class="p">(</span><span class="n">buffer_data</span><span class="p">,</span> <span class="n">container</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">container</span><span class="p">.</span><span class="n">size</span><span class="p">());</span>  <span class="c1">// Single copy</span>
    <span class="k">return</span> <span class="n">typed_array</span><span class="p">;</span>  <span class="c1">// Returns Uint8Array</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This single change took the JavaScript binding from 0.7× to 1.5× V8’s speed. The C++ computation was always winning; naive data transfer was losing it all back.</p>

<p>Mirror Bridge now detects <code class="language-plaintext highlighter-rouge">ByteContainer</code> types (contiguous ranges of bytes) and uses bulk transfers across all three language bindings.</p>

<hr />

<h2 id="vs-existing-solutions">vs. Existing Solutions</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>pybind11</th>
      <th>sol2</th>
      <th>N-API</th>
      <th>Mirror Bridge</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Lines per method</td>
      <td>3-5</td>
      <td>2-4</td>
      <td>5-10</td>
      <td>0</td>
    </tr>
    <tr>
      <td>New method added</td>
      <td>Update binding</td>
      <td>Update binding</td>
      <td>Update binding</td>
      <td>Recompile</td>
    </tr>
    <tr>
      <td>Multi-language</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>Type conversions</td>
      <td>Manual registration</td>
      <td>Automatic</td>
      <td>Manual</td>
      <td>Automatic</td>
    </tr>
  </tbody>
</table>

<p><strong>Why not just use pybind11?</strong></p>

<p>You absolutely can. pybind11 is mature, well-documented, and battle-tested.</p>

<p>But if you’re binding the same types to multiple languages, or you’re tired of keeping binding declarations in sync with your headers, reflection gives you a third option: declare nothing, discover everything.</p>

<hr />

<h2 id="what-about-c26">What About C++26?</h2>

<blockquote>
  <p>“C++26 isn’t in compilers yet!”</p>
</blockquote>

<p>True. P2996 was voted into C++26 at the Sofia meeting in June 2025, so the standard is locked. But mainstream compiler support is still catching up.</p>

<p>Mirror Bridge uses <a href="https://github.com/bloomberg/clang-p2996">Bloomberg’s experimental Clang fork</a>. The dev container includes everything pre-configured:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./start_dev_container.sh  <span class="c"># Reflection-enabled Clang, ready to go</span>
</code></pre></div></div>

<p>This is what idiomatic C++ binding code will look like once compilers catch up.</p>

<hr />

<h2 id="try-it">Try It</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/FranciscoThiesen/mirror_bridge
<span class="nb">cd </span>mirror_bridge
./start_dev_container.sh

<span class="nb">cd </span>examples/mandelbrot-demo
bash build_mandelbrot_full.sh
</code></pre></div></div>

<p>You’ll see:</p>
<ul>
  <li>Benchmark comparisons across all runtimes</li>
  <li>Generated animation frames (the GIF above)</li>
  <li>The full binding code (it really is one line per language)</li>
</ul>

<hr />

<h2 id="the-code">The Code</h2>

<p><strong>C++ (the only implementation):</strong></p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Mandelbrot</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="n">max_iter</span><span class="p">;</span>
    <span class="kt">double</span> <span class="n">x_min</span><span class="p">,</span> <span class="n">x_max</span><span class="p">,</span> <span class="n">y_min</span><span class="p">,</span> <span class="n">y_max</span><span class="p">;</span>

    <span class="kt">void</span> <span class="n">set_viewport</span><span class="p">(</span><span class="kt">double</span> <span class="n">xmin</span><span class="p">,</span> <span class="kt">double</span> <span class="n">xmax</span><span class="p">,</span> <span class="kt">double</span> <span class="n">ymin</span><span class="p">,</span> <span class="kt">double</span> <span class="n">ymax</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">x_min</span> <span class="o">=</span> <span class="n">xmin</span><span class="p">;</span> <span class="n">x_max</span> <span class="o">=</span> <span class="n">xmax</span><span class="p">;</span> <span class="n">y_min</span> <span class="o">=</span> <span class="n">ymin</span><span class="p">;</span> <span class="n">y_max</span> <span class="o">=</span> <span class="n">ymax</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">uint8_t</span><span class="o">&gt;</span> <span class="n">render_rgb</span><span class="p">()</span> <span class="k">const</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">uint8_t</span><span class="o">&gt;</span> <span class="n">pixels</span><span class="p">(</span><span class="n">width</span> <span class="o">*</span> <span class="n">height</span> <span class="o">*</span> <span class="mi">3</span><span class="p">);</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">py</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">py</span> <span class="o">&lt;</span> <span class="n">height</span><span class="p">;</span> <span class="n">py</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">px</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">px</span> <span class="o">&lt;</span> <span class="n">width</span><span class="p">;</span> <span class="n">px</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">double</span> <span class="n">x</span> <span class="o">=</span> <span class="n">x_min</span> <span class="o">+</span> <span class="p">(</span><span class="n">x_max</span> <span class="o">-</span> <span class="n">x_min</span><span class="p">)</span> <span class="o">*</span> <span class="n">px</span> <span class="o">/</span> <span class="n">width</span><span class="p">;</span>
                <span class="kt">double</span> <span class="n">y</span> <span class="o">=</span> <span class="n">y_min</span> <span class="o">+</span> <span class="p">(</span><span class="n">y_max</span> <span class="o">-</span> <span class="n">y_min</span><span class="p">)</span> <span class="o">*</span> <span class="n">py</span> <span class="o">/</span> <span class="n">height</span><span class="p">;</span>
                <span class="kt">double</span> <span class="n">t</span> <span class="o">=</span> <span class="n">escape_time</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
                <span class="c1">// ... color mapping ...</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">pixels</span><span class="p">;</span>
    <span class="p">}</span>

<span class="nl">private:</span>
    <span class="kt">double</span> <span class="n">escape_time</span><span class="p">(</span><span class="kt">double</span> <span class="n">cr</span><span class="p">,</span> <span class="kt">double</span> <span class="n">ci</span><span class="p">)</span> <span class="k">const</span> <span class="p">{</span>
        <span class="kt">double</span> <span class="n">zr</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">zi</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="kt">int</span> <span class="n">iter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">while</span> <span class="p">(</span><span class="n">zr</span><span class="o">*</span><span class="n">zr</span> <span class="o">+</span> <span class="n">zi</span><span class="o">*</span><span class="n">zi</span> <span class="o">&lt;=</span> <span class="mf">4.0</span> <span class="o">&amp;&amp;</span> <span class="n">iter</span> <span class="o">&lt;</span> <span class="n">max_iter</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">double</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">zr</span><span class="o">*</span><span class="n">zr</span> <span class="o">-</span> <span class="n">zi</span><span class="o">*</span><span class="n">zi</span> <span class="o">+</span> <span class="n">cr</span><span class="p">;</span>
            <span class="n">zi</span> <span class="o">=</span> <span class="mi">2</span><span class="o">*</span><span class="n">zr</span><span class="o">*</span><span class="n">zi</span> <span class="o">+</span> <span class="n">ci</span><span class="p">;</span>
            <span class="n">zr</span> <span class="o">=</span> <span class="n">temp</span><span class="p">;</span>
            <span class="n">iter</span><span class="o">++</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">iter</span><span class="p">;</span>  <span class="c1">// + smooth coloring in real code</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p><strong>Python:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">m</span> <span class="o">=</span> <span class="n">mandelbrot</span><span class="p">.</span><span class="n">Mandelbrot</span><span class="p">(</span><span class="mi">800</span><span class="p">,</span> <span class="mi">600</span><span class="p">,</span> <span class="mi">256</span><span class="p">)</span>
<span class="n">m</span><span class="p">.</span><span class="n">set_viewport</span><span class="p">(</span><span class="o">-</span><span class="mf">0.75</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.73</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mf">0.12</span><span class="p">)</span>
<span class="n">pixels</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="n">render_rgb</span><span class="p">()</span>  <span class="c1"># bytes, 39× faster than pure Python
</span></code></pre></div></div>

<p><strong>Lua:</strong></p>
<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">m</span> <span class="o">=</span> <span class="n">mandelbrot</span><span class="p">.</span><span class="n">Mandelbrot</span><span class="p">(</span><span class="mi">800</span><span class="p">,</span> <span class="mi">600</span><span class="p">,</span> <span class="mi">256</span><span class="p">)</span>
<span class="n">m</span><span class="p">:</span><span class="n">set_viewport</span><span class="p">(</span><span class="o">-</span><span class="mi">0</span><span class="p">.</span><span class="mi">75</span><span class="p">,</span> <span class="o">-</span><span class="mi">0</span><span class="p">.</span><span class="mi">73</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">12</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">pixels</span> <span class="o">=</span> <span class="n">m</span><span class="p">:</span><span class="n">render_rgb</span><span class="p">()</span>  <span class="c1">-- 10× faster than pure Lua</span>
</code></pre></div></div>

<p><strong>JavaScript:</strong></p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">m</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">mandelbrot</span><span class="p">.</span><span class="nx">Mandelbrot</span><span class="p">(</span><span class="mi">800</span><span class="p">,</span> <span class="mi">600</span><span class="p">,</span> <span class="mi">256</span><span class="p">);</span>
<span class="nx">m</span><span class="p">.</span><span class="nx">set_viewport</span><span class="p">(</span><span class="o">-</span><span class="mf">0.75</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.73</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mf">0.12</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">pixels</span> <span class="o">=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">render_rgb</span><span class="p">();</span>  <span class="c1">// Uint8Array, 1.5× faster than V8</span>
</code></pre></div></div>

<p>Same computation. Same pixels. Three languages calling one C++ implementation.</p>

<hr />

<h2 id="whats-next">What’s Next</h2>

<p>The same reflection infrastructure that generates language bindings can power:</p>

<ul>
  <li><strong>Serialization</strong>: JSON, MessagePack, Protobuf from struct definitions</li>
  <li><strong>RPC</strong>: gRPC service stubs generated from C++ classes</li>
  <li><strong>GUI</strong>: Automatic property editors for your types</li>
  <li><strong>ORM</strong>: Database schemas derived from struct layouts</li>
</ul>

<p>One new C++ language feature, many interesting applications!</p>

<hr />

<p><small><strong>Benchmark methodology:</strong> Each implementation renders the same viewport 10 times; reported time is the median. “Pure” implementations use idiomatic code for each language (numpy-style vectorization for Python, standard loops for Lua/JS). Hardware: Apple M3 Max, single-threaded. Full benchmark code in the repo.</small></p>

<hr />

<p><em><a href="https://github.com/FranciscoThiesen/mirror_bridge">Mirror Bridge on GitHub</a></em></p>]]></content><author><name></name></author><category term="Modern" /><category term="C++," /><category term="Python," /><category term="Lua," /><category term="JavaScript," /><category term="Bindings," /><category term="Reflection," /><category term="C++26," /><category term="Performance" /><summary type="html"><![CDATA[We know Mirror Bridge generated bindings beat the standard Python interpreter. But how do they fare against the big guns?]]></summary></entry><entry><title type="html">mirror_bridge - making Python bindings frictionless</title><link href="http://chico.dev/Mirror-Bridge/" rel="alternate" type="text/html" title="mirror_bridge - making Python bindings frictionless" /><published>2025-12-01T00:00:00+00:00</published><updated>2025-12-01T00:00:00+00:00</updated><id>http://chico.dev/Mirror-Bridge</id><content type="html" xml:base="http://chico.dev/Mirror-Bridge/"><![CDATA[<p>You have a Python codebase. Thousands of lines. It works. Your team knows it. Your tests cover it. Your CI/CD deploys it.</p>

<p>But there’s this one function. It shows up in every profile. It’s eating 80% of your runtime. You <em>know</em> it would be faster in C++.</p>

<p>The traditional answer? Rewrite it in C++, then spend sometime writing pybind11 boilerplate to call it from Python. Or just… don’t bother.</p>

<p><strong>Mirror Bridge</strong> is a third option: write C++, run one command, done.</p>

<h2 id="the-setup">The Setup</h2>

<p>Let’s say you have a simple operation - a dot product:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// vec3.hpp</span>
<span class="k">struct</span> <span class="nc">Vec3</span> <span class="p">{</span>
    <span class="kt">double</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">;</span>

    <span class="n">Vec3</span><span class="p">(</span><span class="kt">double</span> <span class="n">x</span><span class="p">,</span> <span class="kt">double</span> <span class="n">y</span><span class="p">,</span> <span class="kt">double</span> <span class="n">z</span><span class="p">)</span> <span class="o">:</span> <span class="n">x</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="n">y</span><span class="p">(</span><span class="n">y</span><span class="p">),</span> <span class="n">z</span><span class="p">(</span><span class="n">z</span><span class="p">)</span> <span class="p">{}</span>

    <span class="kt">double</span> <span class="n">dot</span><span class="p">(</span><span class="k">const</span> <span class="n">Vec3</span><span class="o">&amp;</span> <span class="n">other</span><span class="p">)</span> <span class="k">const</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">x</span> <span class="o">*</span> <span class="n">other</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span> <span class="o">*</span> <span class="n">other</span><span class="p">.</span><span class="n">y</span> <span class="o">+</span> <span class="n">z</span> <span class="o">*</span> <span class="n">other</span><span class="p">.</span><span class="n">z</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Compute the magnitude (norm) of the vector</span>
    <span class="kt">double</span> <span class="n">length</span><span class="p">()</span> <span class="k">const</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">sqrt</span><span class="p">(</span><span class="n">x</span><span class="o">*</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="o">*</span><span class="n">y</span> <span class="o">+</span> <span class="n">z</span><span class="o">*</span><span class="n">z</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>To use this from Python:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./mirror_bridge_auto src/ <span class="nt">--module</span> vec3 <span class="nt">-o</span> <span class="nb">.</span>
</code></pre></div></div>

<p>That’s it. No binding code. Mirror Bridge uses C++26 reflection to discover your classes, methods, and fields automatically.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">vec3</span>

<span class="n">a</span> <span class="o">=</span> <span class="n">vec3</span><span class="p">.</span><span class="n">Vec3</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
<span class="n">b</span> <span class="o">=</span> <span class="n">vec3</span><span class="p">.</span><span class="n">Vec3</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">b</span><span class="p">))</span>  <span class="c1"># 32.0
</span></code></pre></div></div>

<h2 id="the-naive-benchmark-and-why-its-misleading">The Naive Benchmark (And Why It’s Misleading)</h2>

<p>Let’s call <code class="language-plaintext highlighter-rouge">dot()</code> a million times:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">):</span>
    <span class="n">a</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Results (M3 Max MacBook Pro):</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Python class: 0.11s
C++ via Mirror Bridge: 0.04s
Speedup: 2.9x
</code></pre></div></div>

<p>A ~3x speedup. Not bad, but not eye-popping. What’s going on?</p>

<p><strong>This benchmark can be somewhat misleading</strong></p>

<p>Each call from Python to C++ pays a toll - argument conversion, language boundary crossing, result conversion. For a trivial operation like <code class="language-plaintext highlighter-rouge">dot()</code> (3 multiplies, 2 adds), that toll dominates the actual work.</p>

<p>This is like benchmarking a Ferrari by measuring how fast it can start and stop at every intersection. You’re measuring overhead, not speed.</p>

<h3 id="dissecting-the-cross-language-barrier">Dissecting the Cross-Language Barrier</h3>

<p>What actually happens when Python calls a C++ function? Let’s trace through a single <code class="language-plaintext highlighter-rouge">a.dot(b)</code> call:</p>

<ol>
  <li>
    <p><strong>Method lookup.</strong> Python sees <code class="language-plaintext highlighter-rouge">a.dot</code> and searches for the <code class="language-plaintext highlighter-rouge">dot</code> attribute. It checks <code class="language-plaintext highlighter-rouge">a.__dict__</code>, then <code class="language-plaintext highlighter-rouge">type(a).__dict__</code>, walking up the MRO (Method Resolution Order). For our bound C++ class, this eventually finds a descriptor that wraps the C++ method.</p>
  </li>
  <li>
    <p><strong>Argument boxing.</strong> Python passes <code class="language-plaintext highlighter-rouge">b</code> as a <code class="language-plaintext highlighter-rouge">PyObject*</code> - a pointer to a heap-allocated structure containing a reference count, type pointer, and the actual data. The binding layer (nanobind, in Mirror Bridge’s case) must extract the underlying C++ <code class="language-plaintext highlighter-rouge">Vec3</code> from this Python wrapper.</p>
  </li>
  <li>
    <p><strong>Type checking.</strong> Is <code class="language-plaintext highlighter-rouge">b</code> actually a <code class="language-plaintext highlighter-rouge">Vec3</code>? The binding layer verifies this at runtime. In pure C++, the compiler guarantees types at compile time - no runtime check needed.</p>
  </li>
  <li>
    <p><strong>GIL management.</strong> The Global Interpreter Lock might need to be considered. For simple numeric operations we keep it held, but for longer operations you’d release it to allow other Python threads to run.</p>
  </li>
  <li>
    <p><strong>The actual call.</strong> Finally, we call the C++ <code class="language-plaintext highlighter-rouge">dot()</code> method. This is the fast part - a few nanoseconds for 3 multiplies and 2 adds.</p>
  </li>
  <li>
    <p><strong>Result conversion.</strong> The C++ <code class="language-plaintext highlighter-rouge">double</code> result must be wrapped in a Python <code class="language-plaintext highlighter-rouge">float</code> object - another heap allocation, reference count initialization, and type pointer setup.</p>
  </li>
  <li>
    <p><strong>Return.</strong> Python receives a <code class="language-plaintext highlighter-rouge">PyObject*</code> pointing to the new float.</p>
  </li>
</ol>

<p>For a function that does microseconds of work, this overhead is negligible. For a function that does <em>nanoseconds</em> of work - like our <code class="language-plaintext highlighter-rouge">dot()</code> - the overhead dominates. You’re spending more time crossing the border than doing actual computation.</p>

<h3 id="why-is-c-faster-in-the-first-place">Why Is C++ Faster in the First Place?</h3>

<p>Even setting aside the cross-language overhead, why is C++ inherently faster for the same operation? Let’s look at what each language actually does for <code class="language-plaintext highlighter-rouge">dot()</code>:</p>

<p><strong>Python’s journey:</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">dot</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
    <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">x</span><span class="o">*</span><span class="n">other</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="bp">self</span><span class="p">.</span><span class="n">y</span><span class="o">*</span><span class="n">other</span><span class="p">.</span><span class="n">y</span> <span class="o">+</span> <span class="bp">self</span><span class="p">.</span><span class="n">z</span><span class="o">*</span><span class="n">other</span><span class="p">.</span><span class="n">z</span>
</code></pre></div></div>

<p>Each attribute access (<code class="language-plaintext highlighter-rouge">self.x</code>) involves:</p>
<ul>
  <li>Dictionary lookup in <code class="language-plaintext highlighter-rouge">self.__dict__</code></li>
  <li>If not found, search the class hierarchy</li>
  <li>Return a Python <code class="language-plaintext highlighter-rouge">float</code> object (heap-allocated, reference-counted)</li>
</ul>

<p>Each multiplication:</p>
<ul>
  <li>Check types of both operands at runtime</li>
  <li>Dispatch to the appropriate <code class="language-plaintext highlighter-rouge">__mul__</code> implementation</li>
  <li>Allocate a new Python <code class="language-plaintext highlighter-rouge">float</code> for the result</li>
</ul>

<p>Each addition: same story. We can see this by looking at the Python bytecode:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Python bytecode for Vec3.dot() - run: python3 -c "import dis; dis.dis(Vec3.dot)"
</span>  <span class="mi">0</span> <span class="n">LOAD_FAST</span>      <span class="mi">0</span> <span class="p">(</span><span class="bp">self</span><span class="p">)</span>
  <span class="mi">2</span> <span class="n">LOAD_ATTR</span>      <span class="mi">0</span> <span class="p">(</span><span class="n">x</span><span class="p">)</span>      <span class="c1"># dict lookup for self.x
</span>  <span class="mi">4</span> <span class="n">LOAD_FAST</span>      <span class="mi">1</span> <span class="p">(</span><span class="n">other</span><span class="p">)</span>
  <span class="mi">6</span> <span class="n">LOAD_ATTR</span>      <span class="mi">0</span> <span class="p">(</span><span class="n">x</span><span class="p">)</span>      <span class="c1"># dict lookup for other.x
</span>  <span class="mi">8</span> <span class="n">BINARY_MULTIPLY</span>           <span class="c1"># type check, dispatch, allocate result
</span> <span class="mi">10</span> <span class="n">LOAD_FAST</span>      <span class="mi">0</span> <span class="p">(</span><span class="bp">self</span><span class="p">)</span>
 <span class="mi">12</span> <span class="n">LOAD_ATTR</span>      <span class="mi">1</span> <span class="p">(</span><span class="n">y</span><span class="p">)</span>      <span class="c1"># dict lookup for self.y
</span> <span class="mi">14</span> <span class="n">LOAD_FAST</span>      <span class="mi">1</span> <span class="p">(</span><span class="n">other</span><span class="p">)</span>
 <span class="mi">16</span> <span class="n">LOAD_ATTR</span>      <span class="mi">1</span> <span class="p">(</span><span class="n">y</span><span class="p">)</span>      <span class="c1"># dict lookup for other.y
</span> <span class="mi">18</span> <span class="n">BINARY_MULTIPLY</span>           <span class="c1"># type check, dispatch, allocate result
</span> <span class="mi">20</span> <span class="n">BINARY_ADD</span>                <span class="c1"># type check, dispatch, allocate result
</span> <span class="mi">22</span> <span class="n">LOAD_FAST</span>      <span class="mi">0</span> <span class="p">(</span><span class="bp">self</span><span class="p">)</span>
 <span class="mi">24</span> <span class="n">LOAD_ATTR</span>      <span class="mi">2</span> <span class="p">(</span><span class="n">z</span><span class="p">)</span>      <span class="c1"># dict lookup for self.z
</span> <span class="mi">26</span> <span class="n">LOAD_FAST</span>      <span class="mi">1</span> <span class="p">(</span><span class="n">other</span><span class="p">)</span>
 <span class="mi">28</span> <span class="n">LOAD_ATTR</span>      <span class="mi">2</span> <span class="p">(</span><span class="n">z</span><span class="p">)</span>      <span class="c1"># dict lookup for other.z
</span> <span class="mi">30</span> <span class="n">BINARY_MULTIPLY</span>           <span class="c1"># type check, dispatch, allocate result
</span> <span class="mi">32</span> <span class="n">BINARY_ADD</span>                <span class="c1"># type check, dispatch, allocate result
</span> <span class="mi">34</span> <span class="n">RETURN_VALUE</span>
</code></pre></div></div>

<p>That’s 18 bytecode instructions, each of which expands to many machine instructions. The <code class="language-plaintext highlighter-rouge">LOAD_ATTR</code> operations involve hash table lookups. The <code class="language-plaintext highlighter-rouge">BINARY_*</code> operations involve type checking and dynamic dispatch. By the end, we’ve done 6 attribute lookups, 5 arithmetic operations (each with type checks), and allocated several intermediate <code class="language-plaintext highlighter-rouge">float</code> objects.</p>

<p><strong>C++’s journey:</strong></p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">double</span> <span class="n">dot</span><span class="p">(</span><span class="k">const</span> <span class="n">Vec3</span><span class="o">&amp;</span> <span class="n">other</span><span class="p">)</span> <span class="k">const</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">x</span> <span class="o">*</span> <span class="n">other</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span> <span class="o">*</span> <span class="n">other</span><span class="p">.</span><span class="n">y</span> <span class="o">+</span> <span class="n">z</span> <span class="o">*</span> <span class="n">other</span><span class="p">.</span><span class="n">z</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The compiler knows at compile time:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">x</code>, <code class="language-plaintext highlighter-rouge">y</code>, <code class="language-plaintext highlighter-rouge">z</code> are <code class="language-plaintext highlighter-rouge">double</code> values at fixed offsets from <code class="language-plaintext highlighter-rouge">this</code></li>
  <li><code class="language-plaintext highlighter-rouge">other.x</code>, <code class="language-plaintext highlighter-rouge">other.y</code>, <code class="language-plaintext highlighter-rouge">other.z</code> are at fixed offsets from <code class="language-plaintext highlighter-rouge">&amp;other</code></li>
  <li>All operations are <code class="language-plaintext highlighter-rouge">double * double</code> and <code class="language-plaintext highlighter-rouge">double + double</code></li>
</ul>

<p>Here’s the <a href="https://godbolt.org/z/Txhs9qd5G">actual generated assembly</a> (clang 18, -O3):</p>

<pre><code class="language-asm">call_dot(Vec3 const&amp;, Vec3 const&amp;):
  movsd xmm1, qword ptr [rdi]       ; load a.x
  movsd xmm0, qword ptr [rdi + 8]   ; load a.y
  mulsd xmm0, qword ptr [rsi + 8]   ; a.y * b.y
  mulsd xmm1, qword ptr [rsi]       ; a.x * b.x
  addsd xmm1, xmm0                  ; (a.x*b.x) + (a.y*b.y)
  movsd xmm0, qword ptr [rdi + 16]  ; load a.z
  mulsd xmm0, qword ptr [rsi + 16]  ; a.z * b.z
  addsd xmm0, xmm1                  ; add all three
  ret                                ; result in xmm0
</code></pre>

<p>That’s it. Four memory loads (some folded into <code class="language-plaintext highlighter-rouge">mulsd</code>), three multiplies, two adds, one return. No allocations. No type checks. No dictionary lookups. The entire function is 9 instructions.</p>

<p>This is why the “naive” benchmark shows only 3x speedup instead of 100x: Python’s overhead for calling a function (even a Python function) is substantial, so replacing the <em>implementation</em> with C++ helps less than you’d expect. The win comes when you stop crossing the boundary repeatedly.</p>

<h2 id="the-real-benchmark-surgical-optimization">The Real Benchmark: Surgical Optimization</h2>

<p>In practice, you don’t call C++ a million times from Python. You identify a hot loop and move <em>the entire loop</em> to C++.</p>

<p>Imagine you have this in your Python codebase:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">hot_loop</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
    <span class="s">"""This function showed up in your profiler."""</span>
    <span class="n">direction</span> <span class="o">=</span> <span class="n">Vec3</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
    <span class="n">dir_len</span> <span class="o">=</span> <span class="n">direction</span><span class="p">.</span><span class="n">length</span><span class="p">()</span>  <span class="c1"># Magnitude (norm) of direction vector
</span>    <span class="n">total</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
        <span class="n">v</span> <span class="o">=</span> <span class="n">Vec3</span><span class="p">(</span><span class="n">i</span> <span class="o">*</span> <span class="mf">0.1</span><span class="p">,</span> <span class="n">i</span> <span class="o">*</span> <span class="mf">0.2</span><span class="p">,</span> <span class="n">i</span> <span class="o">*</span> <span class="mf">0.3</span><span class="p">)</span>
        <span class="n">total</span> <span class="o">+=</span> <span class="n">v</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">direction</span><span class="p">)</span> <span class="o">/</span> <span class="n">dir_len</span>
    <span class="k">return</span> <span class="n">total</span>
</code></pre></div></div>

<p>It’s called from dozens of places. The <code class="language-plaintext highlighter-rouge">Vec3</code> class is used throughout your codebase. You don’t want to rewrite everything - you just want <em>this loop</em> to go fast.</p>

<p>So you write the C++ version:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Add to vec3.hpp</span>
<span class="k">struct</span> <span class="nc">Vec3</span> <span class="p">{</span>
    <span class="c1">// ... existing code ...</span>

    <span class="k">static</span> <span class="kt">double</span> <span class="n">hot_loop</span><span class="p">(</span><span class="kt">int</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">Vec3</span> <span class="n">direction</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
        <span class="kt">double</span> <span class="n">dir_len</span> <span class="o">=</span> <span class="n">direction</span><span class="p">.</span><span class="n">length</span><span class="p">();</span>
        <span class="kt">double</span> <span class="n">total</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">Vec3</span> <span class="n">v</span><span class="p">(</span><span class="n">i</span> <span class="o">*</span> <span class="mf">0.1</span><span class="p">,</span> <span class="n">i</span> <span class="o">*</span> <span class="mf">0.2</span><span class="p">,</span> <span class="n">i</span> <span class="o">*</span> <span class="mf">0.3</span><span class="p">);</span>
            <span class="n">total</span> <span class="o">+=</span> <span class="n">v</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">direction</span><span class="p">)</span> <span class="o">/</span> <span class="n">dir_len</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">total</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Rebuild: <code class="language-plaintext highlighter-rouge">./mirror_bridge_auto src/ --module vec3 -o . --force</code></p>

<p>Now benchmark:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Python version
</span><span class="n">result</span> <span class="o">=</span> <span class="n">hot_loop</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span>

<span class="c1"># C++ version - ONE call, all work happens in C++
</span><span class="n">result</span> <span class="o">=</span> <span class="n">vec3</span><span class="p">.</span><span class="n">Vec3</span><span class="p">.</span><span class="n">hot_loop</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Results (M3 Max MacBook Pro):</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Python: 0.26s
C++:    0.004s
Speedup: 67x
</code></pre></div></div>

<p><em>That’s</em> the real number. You pay the Python→C++ toll once. The loop runs at full native speed.</p>

<h2 id="why-not-just-rewrite-everything-in-c">Why Not Just Rewrite Everything in C++?</h2>

<p>Because you probably shouldn’t.</p>

<p>Python gives you:</p>
<ul>
  <li>Rapid iteration (no compile step during development)</li>
  <li>A massive ecosystem (PyTorch, pandas, requests, FastAPI…)</li>
  <li>Readable glue code that connects everything</li>
  <li>Easy onboarding for new team members</li>
</ul>

<p>The Pareto principle applies aggressively here. 80% of your runtime might come from 20% of your code. Often it’s even more skewed - a single hot loop can dominate everything.</p>

<p>Mirror Bridge lets you surgically replace <em>just the hot 20%</em> with C++, keeping the other 80% in Python. You get:</p>
<ul>
  <li>Python’s ergonomics for the code that doesn’t need to be fast</li>
  <li>C++ performance for the code that does</li>
  <li>Zero binding boilerplate connecting them</li>
</ul>

<h2 id="the-magic-c26-reflection">The Magic: C++26 Reflection</h2>

<p>Here’s where it gets interesting. How does Mirror Bridge discover your classes automatically?</p>

<p>The answer is <strong>C++26 static reflection</strong> (<a href="https://wg21.link/p2996">P2996</a>) - a new language feature that lets code inspect itself at compile time.</p>

<p>When you run <code class="language-plaintext highlighter-rouge">mirror_bridge_auto</code>, it generates code that looks something like this:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Auto-generated binding code (simplified)</span>
<span class="cp">#include</span> <span class="cpf">"vec3.hpp"</span><span class="cp">
#include</span> <span class="cpf">&lt;mirror_bridge.hpp&gt;</span><span class="cp">
</span>
<span class="n">MIRROR_BRIDGE_MODULE</span><span class="p">(</span><span class="n">vec3</span><span class="p">,</span>
    <span class="n">mirror_bridge</span><span class="o">::</span><span class="n">bind_class</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="s">"Vec3"</span><span class="p">);</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The magic is in <code class="language-plaintext highlighter-rouge">bind_class&lt;Vec3&gt;</code>. Using reflection, Mirror Bridge can ask the compiler:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// What Mirror Bridge does internally</span>
<span class="k">constexpr</span> <span class="k">auto</span> <span class="n">type_info</span> <span class="o">=</span> <span class="o">^</span><span class="n">Vec3</span><span class="p">;</span>  <span class="c1">// "reflection operator" - get metadata about Vec3</span>

<span class="c1">// Get all members of Vec3</span>
<span class="k">constexpr</span> <span class="k">auto</span> <span class="n">members</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">members_of</span><span class="p">(</span><span class="n">type_info</span><span class="p">);</span>

<span class="c1">// Iterate over them AT COMPILE TIME</span>
<span class="k">template</span> <span class="nf">for</span> <span class="p">(</span><span class="k">constexpr</span> <span class="k">auto</span> <span class="n">member</span> <span class="o">:</span> <span class="n">members</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// What's the name of this member?</span>
    <span class="k">constexpr</span> <span class="k">auto</span> <span class="n">name</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">identifier_of</span><span class="p">(</span><span class="n">member</span><span class="p">);</span>

    <span class="c1">// Is it public?</span>
    <span class="k">if</span> <span class="k">constexpr</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">is_public</span><span class="p">(</span><span class="n">member</span><span class="p">))</span> <span class="p">{</span>

        <span class="c1">// Is it a function?</span>
        <span class="k">if</span> <span class="k">constexpr</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">is_function</span><span class="p">(</span><span class="n">member</span><span class="p">))</span> <span class="p">{</span>
            <span class="c1">// Bind it as a Python method</span>
            <span class="n">cls</span><span class="p">.</span><span class="n">def</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="cm">/* pointer to member function */</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="c1">// Is it a data member?</span>
        <span class="k">else</span> <span class="k">if</span> <span class="k">constexpr</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">meta</span><span class="o">::</span><span class="n">is_nonstatic_data_member</span><span class="p">(</span><span class="n">member</span><span class="p">))</span> <span class="p">{</span>
            <span class="c1">// Bind it as a Python attribute</span>
            <span class="n">cls</span><span class="p">.</span><span class="n">def_readwrite</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="cm">/* pointer to member */</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">^</code> operator (called the “reflection operator”) takes a type and returns a <em>compile-time value</em> representing that type’s metadata. From there, <code class="language-plaintext highlighter-rouge">std::meta</code> functions let you query everything about it: members, their types, their names, whether they’re public, static, const, etc.</p>

<p>This is fundamentally different from runtime reflection (like Java or C#). Everything happens at compile time:</p>
<ul>
  <li><strong>Zero runtime overhead</strong> - the reflection queries are resolved during compilation</li>
  <li><strong>Full type safety</strong> - the compiler knows exact types, no casting or dynamic lookups</li>
  <li><strong>Works with templates</strong> - you can reflect on template instantiations</li>
</ul>

<p>Before P2996, generating Python bindings required either:</p>
<ol>
  <li>Manual listing (pybind11) - tedious and error-prone</li>
  <li>Code parsing (SWIG) - fragile and limited</li>
  <li>Macros (Boost.Python) - ugly and inflexible</li>
</ol>

<p>Reflection gives us a fourth option: <strong>ask the compiler directly</strong>. It already knows everything about your types. Now we can access that knowledge programmatically.</p>

<h2 id="the-comparison">The Comparison</h2>

<p>For context, here’s what the traditional pybind11 approach requires:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;pybind11/pybind11.h&gt;</span><span class="cp">
#include</span> <span class="cpf">"vec3.hpp"</span><span class="cp">
</span>
<span class="n">PYBIND11_MODULE</span><span class="p">(</span><span class="n">vec3</span><span class="p">,</span> <span class="n">m</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">py</span><span class="o">::</span><span class="n">class_</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="s">"Vec3"</span><span class="p">)</span>
        <span class="p">.</span><span class="n">def</span><span class="p">(</span><span class="n">py</span><span class="o">::</span><span class="n">init</span><span class="o">&lt;</span><span class="kt">double</span><span class="p">,</span> <span class="kt">double</span><span class="p">,</span> <span class="kt">double</span><span class="o">&gt;</span><span class="p">())</span>
        <span class="p">.</span><span class="n">def_readwrite</span><span class="p">(</span><span class="s">"x"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Vec3</span><span class="o">::</span><span class="n">x</span><span class="p">)</span>
        <span class="p">.</span><span class="n">def_readwrite</span><span class="p">(</span><span class="s">"y"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Vec3</span><span class="o">::</span><span class="n">y</span><span class="p">)</span>
        <span class="p">.</span><span class="n">def_readwrite</span><span class="p">(</span><span class="s">"z"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Vec3</span><span class="o">::</span><span class="n">z</span><span class="p">)</span>
        <span class="p">.</span><span class="n">def</span><span class="p">(</span><span class="s">"dot"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Vec3</span><span class="o">::</span><span class="n">dot</span><span class="p">)</span>
        <span class="p">.</span><span class="n">def</span><span class="p">(</span><span class="s">"length"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Vec3</span><span class="o">::</span><span class="n">length</span><span class="p">)</span>
        <span class="p">.</span><span class="n">def_static</span><span class="p">(</span><span class="s">"hot_loop"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Vec3</span><span class="o">::</span><span class="n">hot_loop</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Every single method needs to be manually listed. And when you add a new one? Update the Python bindings. Forgot one? Silent failure at runtime.</p>

<p>With Mirror Bridge: write C++, run command, done.</p>

<h2 id="getting-started">Getting Started</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/FranciscoThiesen/mirror_bridge
<span class="nb">cd </span>mirror_bridge
./start_dev_container.sh  <span class="c"># Pre-built Docker image with clang-p2996</span>

<span class="c"># Inside container - try the example from this post</span>
<span class="nb">cd</span> /workspace/examples/blog_vec3
../../mirror_bridge_auto <span class="nb">.</span> <span class="nt">--module</span> vec3 <span class="nt">-o</span> <span class="nb">.</span>
python3 benchmark.py
</code></pre></div></div>

<p>The <a href="https://github.com/FranciscoThiesen/mirror_bridge/tree/main/examples/blog_vec3"><code class="language-plaintext highlighter-rouge">examples/blog_vec3</code></a> directory contains all the code from this post, ready to run.</p>

<p>The Docker image includes <a href="https://github.com/bloomberg/clang-p2996">clang-p2996</a>, Bloomberg’s experimental Clang fork that implements the reflection proposal. As P2996 moves toward standardization, expect this to land in mainline compilers.</p>

<h2 id="the-bottom-line">The Bottom Line</h2>

<p>The next time you’re staring at a Python profiler, wondering if it’s worth the hassle to optimize that hot loop in C++, the answer is: <strong>yes, and it should take less than five minutes</strong>.</p>

<p>Write the C++ version. Run <code class="language-plaintext highlighter-rouge">mirror_bridge_auto</code>. Replace the call. Ship it.</p>

<hr />

<ul>
  <li><a href="https://github.com/FranciscoThiesen/mirror_bridge">Mirror Bridge on GitHub</a></li>
  <li><a href="https://wg21.link/p2996">C++26 Reflection Proposal (P2996)</a></li>
  <li><a href="https://github.com/bloomberg/clang-p2996">clang-p2996</a></li>
</ul>

<h2 id="acknowledgments">Acknowledgments</h2>

<p>Mirror Bridge wouldn’t exist without the work of many others. Thanks to Wyatt Childers, Peter Dimov, Dan Katz, Barry Revzin, Andrew Sutton, Faisal Vali, and Daveed Vandevoorde for authoring and championing P2996. Special thanks to Dan Katz for maintaining the <a href="https://github.com/bloomberg/clang-p2996">clang-p2996</a> branch that makes this all possible today.</p>

<p>Herb Sutter’s <a href="https://www.youtube.com/watch?v=7z9NNrRDHQU&amp;list=PLHTh1InhhwT57vblPGsVag5MkTm_Z9-uq&amp;index=16">CppCon 2025 talk on reflection</a> was also a motivating force for this work, making the case of a common language to rule them all, which motivate me to try to make it easier for other languages to call C++.</p>

<p>The idea of using reflection to generate Python bindings isn’t new - it was explored by others before, including in <a href="https://www.youtube.com/watch?v=SJ0NFLpR9vE">this ACCU 2025 talk</a>. Mirror Bridge builds on these ideas and packages them into a tool you can use today.</p>]]></content><author><name></name></author><category term="Modern" /><category term="C++," /><category term="Python," /><category term="Bindings," /><category term="Reflection," /><category term="C++26," /><category term="Performance" /><summary type="html"><![CDATA[You have a Python codebase. Thousands of lines. It works. Your team knows it. Your tests cover it. Your CI/CD deploys it.]]></summary></entry><entry><title type="html">Reflection-based JSON in C++ at Gigabytes per Second</title><link href="http://chico.dev/Reflection-Based-Serialization/" rel="alternate" type="text/html" title="Reflection-based JSON in C++ at Gigabytes per Second" /><published>2024-08-13T00:00:00+00:00</published><updated>2024-08-13T00:00:00+00:00</updated><id>http://chico.dev/Reflection-Based-Serialization</id><content type="html" xml:base="http://chico.dev/Reflection-Based-Serialization/"><![CDATA[<p>JSON (JavaScript Object Notation) is a popular format for storing and transmitting data. It uses human-readable text to represent structured data in the form of attribute–value pairs and arrays. E.g., <code class="language-plaintext highlighter-rouge">{"age":5, "name":"Daniel", toys:["wooden dog", "little car"]}</code>.
Ingesting and producing JSON documents can be a performance bottleneck.</p>

<p>Thankfully, a few JSON parsers have shown that we can process JSON at high speeds, reaching gigabytes per second<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>.</p>

<p>However, producing and ingesting JSON data can remain a chore in C++. The programmer often needs to address potential errors such as unexpected content.</p>

<p>Often, the programmer only needs to map the content to and from a native C/C++ data structure. E.g., our JSON example
might correspond to the following C++ structure:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">kid</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">age</span><span class="p">;</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">name</span><span class="p">;</span>
    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">toys</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Hence, we would often like to write simple code such as <code class="language-plaintext highlighter-rouge">kid k; store_json(k, out)</code> or <code class="language-plaintext highlighter-rouge">kid k; load_json(k, in)</code>. Ideally, we would like this code to be fast and safe. That is, we would like to write and read our data structures at gigabytes per second. Further, we want proper validation especially since the JSON data might come from the Internet.</p>

<p>A library such as simdjson already allows elegant deserialization, e.g., we can make the following example work:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">kid</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">age</span><span class="p">;</span>
  <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">name</span><span class="p">;</span>
  <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">toys</span><span class="p">;</span>
<span class="p">};</span>

<span class="kt">void</span> <span class="n">demo</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">auto</span> <span class="n">json_str</span> <span class="o">=</span>
      <span class="s">R"({"age": 12, "name": "John", "toys": ["car", "ball"]})"</span><span class="n">_padded</span><span class="p">;</span>
  <span class="n">simdjson</span><span class="o">::</span><span class="n">ondemand</span><span class="o">::</span><span class="n">parser</span> <span class="n">parser</span><span class="p">;</span>
  <span class="k">auto</span> <span class="n">doc</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">iterate</span><span class="p">(</span><span class="n">json_str</span><span class="p">);</span>
  <span class="n">kid</span> <span class="n">k</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="n">get</span><span class="o">&lt;</span><span class="n">kid</span><span class="o">&gt;</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>However, for the time being, it requires a <a href="https://github.com/simdjson/simdjson/blob/master/doc/basics.md#adding-support-for-custom-types">bit of effort</a> on the part of the programmer—as they need to write a custom function.</p>

<p>Similarly, going from the C++ structures to JSON can be made
convenient, but typically only after the programmer has done
a bit of work, writing custom code. E.g., in the popular JSON for Modern C++ (<a href="https://github.com/nlohmann/json">nlohmann/json</a>) library, we need to provide glue functions:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">to_json</span><span class="p">(</span><span class="n">json</span><span class="o">&amp;</span> <span class="n">j</span><span class="p">,</span> <span class="k">const</span> <span class="n">person</span><span class="o">&amp;</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">j</span> <span class="o">=</span> <span class="n">json</span><span class="p">{{</span><span class="s">"name"</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="n">name</span><span class="p">},</span> <span class="p">{</span><span class="s">"address"</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="n">address</span><span class="p">},</span> <span class="p">{</span><span class="s">"age"</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="n">age</span><span class="p">}};</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">from_json</span><span class="p">(</span><span class="k">const</span> <span class="n">json</span><span class="o">&amp;</span> <span class="n">j</span><span class="p">,</span> <span class="n">person</span><span class="o">&amp;</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">j</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="s">"name"</span><span class="p">).</span><span class="n">get_to</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">name</span><span class="p">);</span>
    <span class="n">j</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="s">"address"</span><span class="p">).</span><span class="n">get_to</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">address</span><span class="p">);</span>
    <span class="n">j</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="s">"age"</span><span class="p">).</span><span class="n">get_to</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">age</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We know that this extra effort (writing data-structure aware code) is unnecessary because other programming languages (Java, C#, Zig, Rust, Python, etc.) allow us to ‘magically’
serialize and deserialize with almost no specialized code.</p>

<p>One key missing feature in <a href="https://stackoverflow.com/questions/8948087/is-there-any-major-programming-language-that-doesnt-support-any-form-of-reflect">C++ is sufficiently powerful <em>reflection</em></a>. Reflection in programming languages refers to a mechanism that allows code to introspect its own structure.
That is, if you cannot conveniently tell C++ that if a class
has a <code class="language-plaintext highlighter-rouge">std::string name</code> attribute, then you should automatically search for a string value matching the key <code class="language-plaintext highlighter-rouge">name</code> in the JSON.</p>

<p>Thankfully, C++ is soon getting reflection in C++26<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>. And it is getting powerful reflection: reflective metaprogramming. That is, future versions of C++ will allow us to solve the serialization and deserialization problem we described at compile time: a software library can automate the production and consumption of JSON to and from a native data structure. 
Such a feature will simplify the life of the C++ programmer. Furthermore, because it can be done automatically as part of well tested library at compile time, it can generate fast and safe code.</p>

<p><a href="https://herbsutter.com/2024/07/02/trip-report-summer-iso-c-standards-meeting-st-louis-mo-usa/">Herb Sutter wrote about this development</a>:</p>

<blockquote>
  <p>This is huge, because reflection (including generation) will be by far the most impactful feature C++ has ever added since C++98, and it will dominate the next decade and more of C++ usage.</p>
</blockquote>

<p>Though many programming languages have reflection, the
reflective metaprogramming provided by the upcoming
C++ standard is unique to our knowledge. Some programming
languages have long had powerful reflection
capabilities, but they are mostly a runtime feature:
they do not allow the equivalent of generating
code. Other programming languages allow compile-time
logic generation but they do not always allow optimal
performance. We expect that the C++ approach might be
especially appropriate for high performance.</p>

<p>Though current C++ compilers do not yet support reflection, we have access to prototypical compilers. In particular,
engineers from Bloomberg maintain a <a href="https://github.com/bloomberg/clang-p2996/tree/p2996">fork of LLVM</a>. Though such an implementation should not be used in production and is subject to bugs and other limitations, it should be sufficient to test the performance: how fast can serialization-based JSON processing be in C++?
Importantly, we are not immediately concerned with compilation speed: the <a href="https://www.reddit.com/r/cpp/comments/1c7ugyg/metaprogramming_benchmark_p2996_vs_p1858_vs/?rdt=55615">Bloomberg fork is purposefully
suboptimal in this respect</a>.</p>

<p>To test out this new C++ feature, <a href="https://github.com/simdjson/experimental_json_builder/">we wrote a prototype</a>
that can serialize and deserialize data structures to and from JSON strings automatically. Given an instance of <code class="language-plaintext highlighter-rouge">kid</code>, we can convert it to a JSON string without any effort, the compile-time reflection does the work:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">kid</span> <span class="n">k</span><span class="p">{</span><span class="mi">12</span><span class="p">,</span> <span class="s">"John"</span><span class="p">,</span> <span class="p">{</span><span class="s">"car"</span><span class="p">,</span> <span class="s">"ball"</span><span class="p">}};</span>
  <span class="n">std</span><span class="o">::</span><span class="n">print</span><span class="p">(</span><span class="s">"My JSON is {}</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">simdjson</span><span class="o">::</span><span class="n">json_builder</span><span class="o">::</span><span class="n">to_json_string</span><span class="p">(</span><span class="n">k</span><span class="p">));</span>
</code></pre></div></div>

<h3 id="benchmark-results-with-speed-up">Benchmark Results with Speed-up</h3>

<p>Automatically converting your C++ data types into JSON strings is convenient. But is it fast? To find out, we have used 2 instances for benchmarking, an “Artificial instance” that was obtained using the <a href="https://json-generator.com/">JSON Generator</a> Website and the other one is the <a href="https://github.com/simdjson/simdjson/blob/master/jsonexamples/twitter.json">twitter.json</a> file that is used by many other serialization libraries for benchmarking.</p>

<p>We test on two systems (Apple and Intel/Linux) with the experimental LLVM Bloomberg compiler.</p>

<p>M3 MAX Macbook Pro:</p>

<table>
  <thead>
    <tr>
      <th>Test Instance</th>
      <th>Library</th>
      <th>Speed (MB/s)</th>
      <th>speedup</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Twitter Serialization</td>
      <td>nlohmann/json</td>
      <td>100</td>
      <td> </td>
    </tr>
    <tr>
      <td> </td>
      <td>our C++26 serializer</td>
      <td>1900</td>
      <td>19×</td>
    </tr>
    <tr>
      <td>Artificial Serialization</td>
      <td>nlohmann/json</td>
      <td>50</td>
      <td> </td>
    </tr>
    <tr>
      <td> </td>
      <td>our C++26 serializer</td>
      <td>1800</td>
      <td>36×</td>
    </tr>
  </tbody>
</table>

<p>Intel Ice Lake:</p>

<table>
  <thead>
    <tr>
      <th>Test Instance</th>
      <th>Library</th>
      <th>Speed (MB/s)</th>
      <th>speedup</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Twitter Serialization</td>
      <td>nlohmann/json</td>
      <td>110</td>
      <td> </td>
    </tr>
    <tr>
      <td> </td>
      <td>our C++26 serializer</td>
      <td>1800</td>
      <td>16×</td>
    </tr>
    <tr>
      <td>Artificial Serialization</td>
      <td>nlohmann/json</td>
      <td>50</td>
      <td> </td>
    </tr>
    <tr>
      <td> </td>
      <td>our C++26 serializer</td>
      <td>1400</td>
      <td>28×</td>
    </tr>
  </tbody>
</table>

<p>Our benchmark showed that we are roughly 20× faster than <a href="https://github.com/nlohmann/json">nlohmann/json</a>. This was achieved by combining the new reflection capabilities with <a href="https://lemire.me/blog/2024/05/31/quickly-checking-whether-a-string-needs-escaping/">some bit twiddling tricks</a> and a <a href="https://github.com/simdjson/experimental_json_builder/blob/main/src/string_builder.hpp">string_builder class</a> to help minimize the overhead of memory allocations.</p>

<p>Importantly, our implementation requires little code. The C++26 reflection capabilities do all the hard work.
In our benchmarks, we further compare against hand-written functions as well as an 
existing C++ reflection library (reflect-cpp). 
Hand-written code can be slightly faster than our reflection-based code, but the difference is
small (10% to 20%). We can be twice as fast as reflect-cpp although it is also a very fast library.</p>

<h2 id="conclusion">Conclusion</h2>

<p>One results suggest that C++26 with reflection is a powerful tool that should allow C++ programmers to easily serialize
and deserialize data structures at gigabytes per second.
In the near future, the <a href="https://github.com/simdjson/simdjson">simdjson library</a> will adopt
reflection for both serialization and deserialization.
Indeed, we expect that this will help users get 
high performance in some important cases with little effort.</p>

<h2 id="credit">Credit</h2>
<p>Joint work with the amazing <a href="https://twitter.com/lemire">Daniel Lemire</a>!</p>

<p>We are grateful to @the-moisrex for teaching us about tag dispatching and initiating its adoption in the simdson library.</p>

<h2 id="appendix-complete-code-example">Appendix: complete code example</h2>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">kid</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">age</span><span class="p">;</span>
  <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">name</span><span class="p">;</span>
  <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">toys</span><span class="p">;</span>
<span class="p">};</span>

<span class="cm">/**
 * The following function would print:
 *
 *   I am 12 years old
 *   I have a car
 *   I have a ball
 *   My name is John
 *   My JSON is {"age":12,"name":"John","toys":["car","ball"]}
 *
 */</span>
<span class="kt">void</span> <span class="n">demo</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">simdjson</span><span class="o">::</span><span class="n">padded_string</span> <span class="n">json_str</span> <span class="o">=</span>
      <span class="s">R"({"age": 12, "name": "John", "toys": ["car", "ball"]})"</span><span class="n">_padded</span><span class="p">;</span>
  <span class="n">simdjson</span><span class="o">::</span><span class="n">ondemand</span><span class="o">::</span><span class="n">parser</span> <span class="n">parser</span><span class="p">;</span>
  <span class="k">auto</span> <span class="n">doc</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">iterate</span><span class="p">(</span><span class="n">json_str</span><span class="p">);</span>
  <span class="n">kid</span> <span class="n">k</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="n">get</span><span class="o">&lt;</span><span class="n">kid</span><span class="o">&gt;</span><span class="p">();</span>
  <span class="n">std</span><span class="o">::</span><span class="n">print</span><span class="p">(</span><span class="s">"I am {} years old</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">k</span><span class="p">.</span><span class="n">age</span><span class="p">);</span>
  <span class="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span> <span class="o">&amp;</span><span class="n">toy</span> <span class="o">:</span> <span class="n">k</span><span class="p">.</span><span class="n">toys</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">print</span><span class="p">(</span><span class="s">"I have a {}</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">toy</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="n">std</span><span class="o">::</span><span class="n">print</span><span class="p">(</span><span class="s">"My name is {}</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">k</span><span class="p">.</span><span class="n">name</span><span class="p">);</span>

  <span class="n">std</span><span class="o">::</span><span class="n">print</span><span class="p">(</span><span class="s">"My JSON is {}</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">simdjson</span><span class="o">::</span><span class="n">json_builder</span><span class="o">::</span><span class="n">to_json_string</span><span class="p">(</span><span class="n">k</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Langdale, Geoff, and Daniel Lemire. “<a href="https://arxiv.org/abs/1902.08318">Parsing gigabytes of JSON per second</a>” The VLDB Journal 28.6 (2019): 941-960. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>Keiser, John, and Daniel Lemire. “<a href="https://arxiv.org/abs/2312.17149">On‐demand JSON: A better way to parse documents?</a>” Software: Practice and Experience 54.6 (2024): 1074-1086. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>Wyatt Childers, Peter Dimov, Dan Katz, Barry Revzin, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, <a href="[https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2996r3.html#converting-a-struct-to-a-tuple](https://isocpp.org/files/papers/P2996R4.html#converting-a-struct-to-a-tuple)">Reflection for C++26</a>, WG21 P2996R3, 2024-05-22 <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="Modern" /><category term="C++," /><category term="JSON," /><category term="Serialization," /><category term="Reflection," /><category term="C++26," /><category term="Performance" /><summary type="html"><![CDATA[JSON (JavaScript Object Notation) is a popular format for storing and transmitting data. It uses human-readable text to represent structured data in the form of attribute–value pairs and arrays. E.g., {"age":5, "name":"Daniel", toys:["wooden dog", "little car"]}. Ingesting and producing JSON documents can be a performance bottleneck.]]></summary></entry><entry><title type="html">Expected Linear-Time Minimum Spanning Trees</title><link href="http://chico.dev/Linear-Time-MST/" rel="alternate" type="text/html" title="Expected Linear-Time Minimum Spanning Trees" /><published>2022-11-12T00:00:00+00:00</published><updated>2022-11-12T00:00:00+00:00</updated><id>http://chico.dev/Linear-Time-MST</id><content type="html" xml:base="http://chico.dev/Linear-Time-MST/"><![CDATA[<h3 id="tldr">TLDR</h3>
<ul>
  <li>Implemented a complex expected linear time algorithm for finding Minimum Spanning
Tree.</li>
  <li>Found a bug in an implementation in a 13 years old paper.</li>
  <li>Code available at <a href="https://github.com/FranciscoThiesen/karger-klein-tarjan">github</a></li>
</ul>

<h2 id="context">Context</h2>

<p>It was the end of 2019 and I was searching for an interesting
topic for my CS undergraduate final thesis.
I was looking into randomized algorithms and was really
surprised to find out that there was an <a href="https://en.wikipedia.org/wiki/Expected_linear_time_MST_algorithm">expected linear time complexity algorithm for the Minimum
Spanning Tree problem</a>. 
The authors are Karger, Klein and Tarjan and from now on I’ll refer to it as
\(KKT\), not to be confused with <a href="https://en.wikipedia.org/wiki/Karush%E2%80%93Kuhn%E2%80%93Tucker_conditions">“Karush-Kuhn-Tucker”</a>.</p>

<p>In my algorithms class and also during my studies for the <a href="https://icpc.global/">ICPC</a> I had only
stumbled upon 3 different ways of solving the problems, with a complexity of
\(O(m \cdot \log(n))\) where \(m\) is the number of edges and \(n\) is the number of
vertices in the graph</p>
<ul>
  <li><a href="https://en.wikipedia.org/wiki/Bor%C5%AFvka%27s_algorithm">Borůvka</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Kruskal%27s_algorithm">Kruskal</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Prim%27s_algorithm">Prim</a></li>
</ul>

<figure style="text-align:center">
  <video autoplay="" loop="" muted="" playsinline="" style="max-width:100%">
    <source src="/images/boruvka.mp4" type="video/mp4" />
  </video>
  <figcaption>Cute <a href="https://commons.wikimedia.org/wiki/File:Boruvka%27s_algorithm_(Sollin%27s_algorithm)_Anim.gif">Animation of Borůvka algorithm</a>.</figcaption>
</figure>

<p>An algorithm with expected complexity of \(O(m + n)\) belongs to a complexity class
that grows slower than the class offered by the classical algorithms listed
above, so for graphs sufficiently large \(KKT\) algorithm should come in handy. At this
point I was super curious to test the performance of it against the classical
algorithms.</p>

<h2 id="lets-benchmark-it">Let’s benchmark it!</h2>
<p>Benchmarks are nice, but there was not going to be easy…
As of early 2020, I couldn’t find a working implementation of the
\(KKT\) algorithm that I could use for benchmarking purposes.</p>

<p>At this point I had 2 reasonable options:</p>
<ol>
  <li>Go find some other topic for my undergrad thesis.</li>
  <li>Be brave and implement this not-so-trivial \(KKT\) algorithm and make a publicly available implementation. 
This can work as a thesis + satisfy my personal curiosity + hopefully contribute a working implementation to
society.</li>
</ol>

<p>As you have probably guessed (given the title of this post), I went for option 2.</p>

<h2 id="when-implementing-a-complex-algorithm-how-can-we-find-out-estimate-the-implementation-complexity">When implementing a complex algorithm, how can we <del>find out</del> estimate the implementation complexity?</h2>
<p>When we are dealing with a simple algorithm to implement, it is much easier to
inspect every small detail of the implementation and become somewhat confident
that the implementation actually matches the theoretical complexity. Even in
those cases it is not unusual to find out situations where the implementation does not
behave as expected, like in the <a href="https://ai.googleblog.com/2006/06/extra-extra-read-all-about-it-nearly.html">Binary Search Bug</a>.</p>

<p>For more complex algorithms, things get even trickier. My approach was to use
the <a href="https://github.com/google/benchmark">google benchmark</a> project, that
fortunately has a complexity “estimation” feature built-in. It was also super
convenient to use. It is worth mentioning that this is only an <strong>estimate</strong> of the
implementation complexity and <strong>not a guarantee</strong>. To see how I used it in the scope
of my project, see <a href="https://github.com/FranciscoThiesen/karger-klein-tarjan/blob/master/benchmark/mst_benchmark.cpp">my benchmarking file</a>.</p>

<h2 id="hard-bug">Hard bug</h2>
<p>After <del>completing</del> thinking I had completed the implementation of \(KKT\). It
was natural for me to test my implementation with a rich set of different
graphs. I had ~3 months to complete the tests and write my thesis report
and prepare my presentation.</p>

<p>But there was a catch! I was validating the \(KKT\) result against the minimum
spanning trees produced by the other classical algorithms. When testing on
graphs with &gt; 1000 nodes, I got an error in about 1% of the instances, in which
the solution discovered by \(KKT\) were around 1% worse than the solutions found
by all the 3 classical algorithms.</p>

<p>It was something terribly hard to debug, because I couldn’t reproduce the issue
with graphs of a smaller size. As someone who has made a lot of implementation
mistakes in the past, my usual instict is to assume the error is in some
implementation mistake I made. This reminds me of the time when <a href="https://youtu.be/FXL2G1p-EDw?t=521">Rafael Nadal
played with a broken racket</a> when he was 15, because he was so used to taking
responsibility for his losses that the idea that the racket was broken didn’t
even cross his mind.</p>

<p>After scrutinizing the code for a long time and
adding A LOT of print statements, I started suspecting that the error might not
be on me this time. How could that be?</p>

<p>Well, \(KKT\) leverages another complicated algorithm as a fundamental step.
This step basically needs to check if a certain tree \(T\) is a minimum spanning
tree for a certain graph, which is not something specially hard. The hard part
is that needs to have \(O(m + n)\) complexity and that makes the implementation much harder.</p>

<p>It was actually so complicated that it inspired funny paper names:</p>
<ol>
  <li><a href="https://www.researchgate.net/publication/3770673_Linear_Verification_For_Spanning_Trees">Linear Verification For Spanning Trees, 1984 Komlós</a></li>
  <li><a href="https://www.researchgate.net/publication/225138362_A_Simpler_Minimum_Spanning_Tree_Verification_Algorithm">A Simpler Minimum Spanning Tree Verification Algorithm, 1997 King</a></li>
  <li><a href="https://link.springer.com/chapter/10.1007/978-3-642-11409-0_16">An Even Simpler Linear-Time Algorithm for Verifying Minimum Spanning Trees,
2009 Hagerup</a></li>
</ol>

<p>I am forever grateful to Hagerup, which is the only author that included his
implementation as part of the paper. After a lot of time banging my
head against the wall, I was able to find a really subtle typo in the code he
provided. A beautiful case of finding the needle in the haystack.</p>

<p><strong>Wrong version from paper</strong>:<br />
  <code class="language-plaintext highlighter-rouge">S = down(D[v], S &amp; (1 &lt;&lt; (k + 1) - 1) | (1 &lt;&lt; depth[v]));</code><br />
<strong>Corrected version</strong>       : <br />
  <code class="language-plaintext highlighter-rouge">S = down(D[v], S &amp; ((1 &lt;&lt; (k + 1)) - 1) | (1 &lt;&lt; depth[v]));</code></p>

<p>Fixing this line made my implementation work (at least with my tests).</p>

<h2 id="ok-but-how-fast-is-kkt-anyway">Ok, but how fast is \(KKT\) anyway?</h2>
<p>As I mentioned earlier, I used the google-benchmark code to estimate the runtime
complexity of my implementation. Another cool fact is that is also estimates the
constants in your implementation.</p>

<p>In Big-\(O\) analysis we ignore constants, but they can make a
huge difference in the runtime of your algorithm and how large the instances
have to be to make one algorithm “better” than other. You can see my benchmark
results <a href="https://github.com/FranciscoThiesen/karger-klein-tarjan/blob/master/benchmark/benchmark_result.txt">here</a>.</p>

<p>Estimated runtime of Borůvka was \(4.67 * N * \log_{2}{N}\) and for
\(KKT\) the estimated complexity was \(2357 * N\), where \(N\) here is total
edges + nodes in the graph.</p>

<p>So, for sufficiently large \(N\) we expect that \(KKT\) will beat Borůvka. But
how large does \(N\) have to be?</p>

<p><img src="/images/inequation_solution.png" alt="_config.yml" /></p>

<p>So assuming this equation returned by the benchmark library holds for larger
graphs, we would need a graph with \(|Nodes| + |Edges| &gt; 9 * 10^{151}\).</p>

<p>A quick research tells us that:</p>
<ul>
  <li>Total amount of information accumulated by humanity to date is estimated to be
 around <a href="https://superscholar.org/total-storage-capacity-of-humanity-295-exabytes/">\(2.95 * 10^{20}\) bytes</a>.</li>
  <li>The number of atoms in the observable universe is estimated to be in the
 order of <a href="https://www.livescience.com/how-many-atoms-in-universe.html">\(10 * 10^{82}\)</a>.</li>
</ul>

<p>So not only humanity today is not able to store a graph large enough for my
\(KKT\) implementation to beat my Borůvka implementation, but even if the graph
had the size equal to the amount of atoms in the observable universe that would
not be large enough. There is a name for algorithms like that, they are called
<a href="https://en.wikipedia.org/wiki/Galactic_algorithm">Galactic Algorithms</a> (:</p>

<h2 id="final-remarks">Final remarks</h2>
<ul>
  <li>Even though Big-\(O\) analysis ignores constants, they can make a certain
algorithm impractical for real world instances.</li>
  <li>It is obviously possible to try to reduce the constant of my \(KKT\)
implementation, but I don’t believe it will make the algorithm better than the
classical alternatives for real world examples.</li>
  <li>The analysis used estimates of the implementation runtime complexity, this will 
change if a different set of tests are used for benchmarking.</li>
  <li>At least we now have a <a href="https://github.com/FranciscoThiesen/karger-klein-tarjan">publicly available implementation</a>
 of expected linear time algorithm to finding minimum spanning trees.</li>
  <li>Be aware that when you propose a complicated algorithm, you might cause a
student to try to implement it 10/20 years later (:</li>
</ul>

<h2 id="credits">Credits</h2>
<p>I want to thank all the amazing algorithms professors that I had in life,
  who taught me with great enthusiasm and also offered several suggestions on how to improve
  my thesis/this blog post.</p>
<ul>
  <li><a href="http://www-di.inf.puc-rio.br/~poggi//index.html">Marcus Poggi</a></li>
  <li><a href="https://www-di.inf.puc-rio.br/~laber/">Eduardo Laber</a></li>
  <li><a href="https://www-di.inf.puc-rio.br/~mmolinaro/">Marco Molinaro</a></li>
  <li>Daniel Fleischman</li>
</ul>

<h2 id="unanswered-questions--additional-thoughts">Unanswered questions / additional thoughts</h2>
<ul>
  <li>Is it worth the time trying to reduce the constant of my \(KKT\)
implementation? A significant speed-up could make this somewhat useful for
really large (but real-world) instances.</li>
  <li>A breakdown of the time spent in each function of \(KKT\) implementation would
be interesting to see.</li>
  <li>Is there a class of graphs in which \(KKT\) is more “competitive”?</li>
</ul>

<h2 id="relevant-links">Relevant links</h2>
<ul>
  <li><a href="https://github.com/FranciscoThiesen/karger-klein-tarjan/blob/master/relatorio_final.pdf">My final thesis report (portuguese)</a></li>
</ul>

<h3 id="suggestions">Suggestions</h3>
<p>I am always open to suggestions and/or feedback, if you have something to say
please send me an <a href="mailto:franciscogthiesen@gmail.com">e-mail</a></p>]]></content><author><name></name></author><category term="galactic-algorithms" /><category term="algorithms" /><category term="minimum-spanning-trees" /><category term="karger-klein-tarjan" /><summary type="html"><![CDATA[TLDR Implemented a complex expected linear time algorithm for finding Minimum Spanning Tree. Found a bug in an implementation in a 13 years old paper. Code available at github]]></summary></entry></feed>