Repository navigation
Avoid fmod when rounding layout results to the pixel grid - #2041
Merged
intergalacticspacehighway merged 1 commit intoOct 16, 2026
Merged
Conversation
copysign(x - trunc(x), x) is bit-identical to fmod(x, 1.0) for every double but avoids the libm fmod loop, which is slow on Android's bionic. roundValueToPixelGridDouble runs six times per node on every layout pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tomekzaw
marked this pull request as ready for review
October 15, 2026 17:31
intergalacticspacehighway
approved these changes
Oct 15, 2026
intergalacticspacehighway
left a comment
Collaborator
There was a problem hiding this comment.
Thanks!
hsource
added a commit
to wanderlog/react-native
that referenced
this pull request
Oct 21, 2026
…#2041) Backport of upstream Yoga commit 52f6d0bd38 (RN bf8d8c9): react/yoga#2041 This commit can be dropped when upgrading to React Native 0.89 or later. As of 2026-10-20, the change is on RN main but in no release, and is not on 0.88-stable. 0.89 is the expected first release, unless it is cherry-picked into a 0.88 patch release. roundValueToPixelGrid runs six times per node on every layout pass, and each call computed fmod(scaledValue, 1.0). Bionic's fmod is a slow software loop. This replaces it with copysign(x - trunc(x), x), which gives the same bits as fmod(x, 1.0). x - trunc(x) is always exact in IEEE doubles. copysign keeps the -0.0 that fmod returns for negative whole numbers and -0.0. Before applying it, we fuzz-tested the replacement on 70M doubles against fmod on our machine, with zero mismatches. In our Pixel 6a profile (see the previous commit), rounding plus fmod took ~17% of JS-thread samples while scrolling. With this commit and the previous one, self time in rounding and fmod fell from 8,797 to 280 samples over a 20s scroll (-97%). The rebuilt libreactnative.so has 1 fmod call site, versus 11 before. This fork predates upstream's rename of `fractial` to `fractional`, so the local name is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hsource
added a commit
to wanderlog/react-native
that referenced
this pull request
Oct 21, 2026
…#2041) Backport of upstream Yoga commit 52f6d0bd38 (RN bf8d8c9): react/yoga#2041 This commit can be dropped when upgrading to React Native 0.89 or later. As of 2026-10-20, the change is on RN main but in no release, and is not on 0.88-stable. 0.89 is the expected first release, unless it is cherry-picked into a 0.88 patch release. roundValueToPixelGrid runs six times per node on every layout pass, and each call computed fmod(scaledValue, 1.0). Bionic's fmod is a slow software loop. This replaces it with copysign(x - trunc(x), x), which gives the same bits as fmod(x, 1.0). x - trunc(x) is always exact in IEEE doubles. copysign keeps the -0.0 that fmod returns for negative whole numbers and -0.0. Before applying it, we fuzz-tested the replacement on 70M doubles against fmod on our machine, with zero mismatches. In our Pixel 6a profile (see the previous commit), rounding plus fmod took ~17% of JS-thread samples while scrolling. With this commit and the previous one, self time in rounding and fmod fell from 8,797 to 280 samples over a 20s scroll (-97%). The rebuilt libreactnative.so has 1 fmod call site, versus 11 before. This fork predates upstream's rename of `fractial` to `fractional`, so the local name is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hsource
added a commit
to wanderlog/react-native
that referenced
this pull request
Oct 21, 2026
…#2041) Backport of upstream Yoga commit 52f6d0bd38 (RN bf8d8c9): react/yoga#2041 This commit can be dropped when upgrading to React Native 0.89 or later. As of 2026-10-20, the change is on RN main but in no release, and is not on 0.88-stable. 0.89 is the expected first release, unless it is cherry-picked into a 0.88 patch release. roundValueToPixelGrid runs six times per node on every layout pass, and each call computed fmod(scaledValue, 1.0). Bionic's fmod is a slow software loop. This replaces it with copysign(x - trunc(x), x), which gives the same bits as fmod(x, 1.0). x - trunc(x) is always exact in IEEE doubles. copysign keeps the -0.0 that fmod returns for negative whole numbers and -0.0. Before applying it, we fuzz-tested the replacement on 70M doubles against fmod on our machine, with zero mismatches. In our Pixel 6a profile (see the previous commit), rounding plus fmod took ~17% of JS-thread samples while scrolling. With this commit and the previous one, self time in rounding and fmod fell from 8,797 to 280 samples over a 20s scroll (-97%). The rebuilt libreactnative.so has 1 fmod call site, versus 11 before. This fork predates upstream's rename of `fractial` to `fractional`, so the local name is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
roundValueToPixelGridDoubleruns six times per node on every layout pass, and each call computesfmod(scaledValue, 1.0). On Android, bionic'sfmodis a slow software loop: about 90 ns per call on a Cortex-A76 and 130 ns on an A55 (Galaxy A16). That madefmodthe single hottest function in React Native layout on that device.This replaces it with
std::copysign(x - std::trunc(x), x), which is bit-identical tofmod(x, 1.0)for every double. It differs fromx - trunc(x)only in keeping the-0.0thatfmodreturns for negative integers.Metrics
Galaxy A16 (Android 16), React Native 0.86 release build of a production app, 3 runs per variant:
fmodWith #2015, most commits only round a few dozen nodes, so the absolute saving there is small: about 28 ms of rounding over a 20 s scroll. The full-pass number applies wherever whole subtrees are rounded, e.g. the first layout of a screen and newly mounted rows. It also applies to React Native versions that ship Yoga without #2015 (0.86 included), where every commit rounds the whole tree: there it was 5.5 s of rounding per 20 s scroll, and this change halves it.
On macOS (Apple libm)
fmodis already fast (1.2 ns vs 0.9 ns), so the gain is mostly an Android one.Test plan
./unit_tests Release: all 859 tests pass, before and after.fmod(x, 1.0)vscopysign(x - trunc(x), x)compared bit-for-bit (any NaN counted as equal) for ±0, ±1, ±Inf, NaN, subnormals, ±DBL_MAX, 2^52/2^53 boundaries, 100M random bit patterns and 200M values in the layout range. 0 mismatches.clang-format --dry-run --Werroron the changed file.