我正在创建一个性能高的应用程序,并且想知道编写相同代码的哪种方式在运行时运行得更快。
选项1:
let a = 1 + 2
self.doSomething(with: a)
self.doSomethingElse(with: a)
Run Code Online (Sandbox Code Playgroud)
选项2:
self.doSomething(with: 1 + 2)
self.doSomethingElse(with: 1 + 2)
Run Code Online (Sandbox Code Playgroud)
如果任何一个选项都更快,那么对结构体也是如此吗?例如
let a = CGPoint(x: 1, y: 1)
self.doSomething(with: a)
self.doSomethingElse(with: a)
Run Code Online (Sandbox Code Playgroud)
要么
self.doSomething(with: CGPoint(x: 1, y: 1))
self.doSomethingElse(with: CGPoint(x: 1, y: 1))
Run Code Online (Sandbox Code Playgroud)
编辑:添加了真实的场景
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let currentPoint = touch.location(in: view)
let lastPoint = touch.previousLocation(in: view)
//////////
let newPoint1 = CGPoint(x: lastPoint.x - currentPoint.x, y: lastPoint.y - currentPoint.y)
let newPoint2 = CGPoint(x: lastPoint.y - currentPoint.y, y: lastPoint.x - currentPoint.x)
// OR
let newX = lastPoint.x - currentPoint.x
let newY = lastPoint.y - currentPoint.y
let newPoint11 = CGPoint(x: newX, y: newY)
let newPoint22 = CGPoint(x: newY, y: newX)
///////
print([newPoint1, newPoint2])
print([newPoint11, newPoint22])
}
Run Code Online (Sandbox Code Playgroud)
这些字面意思是一样的。编译器将为a您内联。我们怎么知道?我们问编译器。
struct X {
func doSomething(with value: Int) {
print("something: \(value)")
}
func doSomethingElse(with value: Int) {
print("somethingElse: \(value)")
}
func f() {
let a = 1 + 2
doSomething(with: a)
doSomethingElse(with: a)
}
}
X().f()
$ swiftc -O -emit-sil first.c
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = integer_literal $Builtin.Int64, 3 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // users: %8, %6, %4
debug_value %3 : $Int, let, name "a" // id: %4
// function_ref specialized X.doSomething(with:)
%5 = function_ref @$s5first1XV11doSomething4withySi_tFTf4nd_n : $@convention(thin) (Int) -> () // user: %6
%6 = apply %5(%3) : $@convention(thin) (Int) -> ()
// function_ref specialized X.doSomethingElse(with:)
%7 = function_ref @$s5first1XV15doSomethingElse4withySi_tFTf4nd_n : $@convention(thin) (Int) -> () // user: %8
%8 = apply %7(%3) : $@convention(thin) (Int) -> ()
%9 = integer_literal $Builtin.Int32, 0 // user: %10
%10 = struct $Int32 (%9 : $Builtin.Int32) // user: %11
return %10 : $Int32 // id: %11
} // end sil function 'main'
Run Code Online (Sandbox Code Playgroud)
...
func f() {
doSomething(with: 1 + 2)
doSomethingElse(with: 1 + 2)
}
...
$ swiftc -O -emit-sil second.swift
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = integer_literal $Builtin.Int64, 3 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // users: %7, %5
// function_ref specialized X.doSomething(with:)
%4 = function_ref @$s6second1XV11doSomething4withySi_tFTf4nd_n : $@convention(thin) (Int) -> () // user: %5
%5 = apply %4(%3) : $@convention(thin) (Int) -> ()
// function_ref specialized X.doSomethingElse(with:)
%6 = function_ref @$s6second1XV15doSomethingElse4withySi_tFTf4nd_n : $@convention(thin) (Int) -> () // user: %7
%7 = apply %6(%3) : $@convention(thin) (Int) -> ()
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
return %9 : $Int32 // id: %10
} // end sil function 'main'
Run Code Online (Sandbox Code Playgroud)
注意在这两种情况下都f()消失了。这只是main()因为f()无论如何都会被内联。然后请注意,在这两种情况下,我们都有以下几行:
%2 = integer_literal $Builtin.Int64, 3 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // users: %8, %6, %4
Run Code Online (Sandbox Code Playgroud)
编译器看到有文字值 3 的计算,它在编译时计算它并将其粘贴在编译器变量 (%3) 中。唯一的区别是在第一种情况下,编译器发出 adebug_value以便调试器具有 %3 的本地名称。如果删除调试信息,这将与以后完全无关。但除此之外,它实际上是相同的代码。没有内存或注册分配来保存 3。它在编译时是已知的,并在任何引用它的地方注入。
执行代码中更清晰的操作。编译器会处理它。
对于您对 CGPoint 的问题,您可以做同样的事情。您会看到优化器也可以检测到这些是常量。在 SIL 级别,它将生成两个 CGFloats,并将它们放在一个 CGPoint 中(它会在有或没有局部变量的情况下重用它)。但是,如果您查看最终的汇编输出,它通常能够将整个结构简化为最终的逐字节表示(并且通常将其直接内联到print调用中)。例如,我创建了一个CGPoint(x: 999, y: 888). 发出的程序集为doSomething:
movabsq $7956005065853857651, %rsi
movabsq $-1215907691987450521, %rdx
callq _$sSS5write2toyxz_ts16TextOutputStreamRzlF
Run Code Online (Sandbox Code Playgroud)
由于我只调用doSomething了一次,编译器意识到它可以直接将字符串插值所需的确切值硬编码到程序集中(您也会在其中找到这两个相同的“随机”数字doSomethingElse)。在编译步骤中进行字符串插值不够聪明,但它知道所有涉及的文字。
写清楚。让优化器完成它的工作。然后,只有这样,才能探索您是否可以做得更好。未经仔细测试,切勿猜测优化器。它通常(虽然不总是)比你更聪明。
使用let一次来计算值。这告诉Swift编译器两次使用相同的值,并允许Swift编译器/优化器生成更严格的代码。如果您知道值相同,请与编译器共享该信息,并且不要让优化器弄清楚它本身(因为它可能无法)。
在with的示例中1 + 2,由于Constant Folding的缘故,这肯定会在两个实例中生成相同的代码。编译器将1 + 2在编译时执行,并且生成的代码将仅传递3给每个函数调用。
在第二个示例中,Swift编译器可能无法识别出您已经生成了相同的两个版本struct,并且它可能会发出生成struct两次的代码。通过将该结构分配给一个常量a,Swift随后知道它可以将其传递struct给两个函数,并且避免创建两次。
一般规则:为编译器提供更多信息可以使其进行更好的优化。
额外的好处:使用let使您的代码更具可读性,更易于修改。
在您的实际情况中:
let newPoint1 = CGPoint(x: lastPoint.x - currentPoint.x, y: lastPoint.y - currentPoint.y)
let newPoint2 = CGPoint(x: lastPoint.y - currentPoint.y, y: lastPoint.x - currentPoint.x)
// OR
let newX = lastPoint.x - currentPoint.x
let newY = lastPoint.y - currentPoint.y
let newPoint11 = CGPoint(x: newX, y: newY)
let newPoint22 = CGPoint(x: newY, y: newX)
Run Code Online (Sandbox Code Playgroud)
同样,由于一种称为Common Subexpression Elimination的技术,编译器可能会生成相同的代码,在该技术中,编译器将检测并消除冗余表达式。但是为什么要依靠这个呢?您知道值表示newX和newY,因此通过将这些值首先作为常量进行计算,您可以:1)让编译器知道一次计算该表达式,2)向您自己和您的读者说明代码的意图。
第二个示例除了为编译器/优化器提供了额外的提示之外,更清晰,更易于修改。总的来说,这是更好的代码。
| 归档时间: |
|
| 查看次数: |
78 次 |
| 最近记录: |