Updated Embedded Swift vision

Hi all,

We revised the Embedded Swift vision to capture the philosophical shift that's been happening over the last year or so, toward allowing more Swift features in Embedded Swift while keeping the implementation model predictable. Please have a look if you're interested!

Cheers,
Doug

17 Likes

Taking a look at this revised document now, I noticed that the original 2023 version long noted that one of the key goals was to enable dead stripping, which is something we've been discussing applying for regular Desktop Swift also this year. In that thread, @rauhul notes that "Desktop swift today isn't able to dead strip as effectively as embedded due to things like reflection metadata. I'll avoid speaking more concretely as I don't understand the full details."

Since I don't know the technical details of how that works either but would like to try enabling it, does that mean we will never get much out of such Dead Code Elimination (DCE) in non-Embedded Swift? Any info on the feasibility of applying the DCE techniques in that linked thread for Desktop Swift too would be helpful, with the understanding that it would depend on how much your code and dependencies use features like unspecialized generics and so on.

4 Likes

The standard library’s print() function will be more limited, because it depends on reflection for some functionality.

That makes sense in production, but I think we should be able to relax this restriction in debug mode. For instance, most Swift Wasm apps rely on Embedded Swift to ship binaries between 500-3000 KiB, but when testing a web app locally, a 40 MiB binary with debug metadata loads just as fast. To be clear, I think we should support an Embedded-Debug mode with the same compile-time restrictions as regular embedded, but which spits out a larger binary with type metadata for regular print, debugPrint, etc.

protocol P {
  func f()
  func g<T>(_: T)
}

extension Int: P { ... }

let p: Any = 42
p.f() // okay
p.g(1) // not okay: requires unspecialized generics

Was the type of p supposed to be any P?

Non-allocating Embedded Swift will identify places where the implementation requires a heap allocation. This includes: [...]

What about instead of banning allocation, we make allocation explicit, similar to Zig? I know this would require more language changes, but even in Embedded Swift, developers might have an efficient arena allocator where they want to allocate indirect enums, initialize classes, or create arrays. For instance:

indirect enum Expr {
    case literal(Int)
    case add(Expr, Expr)
    case multiply(Expr, Expr)
}

func allocatingFunction(explicitAllocator: MyArenaAllocator) {
  // Strawman syntax
  let expr = Expr<MyArenaAllocator>(allocator: explicitAllocator).literal(5)
}

I know the syntax I'm proposing is terrible, but my point is that there's value in explicit, custom allocators -- even for non-Embedded Swift. So, if Swift gains support for custom allocators, non-allocating mode would simply become explicit-allocation mode.


P.S. Very exciting to see Embedded Swift grow!

1 Like

FWIW, I've wanted this before for non-Embedded Swift as well. If your types are all structs/enums, you can partially achieve this today with unsafe operations because you can allocate your own memory and initialize those values types into it—you just have to do your own manual cleanup. But if you need a class (for ref-counting, or for isKnownUniquelyReferenced), then you're out of luck because even though the Swift runtime distinguishes between class allocation and initialization, the language surface doesn't. SomeClass() is always an allocating init.

4 Likes

If we dropped comprehensive type metadata from non-Embedded Swift, it would uncover more opportunities for dead code elimination. It would map to some feature degradation, though:

  • If you drop protocol conformances that aren't used statically, it lets you drop all of the code that's reachable from a protocol conformance. What you lose is the ability to take any type (e.g., an Any) and dynamically determine whether it conforms to a protocol by, e.g., casting it to any P.
  • If you drop type metadata for types that aren't used statically, you lose the ability to look up types by name. That's not widely-used functionality, but it does exist.

It's possible that we can change some defaults here on the desktop side.

Doug

2 Likes

@Douglas_Gregor Would it be feasible to introduce this as an opt-in compiler flag (e.g., -disable-runtime-metadata) for desktop builds first? That way, CLI tools or binaries that don't need dynamic reflection can benefit from dead stripping without breaking existing desktop apps.

2 Likes

The problem here is that you don't want the metadata format to differ between debug and non-debug, or you lose the option of mixing and matching. It's also harder for tools like the debugger to figure out which metadata it is.

I haven't thought about this enough to have a sense of how something like this can fit into Swift.

Doug

3 Likes

@Douglas_Gregor Would it be feasible to introduce this as an opt-in compiler flag (e.g., -disable-runtime-metadata) for desktop builds first? That way, CLI tools or binaries that don't need dynamic reflection can benefit from dead stripping without breaking existing desktop apps.

Maybe. Metadata isn't just one thing, it's a couple of things---reflective access to the structure of a type (mirrors, print), reflective access to the type itself (mangled name -> type metadata), and dynamic lookup of protocol conformances (e.g., as? any P). I suspect that the big win is from the last one, which is also the most semantics-breaking. That doesn't mean we can't find a way forward for it, or experiment with the idea to see how much of a win might be available.

Doug

1 Like

And even this one is incredibly load bearing now. Previously, one could easily say that they don't care about Mirror or the default debug representation of print for arbitrary values and disable it without much lost. But now SwiftUI uses reflection metadata to inspect views, and to a lesser extent swift-argument-parser uses it to find flags in command implementations, and the compiler currently only has flags that let it be disabled at the module level.

Right. There's a commonality here that we might exploit (and that others have pointed out before!), where each of these is rooted at some sort of protocol that could convey "hey, I want that metadata!". Maybe there are different flavors of this: SwiftUI and swift-argument-parser are using the mirrors metadata, whereas other libraries might depend on as any? P.

Doug

1 Like