Lets say we define a protocol
public protocol ScreenFlags {
var cornerRadius: CGFloat { get }
}
now I can implement it for example:
struct ScreenFlagsImpl: ScreenFlags {
var cornerRadius: CGFloat { return 0 }
}
or
struct ScreenFlagsImpl: ScreenFlags {
let cornerRadius: CGFloat = 0
}
I am curious about the subtle differences here and possible dangers. Which of the implementations produces smaller/cleaner code in case the cornerRadius never changes and is there a way to enforce that cornerRadius is a constant not a variable
Refer to this compiler explorer link. Right click > "reveal linked code" to inspect corresponding assembly.
Renaming the implementations to reflect their underlying mechanisms.
struct ScreenFlagsImplComputed: ScreenFlags {
var cornerRadius: CGFloat { return 0 }
}
struct ScreenFlagsImplStored: ScreenFlags {
let cornerRadius: CGFloat = 0
}
ScreenFlagsImplComputed
uses a computed property, which compiles down to a function that returns zero.
With optimizations this will get inlined, const-folded and will compile down to one or two instructions. So, a simple assignment
let cr = screenFlagsComputed.cornerRadius
can compile down to a single instruction for a constant value like 0.
The value zero is not stored in the struct instances as it is not a stored property.
print( MemoryLayout<ScreenFlagsImplComputed>.size ) // prints '0'
You get your protocol implementation basically for "free".
ScreenFlagsImplStored
stores it explicitly, and arbitrary code cannot reason that it is going to be the constant zero. So, it will be read from the memory location where the struct is stored, which will usually be one extra instruction, the first is a load from the struct instance screenFlagsImplStored
, the second is the assignment itself to the global variable.
print( MemoryLayout<ScreenFlagsImplStored>.size ) // prints e.g. `8`, machine dependent
I do not see any memory unsafety, API misuse etc kind of dangers in either of the two approaches.
For most practical purposes the performance difference between the two should also be negligible, unless you are processing a lot of ScreenFlagsImpl
objects, and even then measure first. I would personally go for the computed property implementation since it has less overhead overall. It is also much easier for the compiler to optimize especially when you can add @inlinable
and friends for cross-module optimization. Whether it's significant will have to be determined by careful measurements using well-designed benchmarks, runtime profiles etc.