Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Swift optimise chained creation and copy of structs?

If I create a Struct with a function like...

struct SomeStruct {
    var name: String? = nil
    var number: Int = 0
    var date: Date? = nil
    //... many other properties

    func setting<Value>(_ keyPath: WritableKeyPath<SomeStruct, Value>, to value: Value) -> SomeStruct {
        var copy = self
        copy[keyPath: keyPath] = value
        return copy
    }
}

Does Swift do any optimisation on doing something like...

let myStruct = SomeStruct()
    .setting(\.name, to: "Fogmeister")
    .setting(\.number, to: 42)
    .setting(\.date, to: yesterday)
    .setting(\.otherProperty, to: value)
    ...etc
    ...etc

Because the setting function creates a copy and changes the copy each time you are essentially creating a new Struct over and over and over and then throwing away all but one of them.

Are there any overhead considerations to take into account when doing this or does Swift optimise away all these unused values at compile time?

like image 452
Fogmeister Avatar asked Jan 02 '23 15:01

Fogmeister


1 Answers

No, this isn't optimized. It will make a new copy for every call. It's hard to imagine how the optimizer would work this out to avoid the copies, but the wizards that write the optimizers have fooled me before. But as with most optimizer questions we don't have to guess what it does. We can look.

I slightly rewrote this code to get rid of the optionals (which just complicate things slightly without changing the question).

struct SomeStruct {
    var name: String = ""
    var number: Int = 0
    var date: String = ""

    func setting<Value>(_ keyPath: WritableKeyPath<SomeStruct, Value>, to value: Value) -> SomeStruct {
        var copy = self
        copy[keyPath: keyPath] = value
        return copy
    }
}

let myStruct = SomeStruct()
    .setting(\.name, to: "Fogmeister")
    .setting(\.number, to: 42)
    .setting(\.date, to: "yesterday")

And then compiled it to SIL with optimizations:

swiftc -O -emit-sil x.swift

The setting method becomes this:

// SomeStruct.setting<A>(_:to:)
sil hidden @$S1x10SomeStructV7setting_2toACs15WritableKeyPathCyACxG_xtlF : $@convention(method) <Value> (@guaranteed WritableKeyPath<SomeStruct, Value>, @in_guaranteed Value, @guaranteed SomeStruct) -> @owned SomeStruct {
// %0                                             // users: %26, %17, %18, %3
// %1                                             // users: %11, %4
// %2                                             // users: %8, %7, %9, %5
bb0(%0 : $WritableKeyPath<SomeStruct, Value>, %1 : $*Value, %2 : $SomeStruct):
  debug_value %0 : $WritableKeyPath<SomeStruct, Value>, let, name "keyPath", argno 1 // id: %3
  debug_value_addr %1 : $*Value, let, name "value", argno 2 // id: %4
  debug_value %2 : $SomeStruct, let, name "self", argno 3 // id: %5
  %6 = alloc_stack $SomeStruct, var, name "copy"  // users: %12, %28, %9, %29
  %7 = struct_extract %2 : $SomeStruct, #SomeStruct.name // user: %15
  %8 = struct_extract %2 : $SomeStruct, #SomeStruct.date // user: %16
  store %2 to %6 : $*SomeStruct                   // id: %9
  %10 = alloc_stack $Value                        // users: %27, %24, %11
  copy_addr %1 to [initialization] %10 : $*Value  // id: %11
  %12 = address_to_pointer %6 : $*SomeStruct to $Builtin.RawPointer // user: %13
  %13 = struct $UnsafeMutablePointer<SomeStruct> (%12 : $Builtin.RawPointer) // user: %18
  // function_ref _projectKeyPathWritable<A, B>(root:keyPath:)
  %14 = function_ref @$Ss23_projectKeyPathWritable4root03keyC0Spyq_G_yXlSgtSpyxG_s0dbC0Cyxq_Gtr0_lF : $@convention(thin) <τ_0_0, τ_0_1> (UnsafeMutablePointer<τ_0_0>, @guaranteed WritableKeyPath<τ_0_0, τ_0_1>) -> (UnsafeMutablePointer<τ_0_1>, @owned Optional<AnyObject>) // user: %18
  retain_value %7 : $String                       // id: %15
  retain_value %8 : $String                       // id: %16
  strong_retain %0 : $WritableKeyPath<SomeStruct, Value> // id: %17
  %18 = apply %14<SomeStruct, Value>(%13, %0) : $@convention(thin) <τ_0_0, τ_0_1> (UnsafeMutablePointer<τ_0_0>, @guaranteed WritableKeyPath<τ_0_0, τ_0_1>) -> (UnsafeMutablePointer<τ_0_1>, @owned Optional<AnyObject>) // users: %25, %19, %20
  %19 = tuple_extract %18 : $(UnsafeMutablePointer<Value>, Optional<AnyObject>), 0 // user: %21
  %20 = tuple_extract %18 : $(UnsafeMutablePointer<Value>, Optional<AnyObject>), 1 // user: %23
  %21 = struct_extract %19 : $UnsafeMutablePointer<Value>, #UnsafeMutablePointer._rawValue // user: %22
  %22 = pointer_to_address %21 : $Builtin.RawPointer to [strict] $*Value // user: %23
  %23 = mark_dependence %22 : $*Value on %20 : $Optional<AnyObject> // user: %24
  copy_addr [take] %10 to %23 : $*Value           // id: %24
  release_value %18 : $(UnsafeMutablePointer<Value>, Optional<AnyObject>) // id: %25
  strong_release %0 : $WritableKeyPath<SomeStruct, Value> // id: %26
  dealloc_stack %10 : $*Value                     // id: %27
  %28 = load %6 : $*SomeStruct                    // user: %30
  dealloc_stack %6 : $*SomeStruct                 // id: %29
  return %28 : $SomeStruct                        // id: %30
} // end sil function '$S1x10SomeStructV7setting_2toACs15WritableKeyPathCyACxG_xtlF'

Of particular interest is this section:

%6 = alloc_stack $SomeStruct, var, name "copy"  // users: %12, %28, %9, %29
%7 = struct_extract %2 : $SomeStruct, #SomeStruct.name // user: %15
%8 = struct_extract %2 : $SomeStruct, #SomeStruct.date // user: %16
store %2 to %6 : $*SomeStruct                   // id: %9

As expected, a new copy is created every time you call setting.

IMO, the better approach in Swift is this:

let myStruct: SomeStruct = { 
    var s = SomeStruct()
    s.name = "Fogmeister"
    s.number = 42
    s.date = "yesterday"
    return s
}()

This optimizes to the following (plus my annotations):

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):

  # allocate some storage for myStruct as a global
  alloc_global @$S1x8myStructAA04SomeB0Vvp        // id: %2
  %3 = global_addr @$S1x8myStructAA04SomeB0Vvp : $*SomeStruct // user: %23

  # Construct the tagged string value for "Fogmeister"
  %4 = integer_literal $Builtin.Int64, 8391166415069474630 // user: %9
  %5 = integer_literal $Builtin.Int64, -1585267068834385307 // user: %6
  %6 = struct $UInt (%5 : $Builtin.Int64)         // user: %7
  %7 = value_to_bridge_object %6 : $UInt          // user: %8
  %8 = struct $_StringObject (%7 : $Builtin.BridgeObject) // user: %10
  %9 = struct $UInt (%4 : $Builtin.Int64)         // user: %10
  %10 = struct $_StringGuts (%8 : $_StringObject, %9 : $UInt) // user: %11
  %11 = struct $String (%10 : $_StringGuts)       // user: %22

  # Construct the 42
  %12 = integer_literal $Builtin.Int64, 42        // user: %13
  %13 = struct $Int (%12 : $Builtin.Int64)        // user: %22

  # Construct the tagged string for "yesterday"
  %14 = integer_literal $Builtin.Int64, -1657324662872342407 // user: %15
  %15 = struct $UInt (%14 : $Builtin.Int64)       // user: %16
  %16 = value_to_bridge_object %15 : $UInt        // user: %18
  %17 = integer_literal $Builtin.Int64, 7017859899421058425 // user: %19
  %18 = struct $_StringObject (%16 : $Builtin.BridgeObject) // user: %20
  %19 = struct $UInt (%17 : $Builtin.Int64)       // user: %20
  %20 = struct $_StringGuts (%18 : $_StringObject, %19 : $UInt) // user: %21
  %21 = struct $String (%20 : $_StringGuts)       // user: %22

  # init SomeStruct and store it in our global
  %22 = struct $SomeStruct (%11 : $String, %13 : $Int, %21 : $String) // user: %23
  store %22 to %3 : $*SomeStruct                  // id: %23

  # Return 0 (cause it's main)
  %24 = integer_literal $Builtin.Int32, 0         // user: %25
  %25 = struct $Int32 (%24 : $Builtin.Int32)      // user: %26
  return %25 : $Int32                             // id: %26
} // end sil function 'main'

What you'll notice here is that the closure execution has been completely optimized out. The compiler was able to reduce "Fogmeister" and "yesterday" to their tagged-string values, and reduce this entire block into a single init call (at %22) because it noticed I was setting all the values. That's amazing.

like image 103
Rob Napier Avatar answered Jan 13 '23 11:01

Rob Napier