4

Just curious which is more efficient/better in swift:

  • Creating three temporary constants (using let) and using those constants to define other variables
  • Creating one temporary variable (using var) and using that variable to hold three different values which will then be used to define other variables

This is perhaps better explained through an example:

var one = Object()
var two = Object()
var three = Object()

func firstFunction() {
    let tempVar1 = //calculation1
    one = tempVar1

    let tempVar2 = //calculation2
    two = tempVar2

    let tempVar3 = //calculation3
    three = tempVar3

}

func seconFunction() {
    var tempVar = //calculation1
        one = tempVar

    tempVar = //calculation2
        two = tempVar

    tempVar = //calculation3
        three = tempVar

}

Which of the two functions is more efficient? Thank you for your time!

retrovius
  • 782
  • 1
  • 10
  • 25
  • 7
    the compiler will most likely optimize out all the temporary variables and leave you with direct assignments to globals in both cases. – nate Jul 14 '17 at 17:53
  • 4
    1) Don't bother. Write clear (maintainable) and robust code and let the compiler do the optimisations. 2) Why do you need tempvar at all? `one = calculation; two = calculation; ...` – Martin R Jul 14 '17 at 17:54
  • If you're so concerned about efficiency, you realize that you're allocating 3 `Object` instances, only to instantly clobber them? – Alexander Jul 14 '17 at 19:09
  • @MartinR Thank you! This is sort of a simplified case to cut out any code necessary to the question. – retrovius Jul 17 '17 at 14:48
  • @Alexander lol! I guess I really don't know anything about efficiency. Would you mind explaining why it's a bad idea to do what I did? – retrovius Jul 17 '17 at 14:48
  • @retrovius Well think about it. You're asking the computer to create three new instances of `Object`, by invoking the `Object` initializer 3 times. Shortly after, your `firstFunction`/`seconFunction` create an assign 3 new `Object` instance references, replacing the old. Why create the first ones if you're only going to immediately overwrite them? – Alexander Jul 17 '17 at 16:54

3 Answers3

5

Not to be too cute about it, but the most efficient version of your code above is:

var one = Object()
var two = Object()
var three = Object()

That is logically equivalent to all the code you've written since you never use the results of the computations (assuming the computations have no side-effects). It is the job of the optimizer to get down to this simplest form. Technically the simplest form is:

func main() {}

But the optimizer isn't quite that smart. But the optimizer really is smart enough to get to my first example. Consider this program:

var one = 1
var two = 2
var three = 3

func calculation1() -> Int { return 1 }
func calculation2() -> Int { return 2 }
func calculation3() -> Int { return 3 }

func firstFunction() {
    let tempVar1 = calculation1()
    one = tempVar1

    let tempVar2 = calculation2()
    two = tempVar2

    let tempVar3 = calculation3()
    three = tempVar3

}

func secondFunction() {
    var tempVar = calculation1()
        one = tempVar

    tempVar = calculation2()
        two = tempVar

    tempVar = calculation3()
        three = tempVar
}

func main() {
    firstFunction()
    secondFunction()
}

Run it through the compiler with optimizations:

$ swiftc -O -wmo -emit-assembly x.swift

Here's the whole output:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 9
    .globl  _main
    .p2align    4, 0x90
_main:
    pushq   %rbp
    movq    %rsp, %rbp
    movq    $1, __Tv1x3oneSi(%rip)
    movq    $2, __Tv1x3twoSi(%rip)
    movq    $3, __Tv1x5threeSi(%rip)
    xorl    %eax, %eax
    popq    %rbp
    retq

    .private_extern __Tv1x3oneSi
    .globl  __Tv1x3oneSi
.zerofill __DATA,__common,__Tv1x3oneSi,8,3
    .private_extern __Tv1x3twoSi
    .globl  __Tv1x3twoSi
.zerofill __DATA,__common,__Tv1x3twoSi,8,3
    .private_extern __Tv1x5threeSi
    .globl  __Tv1x5threeSi
.zerofill __DATA,__common,__Tv1x5threeSi,8,3
    .private_extern ___swift_reflection_version
    .section    __TEXT,__const
    .globl  ___swift_reflection_version
    .weak_definition    ___swift_reflection_version
    .p2align    1
___swift_reflection_version:
    .short  1

    .no_dead_strip  ___swift_reflection_version
    .linker_option "-lswiftCore"
    .linker_option "-lobjc"
    .section    __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
    .long   0
    .long   1088

Your functions aren't even in the output because they don't do anything. main is simplified to:

_main:
    pushq   %rbp
    movq    %rsp, %rbp
    movq    $1, __Tv1x3oneSi(%rip)
    movq    $2, __Tv1x3twoSi(%rip)
    movq    $3, __Tv1x5threeSi(%rip)
    xorl    %eax, %eax
    popq    %rbp
    retq

This sticks the values 1, 2, and 3 into the globals, and then exits.

My point here is that if it's smart enough to do that, don't try to second-guess it with temporary variables. It's job is to figure that out. In fact, let's see how smart it is. We'll turn off Whole Module Optimization (-wmo). Without that, it won't strip the functions, because it doesn't know whether something else will call them. And then we can see how it writes these functions.

Here's firstFunction():

__TF1x13firstFunctionFT_T_:
    pushq   %rbp
    movq    %rsp, %rbp
    movq    $1, __Tv1x3oneSi(%rip)
    movq    $2, __Tv1x3twoSi(%rip)
    movq    $3, __Tv1x5threeSi(%rip)
    popq    %rbp
    retq

Since it can see that the calculation methods just return constants, it inlines those results and writes them to the globals.

Now how about secondFunction():

__TF1x14secondFunctionFT_T_:
    pushq   %rbp
    movq    %rsp, %rbp
    popq    %rbp
    jmp __TF1x13firstFunctionFT_T_

Yes. It's that smart. It realized that secondFunction() is identical to firstFunction() and it just jumps to it. Your functions literally could not be more identical and the optimizer knows that.

So what's the most efficient? The one that is simplest to reason about. The one with the fewest side-effects. The one that is easiest to read and debug. That's the efficiency you should be focused on. Let the optimizer do its job. It's really quite smart. And the more you write in nice, clear, obvious Swift, the easier it is for the optimizer to do its job. Every time you do something clever "for performance," you're just making the optimizer work harder to figure out what you've done (and probably undo it).


Just to finish the thought: the local variables you create are barely hints to the compiler. The compiler generates its own local variables when it converts your code to its internal representation (IR). IR is in static single assignment form (SSA), in which every variable can only be assigned one time. Because of this, your second function actually creates more local variables than your first function. Here's function one (create using swiftc -emit-ir x.swift):

define hidden void @_TF1x13firstFunctionFT_T_() #0 {
entry:
  %0 = call i64 @_TF1x12calculation1FT_Si()
  store i64 %0, i64* getelementptr inbounds (%Si, %Si* @_Tv1x3oneSi, i32 0, i32 0), align 8
  %1 = call i64 @_TF1x12calculation2FT_Si()
  store i64 %1, i64* getelementptr inbounds (%Si, %Si* @_Tv1x3twoSi, i32 0, i32 0), align 8
  %2 = call i64 @_TF1x12calculation3FT_Si()
  store i64 %2, i64* getelementptr inbounds (%Si, %Si* @_Tv1x5threeSi, i32 0, i32 0), align 8
  ret void
}

In this form, variables have a % prefix. As you can see, there are 3.

Here's your second function:

define hidden void @_TF1x14secondFunctionFT_T_() #0 {
entry:
  %0 = alloca %Si, align 8
  %1 = bitcast %Si* %0 to i8*
  call void @llvm.lifetime.start(i64 8, i8* %1)
  %2 = call i64 @_TF1x12calculation1FT_Si()
  %._value = getelementptr inbounds %Si, %Si* %0, i32 0, i32 0
  store i64 %2, i64* %._value, align 8
  store i64 %2, i64* getelementptr inbounds (%Si, %Si* @_Tv1x3oneSi, i32 0, i32 0), align 8
  %3 = call i64 @_TF1x12calculation2FT_Si()
  %._value1 = getelementptr inbounds %Si, %Si* %0, i32 0, i32 0
  store i64 %3, i64* %._value1, align 8
  store i64 %3, i64* getelementptr inbounds (%Si, %Si* @_Tv1x3twoSi, i32 0, i32 0), align 8
  %4 = call i64 @_TF1x12calculation3FT_Si()
  %._value2 = getelementptr inbounds %Si, %Si* %0, i32 0, i32 0
  store i64 %4, i64* %._value2, align 8
  store i64 %4, i64* getelementptr inbounds (%Si, %Si* @_Tv1x5threeSi, i32 0, i32 0), align 8
  %5 = bitcast %Si* %0 to i8*
  call void @llvm.lifetime.end(i64 8, i8* %5)
  ret void
}

This one has 6 local variables! But, just like the local variables in the original source code, this tells us nothing about final performance. The compiler just creates this version because it's easier to reason about (and therefore optimize) than a version where variables can change their values.

(Even more dramatic is this code in SIL (-emit-sil), which creates 16 local variables for function 1 and 17 for function 2! If the compiler is happy to invent 16 local variables just to make it easier for it to reason about 6 lines of code, you certainly shouldn't be worried about the local variables you create. They're not just a minor concern; they're completely free.)

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Is there a reason the optimizer doesn't strip out the assignment to the globals? – Alexander Jul 14 '17 at 19:50
  • Not certain. There's an existing bug indicating that it is difficult to detect unused globals (in order to generate warnings). That may be related. https://bugs.swift.org/browse/SR-3721 – Rob Napier Jul 14 '17 at 21:51
  • Keep in mind that global object creation is lazy, so this would likely be a really small optimization in all but the most pathological cases. (Expensive initializations that you never use cost nothing.) – Rob Napier Jul 14 '17 at 21:56
  • Thank you very much for that in-depth answer! So the takeaway is that I don't really need to worry about performance when it comes to making a ton of variables because the optimizer handles that all for me? – retrovius Jul 17 '17 at 14:41
3

Unless you're dealing with a VERY specialized use case, this should never make a meaningful performance difference.

It's likely the compiler can easily simplify things to direct assignments in firstFunction, I'm unsure if secondFunction will easily lend itself to similar compiler optimization. You would either have to be an expert on the compiler or do some performance tests to find any differences.

Regardless, unless you're doing this at a scale of hundreds of thousands or millions it's not something to worry about.

I personally think re-using variables in that way of the secondFunction is unnecessarily confusing, but to each their own.

Note: it looks like you're dealing with classes, but be aware that struct copy semantics means re-using variables is useless anyway.

GetSwifty
  • 7,568
  • 1
  • 29
  • 46
  • That's very helpful, thank you! So the takeaway is that on the scale of a few variables, it really doesn't make a difference which method I use? Just focus on human readability of the code? – retrovius Jul 17 '17 at 14:45
  • Yup. Especially with how smart compilers are these days, it's better to deal with ambiguous performance questions only when necessary. – GetSwifty Jul 17 '17 at 15:19
2

You should really just inline the local variables:

var one: Object
var two: Object
var three: Object

func firstFunction() {
    one = //calculation1
    two = //calculation2
    three = //calculation3
}

One exception to this is if you end up writing something like this:

var someOptional: Foo?

func init() {
    self.someOptional = Foo()
    self.someOptional?.a = a
    self.someOptional?.b = b
    self.someOptional?.c = c
}

In which case it would be better to do:

func init() {
    let foo = Foo()

    foo.a = a
    foo.b = b
    foo.c = c

    self.someOptional = foo
}

or perhaps:

func init() {
    self.someOptional = {
        let foo = Foo()
        foo.a = a
        foo.b = b
        foo.c = c
        return foo
    }()
}
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Thanks for that tip! What if I need to use the variable that I am giving a new value _in_ the calculation? – retrovius Jul 17 '17 at 14:43
  • `use the variable that I am giving a new value in the calculation?` Elaborate. What variable, used where? – Alexander Jul 17 '17 at 16:54