-
Notifications
You must be signed in to change notification settings - Fork 396
Do not use Double
arithmetics in Integer.parseInt()
.
#5193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
6d4a737
to
c46f3a5
Compare
I was wondering if you could take a peek and make any recommendations based on this work? https://github.com/scala-native/scala-native/blob/main/javalib/src/main/scala/java/lang/Integer.scala |
Those are fine implementations, but not as good as what's in this PR. In particular, caching the |
I'll review fully, but could you amend the description to include some information about why this is better? |
I added a paragraph in the PR description. I'll integrate it in the commit message next time I amend/rebase/etc. The short answer is "it's faster" ;) Edit: done |
c46f3a5
to
4f2dccf
Compare
val digit = Character.digitWithValidRadix(s.charAt(safeLen), radix) | ||
if (digit == -1 || (result ^ SignBit) > (radixInfo.overflowBarrier ^ SignBit)) | ||
fail() | ||
result = result * radix + digit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to check my understanding: we need both maxLength and overflowBarrier because up to here, result
could be 0, so we couldn't detect overflow as-we-go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
result
cannot be 0 here. We trim leading zeros beforehand. If safeLen != len
, it means there were maxLength
characters after the leading zeros. The main loop has executed exactly maxLength - 1
times, and during the first iteration digit
was at least 1. Therefore at this point result
is at least radix^(maxLength - 2)
.
The maxLength
check ahead of time is so that we can avoid the overflow checks during the main iteration. The overflow barrier is required for that last overflow check. We could write the algorithm without maxLength
at all, and check for overflow at every iteration instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. So if we were willing pay the overflow check on every iteration, we would not need maxLength
.
Do we have any idea of how this performance / code size / complexity trade-off looks like?
It feels to me like it's a lot of complexity to optimize something that'd we'd expect to be somewhat slow anyways.
private final class StringRadixInfo(val maxLength: Int, val overflowBarrier: Int) | ||
|
||
/** Precomputed table for parseIntInternal. */ | ||
private lazy val StringRadixInfos: Array[StringRadixInfo] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you compare this in code size and speed with two Array[Int]
s? Especially since lookup in overflowBarriers
would be rare, this might be worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code size is unfortunately not meaningfully different. The extra lazy val
and the extra loop to compute it compensate for the removed class StringRadixInfo
(which is actually very small). IMO the code is less clean if we split into two arrays (see diff), so I lean towards keeping the class StringRadixInfo
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized: Isn't it (relatively) easy to calculate the overflow barrier? So if we stick with the version that pre-calculates safeLen (which I'm not super convinced about TBH, see other comment), would it be a trade-off worth considering to calculate overflowBarrier
again if we actually need it?
I'm getting a bit the feeling that we are over-optimizing here :-/ w/o clear target use-cases for usage of parseInt, I feel it might be very difficult to chose the "right" trade off.
4f2dccf
to
f340b41
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code for the approach in this PR LGTM. However, now that I understand it better, I'm not sure it is worth the complexity to keep the lookup table.
Please let me know what your thoughts are. It might very well be that I'm missing something related to performance that is obvious to you.
Only use `Int` arithmetics. To detect overflow, we compute an overflow barrier in way that should typically be constant-folded. `Int` arithmetics are faster than `Double` arithmetics. The previous code used `Double`s to have a concise way of detecting the overflow, which is not bad on JS engines. However, we some careful analysis of the possible overflows, we can do better. We split the implementation of `parseInt` and `parseUnsignedInt`, since they have more differences than common parts at this point. Moreover, the justifications are quite different in each. The new algorithms are also much more Wasm-friendly.
f340b41
to
e9bfb21
Compare
So, after a day spent exploring many alternatives and benchmarking them both on JS and Wasm, eventually I found the best of all worlds: it's faster than before on both engines; it stays as small as it is today; and it doesn't use any lookup table. The only downside is that we have two different implementations for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
Only use
Int
arithmetics. To detect overflow, we precompute a table of the maximum string length for each radix.Int
arithmetics is faster thanDouble
arithmetics. The previous code usedDouble
s to have a concise way of detecting the overflow, which is not bad on JS engines. Given a cached overflow detection mechanism, resorting toDouble
s is not necessary anymore.