Skip to content

Reimplement floating-point description implementation in Swift. #82750

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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

tbkka
Copy link
Contributor

@tbkka tbkka commented Jul 2, 2025

This replaces the previous SwiftDtoa.cpp with the same algorithm reimplemented in Swift. It supports Float16, Float32, Float64, and Float80 (on Intel).

Performance is reasonable: In my testing (M1 and x86_64): Float16 and Float32 are a bit faster than the C version, Float64 is almost exactly the same, Float80 is a bit slower.

I think I've finally worked out the availability, though I'd appreciate someone who knows better taking a critical look.

@tbkka
Copy link
Contributor Author

tbkka commented Jul 2, 2025

CC: @stephentyrone for visibility. (I'll eventually need advice about how to deal with the availability mess here.)

@tbkka tbkka force-pushed the tbkka-swift-floatingpointtostring branch from 295114c to 28ca1f0 Compare July 29, 2025 19:50
// ================================================================

// Support Legacy ABI on top of new implementation
@_silgen_name("swift_float32ToString2")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stephentyrone Do we really need to keep these legacy ABIs? The old implementation went to some effort to preserve them, but I'm not sure why.

value d: Float64,
buffer utf8Buffer: inout MutableSpan<UTF8.CodeUnit>) -> Range<Int>
{
if #available(macOS 9999, *) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@al45tair What's the right way to write this dance? It is here only to try to safely call the other functions in this same file. But as written here, it ends up doing a runtime availability check, which I would rather avoid as it's entirely pointless.

Copy link
Contributor

Choose a reason for hiding this comment

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

You could go with if #available(StdlibDeploymentTarget 6.0) here and annotate the things it uses with @available(StdlibDeploymentTarget 6.0). That should avoid any runtime tests (everywhere, I think, except possibly if one of this is inlineable and we're back-deploying). I picked 6.0 because that corresponds to macOS 15, and I think there are some existing annotations in the file suggesting that version?

@rauhul rauhul moved this to In Progress in Embedded Swift Aug 1, 2025
@tbkka tbkka force-pushed the tbkka-swift-floatingpointtostring branch from 28ca1f0 to 5bfe1c1 Compare August 1, 2025 22:04
@tbkka tbkka marked this pull request as ready for review August 1, 2025 23:13
value f: Float16,
buffer utf8Buffer: inout MutableSpan<UTF8.CodeUnit>) -> Range<Int>
{
if #available(SwiftStdlib 6.2, *) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does this necessarily do a runtime OS version check? How does one avoid that?

In effect, we are re-implementing an old API using new ones. The new ones are always "available" in the sense that they're always compiled -- the availability on the new APIs is only there to inform clients of their back-deployment options. (Our availability notions seem to mistakenly conflate "when something was introduced" with "what requirements it has".)

Copy link
Contributor

Choose a reason for hiding this comment

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

It won't do a runtime version check if the target that we're building for is high enough — it doesn't always do it.

Entirely coincidentally, I was thinking this morning (before I'd seen this comment) that we really should have both @available() and @requires() and that we were really conflating exactly those two notions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps we should add @introduced() and @requires() and document that @available() implies both of the others? That would give a migration path.

Copy link
Member

@compnerd compnerd left a comment

Choose a reason for hiding this comment

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

This seems to be missing the addition of the new file to swiftCore in the new runtimes build.

@tbkka
Copy link
Contributor Author

tbkka commented Aug 4, 2025

This seems to be missing the addition of the new file to swiftCore in the new runtimes build.

How do I fix that? I'm not familiar with "the new runtimes build."

@etcwilde
Copy link
Member

etcwilde commented Aug 4, 2025

How do I fix that? I'm not familiar with "the new runtimes build."

Add FloatingPointToString.swift to Runtimes/Core/core/CMakeLists.txt

FloatingPoint.swift

@tbkka tbkka force-pushed the tbkka-swift-floatingpointtostring branch from 5b20b0b to cac5983 Compare August 4, 2025 19:08
@tbkka
Copy link
Contributor Author

tbkka commented Aug 4, 2025

@swift-ci Please test

1 similar comment
@tbkka
Copy link
Contributor Author

tbkka commented Aug 4, 2025

@swift-ci Please test

@tbkka
Copy link
Contributor Author

tbkka commented Aug 4, 2025

@etcwilde Now we're failing in CI for AVR. Clearly, a 16-bit platform does not want this file. How do I exclude it from those?

@tbkka
Copy link
Contributor Author

tbkka commented Aug 5, 2025

@swift-ci Please test

@tbkka
Copy link
Contributor Author

tbkka commented Aug 5, 2025

I tweaked one place where 32-bit int was assumed but not specified. This may let it build on AVR.

@tbkka
Copy link
Contributor Author

tbkka commented Aug 6, 2025

32-bit WASM tests revealed a problem with Float32 formatting on 32-bit CPUs. There also seems to be some problem with Float64 and NaNs that I'm trying to track down.

@tbkka
Copy link
Contributor Author

tbkka commented Aug 6, 2025

@swift-ci Please test Linux

1 similar comment
@tbkka
Copy link
Contributor Author

tbkka commented Aug 11, 2025

@swift-ci Please test Linux

@tbkka tbkka force-pushed the tbkka-swift-floatingpointtostring branch from eefe4ff to deccd0a Compare August 13, 2025 14:05
@tbkka
Copy link
Contributor Author

tbkka commented Aug 13, 2025

@swift-ci Please test

@tbkka
Copy link
Contributor Author

tbkka commented Aug 13, 2025

@natecook1000 I'm toying with the idea of restructuring this a bit. Basically, making the new source file directly implement extension Float# : CustomDebugStringConvertible without the layers of indirection. Any thoughts?

return unsafe buffer.withBytes { (bufferPtr) in
unsafe String._fromASCII(
UnsafeBufferPointer(start: bufferPtr, count: length))
if #available(SwiftStdlib 6.2, *) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tshortli @al45tair -- In my local testing, it appears that this check results in a call to __isPlatformVersionAtLeast which takes a full 1/3 of the run time for debugDescription. This shouldn't be necessary at all -- the implementation here has no dependencies outside of the standard library that we're part of. How can we eliminate this?

Copy link
Member

Choose a reason for hiding this comment

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

Do you get a different outcome if you use _unreachable() instead of fatalError()?

Copy link
Contributor

Choose a reason for hiding this comment

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

Local stdlib builds set the deployment target to something way too old, see 144244211

Copy link
Contributor

Choose a reason for hiding this comment

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

You could also use StdlibDeploymentTarget 6.2 here (but as before, you might end up having to move more things to StdlibDeploymentTarget 6.2 as a result). That will get rid of the check, because when you build locally, you'll be testing for the target you're building for.

That would let you test the performance sensibly here, I think.

Copy link
Contributor

@mikeash mikeash Aug 19, 2025

Choose a reason for hiding this comment

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

You can override the default deployment target when building locally by passing something like --darwin-deployment-version-osx 26 to build-script, if you just want to look at performance locally.

Copy link
Contributor

Choose a reason for hiding this comment

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

build-script should really do this by default, because the alternative is even less representative of the way the stdlib actually gets built.

Copy link
Contributor

Choose a reason for hiding this comment

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

Preach.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You could also use StdlibDeploymentTarget 6.2 here (but as before, you might end up having to move more things to StdlibDeploymentTarget 6.2 as a result).

Nope. That doesn't work for InlineArray, at least not with a stock build-script -rA invocation, since "value generics are only available on macOS 26." I discussed this in #83710. If I landed that, then I could duplicate InlineArray to make internal _InlineArray which would not need any availability restrictions at all. Using the same pattern to create internal _MutableSpan would probably just eliminate all of the availability checks in this code.

I've not experimented with _unreachable() yet.

I'm playing with build-script incantations to see what other options exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... something like --darwin-deployment-version-osx 26 to build-script, if you just want to look at performance locally

Sure, but that still misses the point: Nothing in this code requires macOS 26. Everything it needs is in libswiftCore. The runtime host version is irrelevant.

Copy link
Contributor

Choose a reason for hiding this comment

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

It would let you look at performance locally without the version checks getting in the way. It doesn’t solve the larger issue, of course.

unsafe String._fromASCII(
UnsafeBufferPointer(start: bufferPtr, count: length))
if #available(SwiftStdlib 6.2, *) {
var buffer = InlineArray<64, UTF8.CodeUnit>(repeating: 0)
Copy link
Member

Choose a reason for hiding this comment

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

Any reason not to initialize this to 0x30 instead of zero?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A-ha! Nice catch indeed! Yes, that would eliminate some work later on.

For example, this avoids the need to do any explicit byte-by-byte
writes when expanding "123" out to "123000000.0".

This also required reworking the "back out extra digits" process
for Float64 to ensure the unused digits get written as '0' characters
instead of null bytes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

9 participants