【问题标题】:Is there any overhead in wrapping a function in a closure to pass it as argument将函数包装在闭包中以将其作为参数传递是否有任何开销
【发布时间】:2019-07-19 12:09:44
【问题描述】:

在 swift 中,您可以将函数作为参数传递给接受闭包的函数。这对于避免在使用运算符时在语法上污染代码特别有用。例如,你可以写一个总和如下:

let values = 0 ..< 10
let sum = values.reduce(0, +)

不幸的是,当 Swift 的推理无法从其他参数中确定预期闭包的类型时,重载函数可能会导致模棱两可的情况。例如,考虑下面的代码。最后一行无法编译,因为 Swift 无法确定我所指的 + 的“版本”。

func castAndCombine<T, U>(_ pair: (Any, Any), with fn: (T, T) -> U) -> U? {
  guard let first = pair.0 as? T, let second = pair.1 as? T
    else { return nil }
  return fn(first, second)
}

// The following line cannot compile.
let x = castAndCombine((1, 2), with: +)

不幸的是,没有(或者至少我不知道)任何方式来指定我的意思是哪个+。尽管如此,我还是想出了两个解决这个问题的方法:

  1. 为函数添加参数以消除歧义:
func castAndCombine<T, U>(_ pair: (Any, Any), toType: T.Type, with fn: (T, T) -> U) -> U? {
  // ...
}
let x = castAndCombine((1, 2), toType: Int.self, with: +)
  1. 保持函数签名不变,并使用带有显式类型注释的闭包:
func castAndCombine<T, U>(_ pair: (Any, Any), with fn: (T, T) -> U) -> U? {
  // ...
}

let x = castAndCombine((1, 2), with: { (a: Int, b: Int) in a + b })

我个人不喜欢第一种解决方案,因为我觉得它不美观且使用起来不自然。但是,我想知道第二个是否会增加任何性能开销,因为创建了一个基本上包装单个函数而不添加任何行为的闭包。

有谁知道这种性能开销是否确实存在和/或在某种程度上很重要?

【问题讨论】:

  • 另一个解决歧义的选项是let x = castAndCombine((1, 2), with: +) as Int?——如果你真的必须定义一个采用Any操作数的函数。 (实际用例是什么?可能有更好的解决方案。)
  • let x = castAndCombine((1, 2), with: (+) as (Int, Int) -&gt; Int) 也可以解决歧义。
  • 用例远比我在这里发布的代码复杂,因为我试图提取一个最小的例子。我实际上正在为一种语言编写一个解释器,它的指令应该与 Swift 的指令匹配(以实现语言内置)。

标签: swift performance closures


【解决方案1】:

如果您使用优化进行编译,则不应该有任何开销,因为编译器很可能会内联您的闭包。

您可以通过比较 Swift 编写的 LLVM 代码,用您的第一个解决方案(因为它支持两种样式)验证这个假设。 LLVM 是编译器在创建实际机器代码之前使用的中间表示。

直接用操作符写一个文件,即:

let x = castAndCombine((1, 2), toType: Int.self, with: +)

使用闭包编写第二个文件,即:

let x = castAndCombine((1, 2), toType: Int.self, with: { (a: Int, b: Int) in a + b })

现在用优化编译这两个,要求 Swift 的编译器生成 LLVM IR。假设您的文件名为 main1.swiftmain2.swift,您可以运行以下命令:

swift -O -emit-ir main1.swift 1>main1.ll
swift -O -emit-ir main2.swift 1>main2.ll

两个生成的文件应该是相同的。

diff main1.ll main2.ll
# No output

请注意,cmets 中建议的解决方案也不会增加任何性能开销,因为静态保证强制转换不会花费任何操作。

【讨论】:

    【解决方案2】:

    您可以将+ 转换为所需的类型,而不是创建closure 来消除类型的歧义:

    func castAndCombine<T, U>(_ pair: (Any, Any), with fn: (T, T) -> U) -> U? {
        guard let first = pair.0 as? T, let second = pair.1 as? T
            else { return nil }
        return fn(first, second)
    }
    
    // Add two Ints by concatenating them as Strings    
    func +(_ lhs: Int, _ rhs: Int) -> String {
        return "\(lhs)\(rhs)"
    }
    
    if let x = castAndCombine((1, 2), with: (+) as (Int, Int) -> String) {
        print(x)
    }
    
    12
    
    if let x = castAndCombine((1, 2), with: (+) as (Int, Int) -> Int) {
        print(x)
    }
    
    3
    

    【讨论】:

    • 注意,因为我添加了一个附加函数,它结合了两个Int+,所以你的解决方案let x = castAndCombine((1, 2), with: { (a: Int, b: Int) in a + b })仍然不明确。
    • 你也可以给闭包的codomain添加注解,即with: { (a: Int, b: Int) -&gt; String in a + b }
    • 当然,但这比with: (+) as (Int, Int) -&gt; String 长,关闭的原因只是为了消除歧义。演员阵容是一个更直接的解决方案。
    • 感谢您的建议,我没有意识到我可以在这里简单地使用强制转换。它实际上更简洁,我会使用它。尽管如此,我还是选择了@Alvae 的答案,因为我问的是可能的性能开销,而不是解决方案。
    • @damdamo 没问题。 Alvae 的回答是对所问问题的更直接的回答。一般来说,如果 Swift 存在歧义问题,则意味着它的类型推断没有足够的信息。强制转换是告诉 Swift 编译器你在它的选项中的偏好的好方法。
    猜你喜欢
    • 2019-11-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-12
    • 1970-01-01
    相关资源
    最近更新 更多