Skip to content

Avoid fmod when rounding layout results to the pixel grid - #2041

Merged
intergalacticspacehighway merged 1 commit into
react:mainfrom
tomekzaw:round-pixel-grid-without-fmod
Oct 16, 2026
Merged

intergalacticspacehighway merged 1 commit into
react:mainfrom
tomekzaw:round-pixel-grid-without-fmod

Conversation

@tomekzaw

@tomekzaw tomekzaw commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

roundValueToPixelGridDouble runs six times per node on every layout pass, and each call computes fmod(scaledValue, 1.0). On Android, bionic's fmod is a slow software loop: about 90 ns per call on a Cortex-A76 and 130 ns on an A55 (Galaxy A16). That made fmod the 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 to fmod(x, 1.0) for every double. It differs from x - trunc(x) only in keeping the -0.0 that fmod returns for negative integers.

Metrics

Galaxy A16 (Android 16), React Native 0.86 release build of a production app, 3 runs per variant:

fmod this change
Micro-benchmark, A76 / A55 90 / 130 ns per call 1.1 / 7.4 ns per call
Full-tree rounding pass, ~4,000 nodes 2.53 ms 1.37 ms (−46%)
Rounding pass with the non-owned-subtree skip (#2015), React commit while scrolling (~670 nodes) 0.38 ms 0.21 ms (−45%)

With #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) fmod is 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.
  • Exhaustive-style equivalence check on macOS arm64 and on the device (bionic): fmod(x, 1.0) vs copysign(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 --Werror on the changed file.

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>
@meta-cla meta-cla Bot added the CLA Signed label Oct 15, 2026
@tomekzaw
tomekzaw marked this pull request as ready for review October 15, 2026 17:31
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 15, 2026

@intergalacticspacehighway intergalacticspacehighway left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@intergalacticspacehighway
intergalacticspacehighway merged commit 52f6d0b into react:main Oct 16, 2026
33 checks passed
@tomekzaw
tomekzaw deleted the round-pixel-grid-without-fmod branch October 16, 2026 17:31
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants