How I Wrote A Fixed-Point Solidity Sqrt That Runs In 197 Gas
I live by a simple rule: if a function is on your pocket calculator, I want it in Solidity — as fast as possible, and precise enough that you never have to think about it. Square root is exactly that kind of workhorse, so I'll skip the introduction and jump to the result: DeFiMath's sqrt takes any uint256, returns an 18-decimal fixed-point value, and does it in 197 gas — bit-exact when the result is below 1, and within 2e-18 relative error above it.
That efficiency matters because sqrt is everywhere. This started as part of optimizing Black-Scholes — one call per option price — but sqrt also shows up in every Greek, several times over inside an implied-volatility solve, and buried in almost every derivative, DeFi protocol, and serious piece of on-chain math.
Here's how it works, top to bottom.
Sqrt Function Code
Let's take a look at the Solidity code directly, then analyze it, line by line.
/// @notice Computes square root of x in 18-decimal fixed-point.
/// @dev Accepts the full uint256 domain, never reverts.
/// Max relative error: < 2e-18 for any x >= 1e18.
/// Max absolute error: bit-exact for any x < 1e18.
/// @param x Input in 18-decimal fixed-point format.
/// @return y Square root in 18-decimal fixed-point format.
function sqrt(uint256 x) internal pure returns (uint256 y) {
unchecked {
if (x <= type(uint128).max) {
assembly ("memory-safe") {
// pre-scale x to 1e36 base (because x can be small)
x := mul(x, 1000000000000000000)
// generate seed y using clz
y := shl(shr(1, sub(254, clz(x))), 2)
// refine y using 5x Newton's method
y := shr(1, add(y, div(x, y)))
y := shr(1, add(y, div(x, y)))
y := shr(1, add(y, div(x, y)))
y := shr(1, add(y, div(x, y)))
y := shr(1, add(y, div(x, y)))
}
} else {
assembly ("memory-safe") {
// generate seed y using clz
y := shl(shr(1, sub(254, clz(x))), 2)
// refine y using 5x Newton's method
y := shr(1, add(y, div(x, y)))
y := shr(1, add(y, div(x, y)))
y := shr(1, add(y, div(x, y)))
y := shr(1, add(y, div(x, y)))
y := shr(1, add(y, div(x, y)))
// post-scale y to 1e18 base
y := mul(y, 1000000000)
}
}
}
}
As we can see, the whole function is wrapped inside of unchecked block, which helps keep gas cost low. But we must be careful not to overflow or underflow anywhere. We can also see that the function is divided into 2 parts, each in almost identical assembly blocks. We are using assembly here because it's cheaper. The first half of the function calculates sqrt of x that is ≤ uint128 max, which probably covers 99% of usage in DeFi. Let's analyze the if branch.
If we ignore scaling to 1e36 base (which I will explain later), we are left with only 6 lines of code, which are enough to calculate sqrt down to the specified precision (2e-18). In case you need more precision than I specified, just add one more Newton iteration. The algorithm is simple: generate a close-enough seed value y ≈ sqrt(x), and then refine it using Newton's method.
Newton's Method
Newton's method helps us find the square root by iterating over seed y. Each iteration gets us closer to the solution, and it has some nice properties: it's simple and cheap gas-wise, and it converges to the solution quickly if we start close enough. Here's Newton's method:
yₙ₊₁ = (yₙ + x / yₙ) / 2
Each iteration takes the current estimate yₙ, averages it with x / yₙ, and produces a better estimate yₙ₊₁. Convergence is quadratic — the number of correct digits roughly doubles with every step. It is extremely efficient, especially if the seed is close to true y. In Solidity, we implement each step like this:
y := shr(1, add(y, div(x, y)))
We use shift right to divide by 2, because shr is cheaper than div. This line is measured to cost 20 gas, so 5 Newton iterations cost around 100 gas. Also, an important note here is that x is already in 1e36 base, while y is in 1e18, so when we do div(x, y), we are back to 1e18 base. Scaling x once to 1e36 at the beginning helps us lower the cost of Newton's method, allowing us to avoid costly muldiv by using div only.
Seed Algorithm
I am using clz opcode extensively throughout DeFiMath to approximate the scale of a number, as I wrote in Counting Leading Zeros In Solidity Using CLZ Opcode. For sqrt specifically, clz gives us a seed y that is close enough to the actual solution.
The best way to understand how seed y is generated is to analyze this line (the shipped code uses an equivalent form — I'll explain why at the end):
y := shl(shr(1, sub(256, clz(x))), 1)
Seeding works like this:
- we find the scale of x by counting leading zeros, and then we subtract leading zeros from 256 to get the scale of the number (most significant bit or msb).
- once we find msb, we divide it by 2, using the shr opcode
- then we generate the seed: seed = 2^(msb/2), using the shl opcode
With this simple method our seed y is always close to real y — it's never off by more than a factor of √2. And that is important to know when choosing the number of iterations that come after seeding: 5 iterations are more than enough for the worst case seed. Here's the table showing how the worst case seed (for example x = 2048) with max relative error of 0.41 (41%) converges to real y:
| Iteration | y | Relative error | Accurate bits |
|---|---|---|---|
| seed | 64 | 4.1e-1 (41%) | 1.3 |
| 1 | 48 | 6.1e-2 | 4.0 |
| 2 | 45.33333333333333 | 1.7e-3 | 9.2 |
| 3 | 45.25490196078431 | 1.5e-6 | 19.3 |
| 4 | 45.25483399599008 | 1.1e-12 | 39.7 |
| 5 | 45.25483399593904 | 6.4e-25 | 80.4 |
We see that 5 Newton iterations are enough to get to 80 bits of precision for the worst case seed. 80 bits is more than enough for 18-decimal fixed-point format. It gives us bit-exact y when sqrt(x) < 1, and max relative error below 2e-18 when sqrt(x) >= 1.
Final Optimization
I tried using different seed methods to get closer to true y so that we can reduce Newton iterations to 4 and shave off 20 gas. The simplest and cheapest way I found was to use minimax linear approximation of y on the narrow range of x in [1, 4], but that was costing more in gas. I also tried quadratic interpolation, but that was even more expensive.
In the end, I tried to tweak parameters in this line:
y := shl(shr(1, sub(256, clz(x))), 1)
I tried changing that last shift-left value to something other than 1, together with reducing the 256. I printed everything so we could see whether we were getting closer to true y with different seeding parameters:

The table shows 3 different versions of seed generation, using y1, y2, and y3, each with different last number and adjusted second number. We can notice that y1 and y2 were producing exactly the same seed, while y3 was producing a slightly higher seed with a higher max relative error.
After analyzing a lot of versions, I decided to write the seed as (254, 2) instead of (256, 1) because it shaves one byte off the bytecode (254 is a single-byte PUSH), and in this version the optimizer happens to schedule it one gas cheaper — though that last gas is a stack-scheduling artifact, not a property of the seed itself.
The else branch is used for large inputs, and works almost identically to the if branch, but with one scaling trick — post-scaling instead of pre-scaling, for 2 reasons:
- x is large enough that we don't need to scale to preserve information through the algorithm, and
- pre-scaling would overflow for inputs close to uint256.max
Finally, I measured the sqrt function to cost 197 gas in a sweep where x is in [1e-6, 1e6], with bit-exact results when sqrt(x) < 1, and max relative error below 2e-18 when sqrt(x) >= 1. My conclusion after spending a full week on implementing it is that simplicity wins by a wide margin in Solidity if the goal is gas-optimized primitives such as the square root function.
Try it
npm install defimath-lib
This sqrt is one of dozens of primitives in DeFiMath, a gas-optimized fixed-point math library for Solidity — exp, ln, pow, cbrt, the standard normal CDF, and a full Black-Scholes options suite, each built with the same obsession over gas and precision. It's MIT-licensed, pure Solidity, with zero runtime dependencies. Source is on GitHub, with reference benchmarks against PRBMath, ABDK, and Solady in defimath-compare.