Does Swift optimise chained creation and copy of structs?
up vote
1
down vote
favorite
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?
swift optimization compilation
add a comment |
up vote
1
down vote
favorite
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?
swift optimization compilation
add a comment |
up vote
1
down vote
favorite
up vote
1
down vote
favorite
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?
swift optimization compilation
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?
swift optimization compilation
swift optimization compilation
asked Nov 12 at 17:29
Fogmeister
55.2k25149239
55.2k25149239
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
up vote
4
down vote
accepted
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.
Awesome, thanks :D I didn't know about compiling that way but just done it myself to have a look into it a bit more. Interesting, thanks.
– Fogmeister
Nov 12 at 18:22
add a comment |
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
4
down vote
accepted
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.
Awesome, thanks :D I didn't know about compiling that way but just done it myself to have a look into it a bit more. Interesting, thanks.
– Fogmeister
Nov 12 at 18:22
add a comment |
up vote
4
down vote
accepted
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.
Awesome, thanks :D I didn't know about compiling that way but just done it myself to have a look into it a bit more. Interesting, thanks.
– Fogmeister
Nov 12 at 18:22
add a comment |
up vote
4
down vote
accepted
up vote
4
down vote
accepted
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.
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.
edited Nov 12 at 18:14
answered Nov 12 at 18:00
Rob Napier
196k27289414
196k27289414
Awesome, thanks :D I didn't know about compiling that way but just done it myself to have a look into it a bit more. Interesting, thanks.
– Fogmeister
Nov 12 at 18:22
add a comment |
Awesome, thanks :D I didn't know about compiling that way but just done it myself to have a look into it a bit more. Interesting, thanks.
– Fogmeister
Nov 12 at 18:22
Awesome, thanks :D I didn't know about compiling that way but just done it myself to have a look into it a bit more. Interesting, thanks.
– Fogmeister
Nov 12 at 18:22
Awesome, thanks :D I didn't know about compiling that way but just done it myself to have a look into it a bit more. Interesting, thanks.
– Fogmeister
Nov 12 at 18:22
add a comment |
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53267232%2fdoes-swift-optimise-chained-creation-and-copy-of-structs%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown