Does Swift Optimise Chained Creation and Copy of Structs

Does Swift optimise chained creation and copy of structs?

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.

How to achieve chain calls with Struct in Swift

You should call the initialiser of the struct and pass it the given value:

struct Employee {

var name: String?
var designation: String?

func name(_ name: String) -> Employee {
.init(name: name, designation: self.designation)
}

func designation(_ designation: String) -> Employee {
.init(name: self.name, designation: designation)
}
}

If all the methods that you are chaining are boring property setters, you might as well just call the automatically generated initialisers:

// either parameter can be omitted!
Employee(name: "...", designation: "...")

This sort of chaining is only really useful when there are more complicated things going on with your properties. For example, when 2 properties must be set at the same time, or if you require generic parameters when setting those properties. For a good example of this, see how SwiftUI does this.

See also: Builder pattern set method in swift

whats the performance of chaining swift's array higher order functions?

The compiler is not smart enough to understand the functions in the standard library and come with ways to optimize their calls.

However, the problem with chaining is not the double iteration. From performance perspective iteration is not a big problem. The real problem are memory implications. Every time you call .filter or .map, the result is a new sequence, therefore we need memory to store the temporary sequence.

To alliviate this, Swift arrays have .lazy property, which enables you to chain lazily, without creating intermediate results and without multiple iterations:

bookResults.books
.lazy
.filter { !$0.pictures.isEmpty }
.map { ... }

As another solution, you can always merge filter and map using compactMap:

let presentableBooks = bookResults.books
.compactMap { book in
guard let image = book.pictures.first else { return nil }
return SearchBooks.Search.Response.BookPresentable(
pictureKey: image,
pictureStatus: .downloading,
name: book.name,
price: book.price
)
}

Why Choose Struct Over Class?

According to the very popular WWDC 2015 talk Protocol Oriented Programming in Swift (video, transcript), Swift provides a number of features that make structs better than classes in many circumstances.

Structs are preferable if they are relatively small and copiable because copying is way safer than having multiple references to the same instance as happens with classes. This is especially important when passing around a variable to many classes and/or in a multithreaded environment. If you can always send a copy of your variable to other places, you never have to worry about that other place changing the value of your variable underneath you.

With Structs, there is much less need to worry about memory leaks or multiple threads racing to access/modify a single instance of a variable. (For the more technically minded, the exception to that is when capturing a struct inside a closure because then it is actually capturing a reference to the instance unless you explicitly mark it to be copied).

Classes can also become bloated because a class can only inherit from a single superclass. That encourages us to create huge superclasses that encompass many different abilities that are only loosely related. Using protocols, especially with protocol extensions where you can provide implementations to protocols, allows you to eliminate the need for classes to achieve this sort of behavior.

The talk lays out these scenarios where classes are preferred:

  • Copying or comparing instances doesn't make sense (e.g., Window)
  • Instance lifetime is tied to external effects (e.g., TemporaryFile)
  • Instances are just "sinks"--write-only conduits to external state (e.g.CGContext)

It implies that structs should be the default and classes should be a fallback.

On the other hand, The Swift Programming Language documentation is somewhat contradictory:

Structure instances are always passed by value, and class
instances are always passed by reference. This means that they are
suited to different kinds of tasks. As you consider the data
constructs and functionality that you need for a project, decide
whether each data construct should be defined as a class or as a
structure.

As a general guideline, consider creating a structure when one or more
of these conditions apply:

  • The structure’s primary purpose is to encapsulate a few relatively simple data values.
  • It is reasonable to expect that the encapsulated values will be copied rather than referenced when you assign or pass around an
    instance of that structure.
  • Any properties stored by the structure are themselves value types, which would also be expected to be copied rather than referenced.
  • The structure does not need to inherit properties or behavior from another existing type.

Examples of good candidates for structures include:

  • The size of a geometric shape, perhaps encapsulating a width property and a height property, both of type Double.
  • A way to refer to ranges within a series, perhaps encapsulating a start property and a length property, both of type Int.
  • A point in a 3D coordinate system, perhaps encapsulating x, y and z properties, each of type Double.

In all other cases, define a class, and create instances of that class
to be managed and passed by reference. In practice, this means that
most custom data constructs should be classes, not structures.

Here it is claiming that we should default to using classes and use structures only in specific circumstances. Ultimately, you need to understand the real world implication of value types vs. reference types and then you can make an informed decision about when to use structs or classes. Also, keep in mind that these concepts are always evolving and The Swift Programming Language documentation was written before the Protocol Oriented Programming talk was given.

Chaining methods in Swift

sketchyTech's answer is correct, but actually it is just hiding the fact, that

let list = arr.map { $0 * 2 }.map { $0 - 1 }

is executed — this is not optimal as the array is enumerated twice.

You could achieve the same with one enumeration with

let complexClosure = { (i: Int) in
return i-1
}
let list = arr.map { complexClosure($0 * 2) }

Chaining Multiple JSON Request Using Decodable - Swift 5

So I've figured it out. I was using Decodable without CodingKeys. Thanks to Vadian for pointing me in the right direction. Here's my example:

struct Response : Decodable {
let results: [Results]

enum CodingKeys: String, CodingKey {
case results = "photos"
}
}

struct Results : Decodable {
let url : String?

enum CodingKeys: String, CodingKey {
case url = "url"
}
}

Append a struct inside a property which is an array inside other struct in Swift

In the following guard statement (in method addProduct of OrderManager)

guard var order = self.order else { /* ... */ }

you create a copy of self.order (given that it is not nil), due to the value semantics of structures in Swift. The subsequent call to addToDetail on this copy will not append a Product instance to the instance variable order of self, but only to the copy which goes out of scope as addProduct goes out of scope.

You could test this theory by replacing the optional binding clause in the guard statement above with a simple nil check (as @MartinR points out below, we don't really need a guard statement (after the fix: no binding), but can just perform an early return in case self.order is nil)

if self.order == nil { return }

Or, remove the explicit nil check altogether, and use optional chaining to decide whether or not to add a product to the order instance (combining the nil check and hasProduct in a single optional chaining clause):

func addProduct(objectId: String, name: String, price: Double, qty: Int, img: String = "", desc: String = "", note: String = "") {
if !(self.order?.detail.contains(where: { $0.objectId == objectId }) ?? true) {
order?.addToDetail(Product(objectId: objectId, name: name, price: price, qty: qty, img: img, desc: desc, note: note))
}
// ... remove the logging
}

This does a poorer job showing the intent of the code, though.



Related Topics



Leave a reply



Submit