<?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: soso</title>
    <description>The latest articles on DEV Community by soso (@generalsimus).</description>
    <link>https://dev.to/generalsimus</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%2F4192692%2Fd009a2e0-46d8-49a9-9ee2-3dea1ab0dc74.png</url>
      <title>DEV Community: soso</title>
      <link>https://dev.to/generalsimus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/generalsimus"/>
    <language>en</language>
    <item>
      <title>Native BigInt. Faster than decimal.js</title>
      <dc:creator>soso</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:37:05 +0000</pubDate>
      <link>https://dev.to/generalsimus/native-bigint-and-its-10-2800x-faster-than-decimaljs-5c5n</link>
      <guid>https://dev.to/generalsimus/native-bigint-and-its-10-2800x-faster-than-decimaljs-5c5n</guid>
      <description>&lt;p&gt;You've probably seen this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0.30000000000000004&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The usual answer is "just use decimal.js". And honestly, that's good advice. decimal.js, big.js and bignumber.js are great libraries. But they were all written before JavaScript had &lt;code&gt;BigInt&lt;/code&gt;, so under the hood they keep digits in arrays and do every multiplication and division by hand, in JavaScript.&lt;/p&gt;

&lt;p&gt;That made me curious. What if a decimal library just handed the hard part to &lt;code&gt;BigInt&lt;/code&gt; and let V8 do the work in native code?&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/Generalsimus/bigint-decimal" rel="noopener noreferrer"&gt;bigint-decimal&lt;/a&gt; to find out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BigintDecimal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bigint-decimal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BigintDecimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// "0.3"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BigintDecimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;19.99&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// 3 items&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0825&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// plus 8.25% tax&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// rounded to cents&lt;/span&gt;

&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// "64.92"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The basic trick
&lt;/h2&gt;

&lt;p&gt;The idea is almost embarrassingly simple. A number like &lt;code&gt;123.45&lt;/code&gt; is stored as the integer &lt;code&gt;12345n&lt;/code&gt;, plus a note that there are 2 decimal places.&lt;/p&gt;

&lt;p&gt;That's it. Adding, subtracting and multiplying are then just normal &lt;code&gt;BigInt&lt;/code&gt; math, so they're exact. And they're fast, because V8 already uses clever algorithms for big numbers (Karatsuba for multiplication, Burnikel–Ziegler for division). I didn't have to write any of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fun part: sqrt, exp, log, sin...
&lt;/h2&gt;

&lt;p&gt;Exact arithmetic was the easy bit. I also wanted almost the whole &lt;code&gt;Math&lt;/code&gt; object, &lt;code&gt;sqrt&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;sin&lt;/code&gt;, &lt;code&gt;atan2&lt;/code&gt;, &lt;code&gt;pow&lt;/code&gt; and so on, working at any precision you ask for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;BigintDecimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPrecisionGlobal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;BigintDecimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// "3.14159265358979323846264338327950288419716939937511"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where I went down a rabbit hole. Most of it came from reading Fredrik Johansson's papers on computing elementary functions, and a few ideas made a huge difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inside the math functions, numbers are stored in binary instead of decimal. That sounds like a small detail, but it means rescaling a number is a bit shift instead of a big division.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exp(x)&lt;/code&gt; gets split into &lt;code&gt;2^n · exp(r)&lt;/code&gt; where &lt;code&gt;r&lt;/code&gt; is tiny, and multiplying by &lt;code&gt;2^n&lt;/code&gt; is, again, just a shift.&lt;/li&gt;
&lt;li&gt;At high precision, the Taylor series use something called rectangular splitting. Instead of one expensive multiplication per term, you need roughly the square root of that.&lt;/li&gt;
&lt;li&gt;Constants like π are computed with the Chudnovsky series and binary splitting, the same method used for world-record π computations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every result is computed with some extra digits and rounded once at the end. &lt;code&gt;sqrt&lt;/code&gt; and &lt;code&gt;cbrt&lt;/code&gt; are always correctly rounded, even in the annoying case where the true answer lands exactly halfway between two possible results.&lt;/p&gt;

&lt;h2&gt;
  
  
  OK, but is it actually faster?
&lt;/h2&gt;

&lt;p&gt;I wrote a benchmark comparing it with decimal.js, bignumber.js and big.js. Every library computes the same thing to the same number of digits, and each time is an average over 16 different inputs (Node 24, Ryzen 9 7940HS).&lt;/p&gt;

&lt;p&gt;At 20 digits, which is what most apps need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Speed-up&lt;/th&gt;
&lt;th&gt;bigint-decimal&lt;/th&gt;
&lt;th&gt;decimal.js&lt;/th&gt;
&lt;th&gt;bignumber.js&lt;/th&gt;
&lt;th&gt;big.js&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic arithmetic&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.4×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.11 µs&lt;/td&gt;
&lt;td&gt;1.58 µs&lt;/td&gt;
&lt;td&gt;3.09 µs&lt;/td&gt;
&lt;td&gt;3.13 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqrt&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.02 µs&lt;/td&gt;
&lt;td&gt;6.37 µs&lt;/td&gt;
&lt;td&gt;6.08 µs&lt;/td&gt;
&lt;td&gt;43 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exp&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.88 µs&lt;/td&gt;
&lt;td&gt;29 µs&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;log&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.85 µs&lt;/td&gt;
&lt;td&gt;25 µs&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;atan&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;45×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.65 µs&lt;/td&gt;
&lt;td&gt;163 µs&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 1000 digits, the gap gets kind of ridiculous:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Speed-up&lt;/th&gt;
&lt;th&gt;bigint-decimal&lt;/th&gt;
&lt;th&gt;decimal.js&lt;/th&gt;
&lt;th&gt;bignumber.js&lt;/th&gt;
&lt;th&gt;big.js&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic arithmetic&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.4×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.85 µs&lt;/td&gt;
&lt;td&gt;5.32 µs&lt;/td&gt;
&lt;td&gt;15 µs&lt;/td&gt;
&lt;td&gt;68 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqrt&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;64 µs&lt;/td&gt;
&lt;td&gt;2.6 ms&lt;/td&gt;
&lt;td&gt;1.9 ms&lt;/td&gt;
&lt;td&gt;195 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exp&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;169×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;224 µs&lt;/td&gt;
&lt;td&gt;37.8 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;log&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;586×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;215 µs&lt;/td&gt;
&lt;td&gt;126 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;atan&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2826×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;734 µs&lt;/td&gt;
&lt;td&gt;2074 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;("—" means that library doesn't have the function.)&lt;/p&gt;

&lt;p&gt;Before anyone asks: no, it's not faster because it cuts corners. At 1000 digits, both libraries give correctly rounded results. I checked. And decimal.js actually can't go much higher for &lt;code&gt;log&lt;/code&gt; and &lt;code&gt;pow&lt;/code&gt;. Somewhere around 1025 digits it throws &lt;code&gt;Precision limit exceeded&lt;/code&gt;, because its ln(10) constant is a fixed 1025-digit string.&lt;/p&gt;

&lt;p&gt;The benchmark is in the repo, so please don't just take my word for it. Run &lt;code&gt;npm run bench&lt;/code&gt; and see what you get.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making sure it's right
&lt;/h2&gt;

&lt;p&gt;Being fast and wrong is easy, so honestly most of my time went into testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;About 300 results, from 20 up to 2000 digits, are compared digit by digit with Python's &lt;code&gt;decimal&lt;/code&gt; module. That includes the nasty cases: &lt;code&gt;asin&lt;/code&gt; right next to 1, &lt;code&gt;tan&lt;/code&gt; right next to π/2, &lt;code&gt;sin(1e300)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Arithmetic and rounding on random huge numbers are checked against plain &lt;code&gt;BigInt&lt;/code&gt; math.&lt;/li&gt;
&lt;li&gt;I also tried mutation testing. I deliberately broke the code in 60 different ways to see if the tests would notice. They caught every bug that could change a result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it paid off. The tests found real bugs before release. &lt;code&gt;asin&lt;/code&gt; was losing precision right next to ±1, exact halfway results were rounding the wrong way, and &lt;code&gt;tan&lt;/code&gt; was throwing an error near π/2 where the real answer is just a very big number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do (yet)
&lt;/h2&gt;

&lt;p&gt;I don't want to oversell this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Precision means decimal places, not significant digits. So really tiny results round to &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;div&lt;/code&gt; cuts off extra digits instead of rounding them.&lt;/li&gt;
&lt;li&gt;There's only one rounding mode, and no formatting helpers like &lt;code&gt;toFixed&lt;/code&gt;. If you need those, decimal.js is still the more complete library.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;bigint-decimal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything's on &lt;a href="https://github.com/Generalsimus/bigint-decimal" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;: code, docs and the benchmark. I'd really love feedback, especially if you need high-precision math in JavaScript, or you know a trick I missed.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>node</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
