What is a 'variable initialization expression'?

While profiling some code recently, I've been seeing some of these 'variable initialization expression' things, and I'm not sure what they're for:

Here's the struct these variables are part of. As you can see, it's generic for a given collection type, and the variables listed here are all Range<T.Index>?. There are inlinable, member-wise initializers, and that's the only way an instance of this struct is created.

Expanding these 'variable initialization expression's, each one is full of runtime calls (despite, as the screenshot above says, this being part of a specialized static method call). I see calls to __swift_instantiateGenericMetadata, swift_storeEnumTagSinglePayloadGeneric, swift::storeEnumTagSinglePayloadImpl, swift_getAssociatedTypeWitness, swift_getAssociatedConformanceWitness, type metadata accessor for Range, etc.

What's up with all of this? I'm initializing these values to nil. Why do I have to go through all of this runtime stuff? I'm using a nightly toolchain from 09/09, so it's a relatively up-to-date compiler. I swear this didn't happen on the ~3-month-old nightly I was previously using.

1 Like

You have stumbled across SR-11777. The short summary of what happens there can be explained by the fact that this compiles:

public class Foo {
    var x: Int?

    init() { }
}

and this does not:

public class Bar {
    var x: Optional<Int>

    init() { }
}

When you use the ? shorthand for optionals in stored properties, the compiler implicitly treats that definition as though you wrote var whatever: Optional<WhateverElse> = nil. That is, it inserts a variable initialization expression. These variable initialization expressions are not inlinable and cannot be marked inlinable (we filed this as SR-11768), and even if the init is inlinable and chooses an initialisation value they seem not to get optimized out. This can be a really nasty performance problem in generic code.

NIO resolved this internally (at least in our hot paths) by changing our spelling to Optional<Whatever>. The the broader issue in SR-11768 has not been fixed, but there is a workaround in the ticket.

12 Likes

Oh wow, thanks @lukasa!

That's really subtle, but it has a huge performance impact! :flushed:

1 Like

Implicitly initialized T? really is the gift that keeps on giving, isn't it?

3 Likes

Y’know, even if we can’t make variable initializers inlineable in general, we could still do it for implicit nil in a non-library-evolution module. Heck, we could safely extend that to several other special cases, such as Int, Bool, and String vars initialized by literals. It’s not a 100% fix but I suspect it’s a 70/30 situation, especially for this one case of implicit initialization.

7 Likes

Wow, I had no idea that Optional<T> and T? have a semantic difference!

2 Likes

The thing that I find strange is that these values are always explicitly initialised. There is a memberwise initialiser (with no default arguments), and a plain init() which explicitly says these fields should be nil.

I learned the hard way that default arguments are not inlined/specialised (at least some of the time; I don't know the exact rules so I just avoid them everywhere), so I intentionally added that plain init() with explicit values. But it seems there's another level of defaults even under the memberwise initialiser :exploding_head:

That's the thing that's messed up, IMO - a memberwise initialiser should be the lowest-level initialiser.

There are two ways to implement struct field initialization: as a default, to be ignored whenever the field is explicitly initialized in a constructor, and as an initial value which constructors can then replace. C++ uses the former, but it can do that because it has different syntax for initialization and assignment. Swift does not: it uses = for both. And since field initialization can have side effects, when it happens is important; since it can involve private functions or types, it can’t automatically be inlinable (today; hopefully that’s a limitation that can be lifted for non-library-evolution modules in the future).

None of this should matter within a module, so I’m confused how the memberwise initializer plays into this. Though maybe the optimizer isn’t locally inlining as much as it could.

I’d just like to reference the acceptance notes for SE–0242:

5 Likes

Does the "double initialization" also happen if you use default values in the initializer's parameter list?

public class Bar { 
  var x: Optional<Int>

 init(x: Optional<Int> = .none) {
   self.x = x
 }

 // Or

 init(x: Int? = nil) { 
   self.x = x
 }
}

Not as written, no.

I feel like avoiding default arguments is not even remotely a worthwhile tradeoff, except as an aggressive performance optimization internally. How does avoiding them compare to overloads, anyway? My impression has always been that overloads are difficult to optimize.

Neither overloads nor default arguments are hard to optimize, since they are both resolved before the optimizer even runs. Default arguments may have a slight edge since the default is always considered inlinable and an overload might not be, but that’s almost certainly negligible if the main body of the function is not inlinable.