Goatt
Goatt

Reputation: 121

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?

Upvotes: 1

Views: 589

Answers (1)

Rob Napier
Rob Napier

Reputation: 299355

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

Upvotes: 2

Related Questions