Subtle protocol difference

50 Views Asked by At

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

2

There are 2 best solutions below

3
loonatick On BEST ANSWER

Refer to this compiler explorer link. Right click > "reveal linked code" to inspect corresponding assembly.

Subtle Differences

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.

enter image description here

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.

Global let constant cr gets assigned the value 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

enter image description here

Dangers?

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.

0
Mahi Al Jawad On

You have 2 versions:

1.

struct ScreenFlagsImpl: ScreenFlags {
    var cornerRadius: CGFloat { return 0 }
}
struct ScreenFlagsImpl: ScreenFlags {
    let cornerRadius: CGFloat = 0
}

If your cornerRadius is really a constant then version 2 (with let constant) is preferable.

On the other hand, if your corner radius is constant for all ScreenFlags then it is better to add default implementation of the protocol:

extension ScreenFlags {
    var cornerRadius: CGFloat { return 0 } // Or somethings else you need
}