Skip to content

Conversation

@sjrd
Copy link
Member

@sjrd sjrd commented Mar 26, 2025

This avoids the JS call overhead for that "primitive" operation.

As can be seen in the implementation, fmod is far from a primitive, though. It requires an involved algorithm in software.

We took an MIT implemention of fmod written in Rust, generic in the bit-width, and translated it to Scala. The "generic" aspect is turned into a big copy-pasted blob between the Float and Double versions, with some "configuration" at the top.

We put that implementation in the linker private library, in a new object WasmRuntime. The Wasm backend compiles Float_% and Double_% as calls to those runtime functions.


(Probably better) alternative to #5147. At least we read Scala code and not Wasm code. It's not a lot less code in total, but there are 175 lines that are a brutal copy-paste, so those duplicate 175 lines don't need to be reviewed.

In our IR, the rhs of Long shifts is an Int, but in Wasm it must be
a Long as well. In the common-case where the rhs is a constant, we
now constant-fold the extension to 64 bits.
@sjrd sjrd requested a review from gzm0 March 26, 2025 12:51
Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

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

Nits and suggestions. Barring major roadblocks for using raw double to long bits, this can be merged IMO.

test(Double.NaN, Double.NaN, 5.5)
test(Double.NaN, Double.NaN, -151.189)
test(Double.NaN, Double.NaN, 3.123e-320)
test(Double.NaN, Double.NaN, 2.253547e-318)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add an explanation to the commit message what these additional values are?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. They're subnormals.

var iy: IType = toBits(y)

// let sx = ix & F::SIGN_MASK;
val sx: IType = ix & SignBit
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Move below ex / ey for consistent ordering with original code?

Copy link
Member Author

Choose a reason for hiding this comment

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

We have to do it here because we're masking off the sign bits. I added a comment.

var ey: Int = wrapToInt(iy >>> mbits)

/* if iy << 1 == zero || y.is_nan() || ex == F::EXP_SAT as i32 {
* return (x * y) / (x * y); -> sent to after the block
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not understand what "sent to after the block" means here. Obsolete?

Copy link
Member Author

Choose a reason for hiding this comment

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

Obsolete, indeed.

*
* I believe the first one is just wrong, and fails to correctly handle
* subnormal values. Removing that one causes the tests for subnormals to
* fail.
Copy link
Contributor

Choose a reason for hiding this comment

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

I was going to suggest contributing upstream, but it seems there are not unit tests for fmod at all upstream :-/ (or I couldn't find them...)

Copy link
Member Author

Choose a reason for hiding this comment

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

I "contributed" a review upstream: rust-lang/libm#469 (review)
So far the author agrees with my analysis, although they haven't had a chance to integrate my suggestion yet.

Choose a reason for hiding this comment

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

Thank you for the consideration! I'm looking into this at rust-lang/libm#535.

@inline def extendFromInt(v: Int): IType = v.toLong
@inline def wrapToInt(v: IType): Int = v.toInt

// --- BEGIN COPY-PASTE wrt. fmodf ----------------------------------------
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a comment that copy-pasting here is better than an @inline helper because of number operations? (<<, etc.)?

val iZero = 0L
val iOne = 1L

@inline def toBits(v: FType): IType = java.lang.Double.doubleToLongBits(v) // TODO use raw variant
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have a strategy to solve this? Feels like the answer to that is relevant to decide between this PR and #5147?

Copy link
Member Author

@sjrd sjrd Apr 6, 2025

Choose a reason for hiding this comment

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

Using the non-raw variant is correct. Using the raw variant would only be more efficient. (I expanded the comment.)

This avoids the JS call overhead for that "primitive" operation.

As can be seen in the implementation, `fmod` is far from a
primitive, though. It requires an involved algorithm in software.

We took an MIT implemention of `fmod` written in Rust, generic
in the bit-width, and translated it to Scala. The "generic"
aspect is turned into a big copy-pasted blob between the `Float`
and `Double` versions, with some "configuration" at the top.

We put that implementation in the linker private library, in a
new object `WasmRuntime`. The Wasm backend compiles `Float_%` and
`Double_%` as calls to those runtime functions.

Since our new implementation is sensitive to the normal/subnormal
distinction, we add new tests for subnormal values.
@sjrd sjrd force-pushed the wasm-fmod-in-scala branch from 146360c to b5e2700 Compare April 6, 2025 10:30
@sjrd sjrd merged commit 95d04c6 into scala-js:main Apr 6, 2025
3 checks passed
@sjrd sjrd deleted the wasm-fmod-in-scala branch April 6, 2025 22:00
tanishiking added a commit to scala-wasm/scala-wasm that referenced this pull request Apr 8, 2025
tanishiking added a commit to scala-wasm/scala-wasm that referenced this pull request Apr 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants