Search code examples
swiftstatic-variablescomputed-properties

Is It Inefficient To Access A Static Property Through Computed Properties In Swift?


Say I have some constants that should be treated as class variables.

static let constant1 = 1
static let constant2 = 2
static let constant3 = 3

In order to access constant1, I need to go thru the class, like OwnerClass.constant1.

But I can also access them thru computed properties. For example

var constant1:Int { get{ return OwnerClass.constant1 }}

This is done to avoid the need to type OwnerClass. repetitively.

But the question is, is that inefficient?


Solution

  • As with most optimization questions, it depends on your precise code and the version of the compiler, and also we don't have to guess, we can check.

    By "inefficient" I'm going to assume you mean "fails to inline the accessor call."

    The TL;DR is: In almost all cases the optimizer will inline this either way. There are corner cases where the accessor version is not inlined, for example if the caller is at the top-level (not inside a function) and the class is non-final. (I don't know why that's a corner-case; it may be an optimizer bug.)

    I'm neutral on whether this is a good design. I'm fine with it (and occasionally use this pattern myself). But I certainly wouldn't avoid it out of performance concerns. (In cases where one extra function call would be a problem, you're going to need to hand-optimize anyway.)

    The details

    As with most optimization/performance questions, it will depend on your exact code and the version of the compiler. As I said, there are some corner cases where this doesn't get optimized. I tested with Swift 5.5.2.

    First, I created a test program:

    // Avoid the complexity around calling print()
    // while ensuring that the variable is not optimized away
    @inline(never)
    func handle(_ x: Int) {
        print(x)
    }
    
    // Stick it in a function to avoid the special handling of
    // global variables
    func f() {
        let c = OwnerClass()
    
        let x = OwnerClass.constant1
        handle(x)
        let y = c.constant1
        handle(y)
    }
    
    // Make sure to call the function so it's not optimized away
    f()
    

    Then I checked it with several version of OwnerClass (I use 12345678 to make it easier to find in the output):

    // Class
    class OwnerClass {
        static let constant1 = 12345678
        var constant1:Int { get{ return OwnerClass.constant1 }}
    }
    
    // Final class
    final class OwnerClass {
        static let constant1 = 12345678
        var constant1:Int { get{ return OwnerClass.constant1 }}
    }
    
    // Struct
    struct OwnerClass {
        static let constant1 = 12345678
        var constant1:Int { get{ return OwnerClass.constant1 }}
    }
    
    // Instance constant 
    class OwnerClass {
        static let constant1 = 12345678
        let constant1:Int = OwnerClass.contant1
    }
    

    The only one that ever had trouble (for example, when I didn't wrap it all in a function), was the non-final class with an accessor.

    To see what the optimizer does, I compiled with swiftc -emit-sil -O x.swift. In all cases, this is what f() compiles to:

    // f()
    sil hidden @$s1x1fyyF : $@convention(thin) () -> () {
    bb0:
      %0 = integer_literal $Builtin.Int64, 12345678   // user: %1
      %1 = struct $Int (%0 : $Builtin.Int64)          // users: %6, %5, %4, %2
      debug_value %1 : $Int, let, name "x"            // id: %2
      // function_ref handle(_:)
      %3 = function_ref @$s1x6handleyySiF : $@convention(thin) (Int) -> () // users: %6, %4
      %4 = apply %3(%1) : $@convention(thin) (Int) -> ()
      debug_value %1 : $Int, let, name "y"            // id: %5
      %6 = apply %3(%1) : $@convention(thin) (Int) -> ()
      %7 = tuple ()                                   // user: %8
      return %7 : $()                                 // id: %8
    } // end sil function '$s1x1fyyF'
    

    The important thing to note is that the constant 12345678 is inlined into the function as %0 (wrapped into %1), and then it's used twice in %4 and %6 to call handle(). No calls are made to the accessor. OwnerClass isn't even referenced (the creation of c is optimized away).