【问题标题】:Which is more efficient: Creating a "var" and re-using it, or creating several "let"s?哪个更有效:创建一个“var”并重新使用它,还是创建几个“let”?
【发布时间】:2017-12-19 21:49:30
【问题描述】:

只是好奇哪个在 swift 中更有效/更好:

  • 创建三个临时常量(使用 let)并使用这些常量定义其他变量
  • 创建一个临时变量(使用 var)并使用该变量来保存三个不同的值,然后这些值将用于定义其他变量

这可能通过一个例子更好地解释:

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

}

这两个函数中哪个更高效?感谢您的宝贵时间!

【问题讨论】:

  • 编译器很可能会优化所有临时变量,并让您在这两种情况下都可以直接分配给全局变量。
  • 1) 不要打扰。编写清晰(可维护)且健壮的代码,并让编译器进行优化。 2) 为什么你需要 tempvar? one = calculation; two = calculation; ...
  • 如果您非常关心效率,您会意识到自己分配了 3 个 Object 实例,却只是为了立即破坏它们?
  • @MartinR 谢谢!这是一种简化的案例,可以删除问题所需的任何代码。
  • @Alexander 哈哈!我想我真的对效率一无所知。你介意解释一下为什么做我所做的事情是个坏主意吗?

标签: swift processing-efficiency memory-efficient


【解决方案1】:

除非您正在处理一个非常专业的用例,否则这永远不会产生有意义的性能差异。

编译器很可能很容易将事情简化为在firstFunction 中的直接赋值,我不确定secondFunction 是否可以轻松地进行类似的编译器优化。您要么必须是编译器方面的专家,要么进行一些性能测试才能发现任何差异。

无论如何,除非您以数十万或数百万的规模执行此操作,否则无需担心。

我个人认为以 secondFunction 的这种方式重用变量会造成不必要的混乱,但对每个人来说都是如此。

注意:看起来您正在处理类,但请注意struct 复制语义意味着重用变量无论如何都没用。

【讨论】:

  • 这很有帮助,谢谢!所以要点是,在几个变量的范围内,我使用哪种方法真的没有区别吗?只关注代码的人类可读性?
  • 是的。尤其是如今的编译器非常聪明,最好只在必要时处理模棱两可的性能问题。
【解决方案2】:

你真的应该只内联局部变量:

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

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

一个例外情况是,如果你最终写出这样的东西:

var someOptional: Foo?

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

在这种情况下最好这样做:

func init() {
    let foo = Foo()

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

    self.someOptional = foo
}

或许:

func init() {
    self.someOptional = {
        let foo = Foo()
        foo.a = a
        foo.b = b
        foo.c = c
        return foo
    }()
}

【讨论】:

  • 感谢您的提示!如果我需要使用我在计算中 in 赋予新值的变量怎么办?
  • use the variable that I am giving a new value in the calculation? 详细说明。什么变量,在哪里使用?
【解决方案3】:

不要太可爱,但上面代码的最有效版本是:

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

这在逻辑上等同于您编写的所有代码,因为您从不使用计算结果(假设计算没有副作用)。优化器的工作就是简化这种最简单的形式。技术上最简单的形式是:

func main() {}

但优化器并不是很聪明。但是优化器确实足够聪明,可以处理我的第一个示例。考虑这个程序:

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()
}

通过优化的编译器运行它:

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

这是整个输出:

    .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

您的函数甚至不是 in 输出,因为它们什么都不做。 main 简化为:

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

这会将值 1、2 和 3 粘贴到全局变量中,然后退出。

我的意思是,如果它足够聪明地做到这一点,不要试图用临时变量来猜测它。它的工作就是弄清楚这一点。事实上,让我们看看它有多聪明。我们将关闭整个模块优化 (-wmo)。没有它,它不会剥离函数,因为它不知道其他东西是否会调用它们。然后我们可以看到它是如何编写这些函数的。

这里是firstFunction()

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

由于它可以看到计算方法只是返回常量,它内联这些结果并将它们写入全局。

现在secondFunction()怎么样:

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

是的。就是这么聪明。它意识到secondFunction()firstFunction() 相同,它只是跳转到它。你的函数简直不能更相同,优化器知道这一点。

那么什么是最有效的?最容易推理的一种。副作用最少的一种。最容易阅读和调试的一种。这就是你应该关注的效率。让优化器完成它的工作。它真的很聪明。你用漂亮、清晰、明显的 Swift 编写的越多,优化器就越容易完成它的工作。每次你为了“性能”而做一些聪明的事情时,你只是让优化器更加努力地弄清楚你做了什么(并且可能撤消它)。


只是为了完成这个想法:您创建的局部变量对编译器几乎没有提示。编译器在将您的代码转换为其内部表示 (IR) 时会生成其自己的 局部变量。 IR 在static single assignment form (SSA) 中,其中每个变量只能分配一次。因此,您的第二个函数实际上比您的第一个函数创建了更多的局部变量。这是函数一(使用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
}

在这种形式中,变量具有% 前缀。如您所见,共有 3 个。

这是你的第二个函数:

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
}

这个有6个局部变量!但是,就像原始源代码中的局部变量一样,这并不能告诉我们最终性能。编译器只是创建这个版本,因为它比变量可以更改其值的版本更容易推理(并因此优化)。

(更引人注目的是SIL (-emit-sil) 中的这段代码,它为函数 1 创建了 16 个局部变量,为函数 2 创建了 17 个局部变量!如果编译器乐于发明 16 个局部变量只是为了更容易它推理大约 6 行代码,你当然不应该担心你创建的局部变量。它们不仅仅是一个小问题;它们是完全免费的。)

【讨论】:

  • 优化器是否有理由不去除对全局变量的赋值?
  • 不确定。存在一个错误,表明很难检测未使用的全局变量(以生成警告)。这可能是相关的。 bugs.swift.org/browse/SR-3721
  • 请记住,全局对象的创建是惰性的,因此除了最病态的情况外,这可能是一个非常小的优化。 (您从不使用的昂贵初始化没有任何成本。)
  • 非常感谢您的深入回答!所以要点是,当涉及到大量变量时,我真的不需要担心性能,因为优化器会为我处理所有这些?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-17
  • 1970-01-01
  • 2012-07-15
  • 2020-06-11
相关资源
最近更新 更多