Static initialisation of classes


(Joanna Carter) #1

I found myself with the need to be able to register a class with various factories, etc., earlier than creating an instance of that class.

Objective-C gave us the +initialise() method but that is only available in Swift if you derive from NSObject; something I don't want to impose as a restriction.

So, I played around a bit and came up with the following, which also relies on a base class but does allow for one-time "static" initialisation on the first call to the "normal" default init()

It's possibly not perfect but I submit it for your comments and criticism

public protocol StaticInitialisable
{
  static var staticInit: (() -> ())? { get }
}

open class Object
{
  private static var initialisedTypes = [StaticInitialisable.Type]()
  
  public required init()
  {
    if let staticInitialisableType = type(of: self) as? StaticInitialisable.Type,
       !Object.self.initialisedTypes.contains { $0 == staticInitialisableType },
       let staticInit = staticInitialisableType.staticInit
    {
      Object.self.initialisedTypes.append(staticInitialisableType)
        
      staticInit()
    }
  }
}

This then allows me to do:

public class Person : Object, StaticInitialisable
{
  public static let staticInit: (() -> ())? =
  {
    // static initialisation code
  }
}

(Joe Groff) #2

This looks like a reasonable stopgap. You might not need to make the init required, even, because any subclass initializer will have to eventually super.init-call your base class's initializer already. We've discussed having proper support for enumeration of types in the runtime, possibly by providing an API for asking for all types conforming to a protocol or using a user defined attribute; would a feature like that cover your use case?

As an aside, although you said you don't want to rely on NSObject inheritance anyway, for anyone else reading this thread, note that +initialize is not generally guaranteed to be invoked even for Swift subclasses of NSObject.


(Joanna Carter) #3

In actual fact, the required is an artefact of the Copyable implementation, not shown in this extract of the Object class.

I'm not sure about the usefulness of being able to enumerate protocol conforming types, but user defined attributes would certainly come in useful for my frameworks.

Although I don't think either would really work for things like this idea of registering functionality related to the type, written in a static method like I demonstrated.

What would be very useful in this case would be the "reinstatement" of a static initialiser because, although I can call the staticInit() from the init method, that would not work if I had static functionality other than the init() that needed to reference the stuff set up in the static initialiser before the init() was called.

Unless you know of a different way around it :grinning:


(Thomas Roughton) #4

I've had need to do this as well, except with structs rather than objects (so this particular workaround doesn't work).

That exact feature (get a list of all types conforming to a protocol) would be very useful – I've got a use-case where I was considering trying to read the runtime metadata to do exactly that (although I decided that manually inserting methods would be a better approach). Effectively, what I currently have is:

public func initialiseComponents() {
    ComponentDataManager.moduleComponentDataManagers.append(ThisModuleComponentDataManager.self)
}

which needs to be manually called for each module during main/AppDelegate; having that be automated would be a nice way to reduce the chance of programmer error.

I think it might be a useful feature to support non-lazy static stored properties (eager maybe)? That way you could have a property of type Void as a way to support this sort of thing, and you can also have properties that are guaranteed to be initialised, removing the need for a check every time the property is accessed.


(Hooman Mehr) #5

Let me jump in to say: Yes, please! It would be extremely helpful for me.


(Joanna Carter) #6

Before getting sidetracked onto other things, can I just ask:

  1. why Swift doesn't have a static initialiser?
  2. when can we have it?

(Joe Groff) #7

Probably never. Static initialization is a blunt instrument with a lot of poor systemic side effects. One problem with "runs before everything else" static initializers is that multiple libraries want their initializers to run before everyone else's, setting up brittle and hard-to-debug order dependencies. Furthermore, static initializers negatively impact startup time. In the specific case of class object initializers in the mold of ObjC's +initialize, supporting these would also have a secondary performance impact by inhibiting optimizations, since it would mean that potentially every type metadata access has to be treated as having side effects.

The Swift runtime does nearly all initialization on demand, and we'd like to provide on-demand alternatives to the most common uses of static initialization. That's why I suggested the idea of providing a way to query all types conforming to a protocol or tagged with an attribute, since such a feature can address many of the same use cases as load-time registration, but the query can be answered on demand instead of with load-time overhead.


(Joanna Carter) #8

Ah! My experience of static initialisers in C# was that they were called only on the first "contact" with a type (I.e. on demand), not necessarily all at one go at start of execution.

(pseudo code)

{
  // first code in the app to talk to the FileManager
  let defaultFileManager = FileManager.default // this would trigger the static initialiser
  
  …
}

But I would be interested to see whether enumerating all conforming types, whether they were going to be referenced or not, is not going to also cause an unnecessary performance hit.


(Joe Groff) #9

I believe that's also the behavior of ObjC +initialize, and if you defined "first contact with the type" as meaning any access to the type, then it would introduce the semantic problem we're trying to avoid of type metadata access having side effects. One thing that might be reasonable is to say that static initialization happens implicitly as part of every designated initializer, as if:

class C {
  // Strawman syntax
  static init { doStaticInit() }

  init(a: Int) { doInstanceInit() }
  init(b: String) { doInstanceInit() }
}

implicitly injected a call to the static init into every initializer, as if you'd written:

class C {
  init(a: Int) { dispatch_once { doStaticInit() }; doInstanceInit() }
  init(b: String) { dispatch_once { doStaticInit() }; doInstanceInit() }
}

which is similar to what you'd coded by hand. This makes it so that the static initialization is a side effect of performing object initialization, not of accessing the class, which fits better into Swift's existing semantic model, since initializers can already have arbitrary side effects.


(Joanna Carter) #10

Yes, I can see exactly what you are saying. Hmmmm. If you would permit me to argue (possibly with myself)…

Microsoft says of their C# static constructors:

A static constructor is used to initialize any static data, or to perform a particular action that needs to be performed once only. It is called automatically before the first instance is created or any static members are referenced.

So, they call it on the first call to an instance initialiser or a static member.

Howabout, if the type metadata is accessed by type(of:_), then you are not calling static members of the type, you should then be accessing the metatype… But that is the type…

In checking the latest MS docs, it appears that, in .NET 4.5, they have separated out most of the reflection metadata into something called TypeInfo, which is apart from Type, apparently only needing a call to GetTypeInfo() on the Type.

Could this be an answer, in that all static members except GetTypeInfo() (or other spelling) trigger the static initialiser?

There's an interesting article on this here

Of course, I may still be talking through my hat but, you never know what can come out of a good mutter and murmur :smiley:


(Thomas Roughton) #11

The only issue I see with disallowing it altogether is the performance overhead of swift_once calls. Consider an example like this (Godbolt):

struct ExampleStruct {
    static var instance = ExampleStruct()

    var value = 3

    @inline(never)
    init() {}
}

@inline(never)
func someOtherFunction() {
    ExampleStruct.instance.value = 5
}

ExampleStruct.instance.value = 3
someOtherFunction()

The compiler is forced to insert swift_once calls defensively before every access, incurring a performance cost. This pattern actually shows up in some of my code; I use static global registries for some types in struct-of-arrays configurations, with the types just being handles into that registry. Ideally, accessing the data for one of those types should be a simple pointer indirection.

Is there any pattern which allows us to say "this static variable will be initialised at a fixed time and can be accessed unconditionally at any point after that"? Optionals with unsafelyUnwrapped gets there, but it's a little ugly to be using frequently.


(Joe Groff) #12

Hm, I would expect the optimizer to already promote a trivial initializer like this into a static initialization. It would be useful to have an annotation to force this, though. Are you seeing this overhead on Apple platforms? Since we inline the fast path check on those platforms, only calling the runtime from the uninitialized state, I would expect the overhead in the fast path to be tiny, just an easily-predicted branch.


(Joe Groff) #13

What you describe makes sense. Triggering initialization on static members would be a bit trickier in Swift than C#, because protocol extensions can add static members indirectly, so every static method that could possibly be invoked on a type with a static initializer would need to have an indirect "does the Self type have static inits" check. Keeping it limited to instance construction would be easier, because the set of designated initializers for a type is fixed by its original definition, so the number of places that need to trigger static initialization is fixed and can be statically resolved by the compiler.


(Joanna Carter) #14

For my particular use case, invoking at instance construction would be fine. I was just extending the possibility for other cases.


(Thomas Roughton) #15

I had to insert the @inline(never) on the init in this particular case to make it not be statically initialised; in real-world cases the initialiser usually allocates a bunch of memory, which isn't so easily inlined.

On macOS the overhead doesn't show up in the profiler, although that's probably because the implementation's inlined. On Windows, however, and particularly in debug builds, there is a small but measurable overhead.

Having done some more performance tests on macOS it looks like Optional.unsafelyUnwrapped is consistently around 0.3-0.4% quicker than the swift_once path in -Ounchecked builds. That's certainly far from a major performance issue, but it would be nice if there were a way to close the gap in performance between the two methods. With that said, after doing these tests I'm a lot less convinced it's an issue worth pursuing.

Performance Details

Test:

struct ExampleStruct {
    static var instance = ExampleStruct()
    static var instance2 : ExampleStruct! = nil
    
    var data : UnsafeMutablePointer<Int>
    
    @inline(never)
    init() {
        self.data = UnsafeMutablePointer.allocate(capacity: 8)
    }
}

ExampleStruct.instance2 = ExampleStruct()

@inline(never)
func testSwiftOnce() {
    let index = Int.random(in: 0..<8)
    ExampleStruct.instance.data[index] = ExampleStruct.instance.data[index] &+ 1
}

@inline(never)
func testUnsafelyUnwrapped() {
    let index = Int.random(in: 0..<8)
    ExampleStruct.instance2.unsafelyUnwrapped.data[index] = ExampleStruct.instance2.unsafelyUnwrapped.data[index] &+ 1
}

let iterations = 100_000_000

var startTime = DispatchTime.now().uptimeNanoseconds
for _ in 0..<iterations {
    testSwiftOnce()
}

var elapsed = DispatchTime.now().uptimeNanoseconds - startTime
print("swift_once: \(Double(elapsed)*1e-6)ms")

startTime = DispatchTime.now().uptimeNanoseconds
for _ in 0..<iterations {
    testUnsafelyUnwrapped()
}

elapsed = DispatchTime.now().uptimeNanoseconds - startTime
print("unsafelyUnwrapped: \(Double(elapsed)*1e-6)ms")

-Ounchecked:

swift_once, @inline(never): 17227.539284ms
unsafelyUnwrapped, @inline(never): 17146.381315ms

swift_once: 17338.139272ms
unsafelyUnwrapped: 17268.130556ms

-O:

swift_once, @inline(never): 17234.522449ms
unsafelyUnwrapped, @inline(never): 17196.227179999998ms

swift_once: 17338.519422999998ms
unsafelyUnwrapped: 17326.178461ms