<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Devansh Kant Kashyap</title>
    <description>The latest articles on DEV Community by Devansh Kant Kashyap (@devanshkant).</description>
    <link>https://dev.to/devanshkant</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4067449%2Fd26d513e-d4b4-4ace-8a21-40716fbe1af4.png</url>
      <title>DEV Community: Devansh Kant Kashyap</title>
      <link>https://dev.to/devanshkant</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devanshkant"/>
    <language>en</language>
    <item>
      <title>We ported JSBI to C++ — and the benchmark taught us more than the port did</title>
      <dc:creator>Devansh Kant Kashyap</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:40:46 +0000</pubDate>
      <link>https://dev.to/devanshkant/we-ported-jsbi-to-c-and-the-benchmark-taught-us-more-than-the-port-did-14c</link>
      <guid>https://dev.to/devanshkant/we-ported-jsbi-to-c-and-the-benchmark-taught-us-more-than-the-port-did-14c</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We ported &lt;a href="https://github.com/GoogleChromeLabs/jsbi" rel="noopener noreferrer"&gt;GoogleChromeLabs/jsbi&lt;/a&gt; — a pure-JS arbitrary-precision integer library — to modern C++17, bridged back to Node via N-API, for Port Mortem 2026 (Track H). The port itself wasn't the hard part. Proving it behaved identically to the original — and being honest about the one place we &lt;em&gt;couldn't&lt;/em&gt; prove that — was.&lt;/p&gt;

&lt;p&gt;This is the story of a compile error that was secretly a correctness bug, a &lt;code&gt;shared_ptr&lt;/code&gt; that lied about owning memory, and a benchmark that changed its mind three times in a row, and what we did when it wouldn't sit still.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JSBI → C++, of all pairs
&lt;/h2&gt;

&lt;p&gt;JSBI isn't really a JavaScript library. It's V8's internal &lt;code&gt;MutableBigInt&lt;/code&gt; — written in C++ — manually &lt;em&gt;downgraded&lt;/em&gt; to 30-bit digits so its arithmetic could survive inside JavaScript's &lt;code&gt;2^53&lt;/code&gt; safe-integer ceiling without silently losing precision. Every multiply in the original source carries a 15-bit split trick that exists for exactly one reason: JS numbers can't safely hold the product of two 32-bit values.&lt;/p&gt;

&lt;p&gt;So porting it back to C++ isn't a random language swap for Track H credit. It's undoing a constraint. In C++, a &lt;code&gt;uint64_t&lt;/code&gt; accumulator holds that same product natively — no split needed. We weren't rewriting an algorithm. We were giving one back its native width.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original: V8 C++ MutableBigInt → downgraded to 30-bit digits → shipped as JS
This port: JS jsbi → C++17, native 64-bit accumulators → the loop closes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The verification pipeline
&lt;/h2&gt;

&lt;p&gt;Two of us, 72 hours. The port had to run the &lt;em&gt;original&lt;/em&gt; test suite unmodified — not a translated copy, the literal upstream files, pinned via git submodule at the kickoff commit hash, so nobody has to take our word for what "unmodified" means.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/original-upstream/*.mjs   (pinned submodule, byte-identical to upstream)
              │  import JSBI from '../dist/jsbi.mjs'
              ▼
tests/dist/jsbi.mjs             (thin JS bridge — API shape only, zero math logic)
              │  require(native addon)
              ▼
src/addon.cpp                   (N-API boundary — ownership + exception mapping)
              │
              ▼
src/jsbi.cpp / jsbi.hpp         (the actual math — zero Node/V8 headers, by design)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line matters more than it looks: the core engine has &lt;em&gt;zero&lt;/em&gt; includes from Node or V8. You can &lt;code&gt;grep -L napi.h src/*.cpp&lt;/code&gt; and confirm it yourself. If this project ever needed to compile standalone — WASM, embedded, whatever — the math layer doesn't care that N-API exists.&lt;/p&gt;

&lt;p&gt;On top of the pinned test suite, we ran a three-way differential fuzzer every session: native V8 &lt;code&gt;BigInt&lt;/code&gt;, the real upstream &lt;code&gt;jsbi&lt;/code&gt; npm package, and our port, on identical inputs, for 60+ seconds at a time. Three-way, not two — because a two-way comparison against native &lt;code&gt;BigInt&lt;/code&gt; alone can't tell you whether a divergence is in your math or in your bridge wrapper. Three-way can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;209,401 iterations, zero divergences, on the final clean run.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke — four bugs, in increasing order of "wait, that's not a style nitpick"
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The compile error that was hiding a correctness question
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was in our division algorithm. It looks like a harmless one-liner. It's actually ill-formed C++ — the ternary operator requires both branches to share a common type, and &lt;code&gt;void&lt;/code&gt; is only compatible with another &lt;code&gt;void&lt;/code&gt;, or with a &lt;code&gt;throw&lt;/code&gt;-expression. &lt;code&gt;r.digits[0] |= 1&lt;/code&gt; has type &lt;code&gt;uint32_t&amp;amp;&lt;/code&gt;. Neither branch qualifies. GCC and Clang under &lt;code&gt;-std=c++17&lt;/code&gt; reject this outright. It happened to &lt;em&gt;build&lt;/em&gt; on our first Windows/MSVC pass, which is a genuinely dangerous kind of luck — it meant our first "it compiles" signal was compiler-specific, not portable. We didn't find this by reading docs. We found it by trying to build on a second toolchain and watching it fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A &lt;code&gt;shared_ptr&lt;/code&gt; that owned nothing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;shared_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jsbi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;JSBI_CPP&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;shared_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jsbi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;JSBI_CPP&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the aliasing constructor, called with an &lt;em&gt;empty&lt;/em&gt; control block. It type-checks. It has &lt;code&gt;-&amp;gt;&lt;/code&gt; and &lt;code&gt;.get()&lt;/code&gt;. Its &lt;code&gt;use_count()&lt;/code&gt; is zero. It contributes nothing to keeping the object alive — it's a raw pointer wearing a &lt;code&gt;shared_ptr&lt;/code&gt; costume, sitting in a codebase whose entire pitch for the Zero-Unsafe bonus was "we don't do that." The fix was smaller than the bug: return an honest, documented, non-owning raw pointer instead, and let the &lt;em&gt;real&lt;/em&gt; &lt;code&gt;shared_ptr&lt;/code&gt; — the one captured in the N-API finalizer closure — do the actual owning. Faking safety is worse than admitting you're borrowing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The spec quirk nobody wrote a test for, until the fuzzer got wide enough
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_negative&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;invalid_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cannot shift by negative amount"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reasonable-looking guard. Spec-wrong. &lt;code&gt;5n &amp;lt;&amp;lt; -1n&lt;/code&gt; doesn't throw in real BigInt — it redirects to &lt;code&gt;5n &amp;gt;&amp;gt; 1n&lt;/code&gt;. Negative shift counts aren't an error, they're a direction flip. This bug was &lt;em&gt;invisible&lt;/em&gt; to our own fuzz harness for a while, because our shift-amount generator only ever produced values &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;64&lt;/code&gt;. The bug wasn't in the math. It was in the range of inputs we were brave enough to generate. Widening the generator to &lt;code&gt;-64..64&lt;/code&gt; is what actually caught it — not code review, not the original test suite, just deciding to stop being polite to our own implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The sign character nobody thought to negate
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;BigInt("-0x1")&lt;/code&gt; throws — non-decimal radixes don't accept a sign, and our code correctly rejected the &lt;code&gt;-&lt;/code&gt; case. &lt;code&gt;BigInt("+0x1")&lt;/code&gt; should &lt;em&gt;also&lt;/em&gt; throw, for the same reason. Ours didn't. We were only tracking &lt;code&gt;is_negative&lt;/code&gt;, and &lt;code&gt;+&lt;/code&gt; doesn't set that flag — so a &lt;code&gt;+&lt;/code&gt; before a hex prefix sailed straight through unguarded. Caught during a manual code-review pass, not fuzzing, because our fuzz string generator never happened to emit a leading &lt;code&gt;+&lt;/code&gt; before a &lt;code&gt;0x&lt;/code&gt;. A reminder that fuzzing finds what your generator can imagine, and code review finds what it can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark that wouldn't agree with itself
&lt;/h2&gt;

&lt;p&gt;Here's the part I actually want other teams to read.&lt;/p&gt;

&lt;p&gt;We built an honest benchmark harness — real &lt;code&gt;hrtime.bigint()&lt;/code&gt; measurements, forced GC between blocks, identical operand pairs fed to both implementations, three operand sizes so we couldn't hide behind a single flattering number. First run, large operands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Original JS&lt;/th&gt;
&lt;th&gt;Our Port&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3.78ms p99&lt;/td&gt;
&lt;td&gt;1.60ms p99&lt;/td&gt;
&lt;td&gt;Port wins, clearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;divide&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.60ms p99&lt;/td&gt;
&lt;td&gt;3.63ms p99&lt;/td&gt;
&lt;td&gt;Port loses, clearly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Clean story. Port wins at addition, loses at division — makes sense, our division is a bit-serial restoring-division algorithm, upstream's is presumably limb-serial. Write it up, move on.&lt;/p&gt;

&lt;p&gt;Except we ran it again.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Run 1&lt;/th&gt;
&lt;th&gt;Run 2&lt;/th&gt;
&lt;th&gt;Run 3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;add&lt;/code&gt; (port vs original)&lt;/td&gt;
&lt;td&gt;1.75× faster&lt;/td&gt;
&lt;td&gt;1.09× faster&lt;/td&gt;
&lt;td&gt;0.95× (slightly &lt;em&gt;slower&lt;/em&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;divide&lt;/code&gt; (port vs original)&lt;/td&gt;
&lt;td&gt;0.69×&lt;/td&gt;
&lt;td&gt;1.08×&lt;/td&gt;
&lt;td&gt;0.86×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three runs. Same machine. Same code. No consistent winner on &lt;em&gt;any&lt;/em&gt; operation. The "our division algorithm is measurably worse" story I was ready to write didn't survive a second data point.&lt;/p&gt;

&lt;p&gt;We had two options. Report the run that told the best story. Or report what actually happened.&lt;/p&gt;

&lt;p&gt;We reported the range. All three runs, in the README, with the honest conclusion: measurement noise on a general-purpose Windows machine dominates whatever true performance difference exists, we don't have a reliable claim to make, and here's exactly why (shared long-lived process across every block, non-isolated host, cumulative RSS climbing past 1.2GB by the last measurement). We even named the fix we didn't have time to do — isolated process per block, Linux CI runner, median-of-ten instead of a point estimate — as explicit future work instead of a silent gap.&lt;/p&gt;

&lt;p&gt;That's not the exciting version. It's the true one. And per this hackathon's own scoring language — &lt;em&gt;hiding a regression scores worse than disclosing it&lt;/em&gt; — inconclusive-and-honest beats confident-and-wrong every time a judge actually checks your numbers instead of just reading your headline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision I'd take back
&lt;/h2&gt;

&lt;p&gt;We spent real hours getting the N-API bridge to a place where the &lt;em&gt;literal, unmodified, pinned&lt;/em&gt; upstream test files could &lt;code&gt;require()&lt;/code&gt; our compiled binary directly — zero-touch, not a translated copy. It's the more defensible architecture, and I'd make the same call again on the merits. But it cost us a full evening fighting Windows toolchain issues that had nothing to do with C++: &lt;code&gt;node-gyp&lt;/code&gt; not recognizing a newer Visual Studio release, a Build Tools install silently missing the Windows SDK component, the kind of error that eats a clock without teaching you anything about your actual port.&lt;/p&gt;

&lt;p&gt;If I ran this again, I'd stand up a Linux/WSL2 build path in the &lt;em&gt;first&lt;/em&gt; hour, not discover I needed one during a compile failure. The bridge architecture was worth it. The hours lost to a toolchain neither of us had properly checked ahead of time were not — that's just tax, and I'd pay it earlier and smaller if I could.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final scorecard
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;246/246&lt;/strong&gt; assertions passing against the pinned, unmodified upstream test suite (submodule-hashed at kickoff, not a copy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;209,401&lt;/strong&gt; three-way differential fuzz iterations, zero divergences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;33+ entries&lt;/strong&gt; in our decision log — including, deliberately, the ones marked &lt;em&gt;Superseded&lt;/em&gt; and &lt;em&gt;Known Limitation&lt;/em&gt;, because a log that only records what worked isn't a log, it's a highlight reel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero&lt;/strong&gt; raw &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt;, zero fake ownership (after we caught the one that was faking it), zero Node/V8 dependency in the math core&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One benchmark result we can't confidently claim&lt;/strong&gt; — and said so, in writing, instead of picking the run that looked good&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/codewisp-ai/Coderesurrection-2026" rel="noopener noreferrer"&gt;https://github.com/codewisp-ai/Coderesurrection-2026&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo (original test suite passing live against the port): &lt;a href="https://youtu.be/UKKCHzXfF5Y" rel="noopener noreferrer"&gt;https://youtu.be/UKKCHzXfF5Y&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Full decision log: &lt;code&gt;DECISIONS.md&lt;/code&gt; in the repo — read the &lt;em&gt;Superseded&lt;/em&gt; entries first, they're more honest than the accepted ones&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Anyone with an AI coding agent can produce C++ that compiles. Ours didn't, the first time — and the compiler catching that was luckier than it should have been, since a more permissive toolchain let it through initially.&lt;/p&gt;

&lt;p&gt;What we actually spent 72 hours on wasn't writing arithmetic. It was building enough ways to catch ourselves being wrong — a pinned test suite we couldn't quietly edit, a three-way fuzzer that could tell our math bugs from our bridge bugs, a benchmark methodology honest enough to report its own noise instead of its best headline.&lt;/p&gt;

&lt;p&gt;The port is the artifact. The willingness to publish the run that didn't flatter us is the actual submission.&lt;/p&gt;

&lt;h1&gt;
  
  
  PortMortem2026 #HackathonRaptors #Cpp #JavaScript #SystemsProgramming #Testing #NAPI
&lt;/h1&gt;

</description>
      <category>cpp</category>
      <category>javascript</category>
      <category>systemsprogramming</category>
      <category>hackathonraptors</category>
    </item>
  </channel>
</rss>
