Search code examples
swiftprotocols

Subtle protocol difference


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


Solution

  • 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.