Swift是否优化了链式创建和结构副本?

Fog*_*ter 1 optimization compilation swift

如果我用像......这样的函数创建一个Struct

struct SomeStruct {
    var name: String? = nil
    var number: Int = 0
    var date: Date? = nil
    //... many other properties

    func setting<Value>(_ keyPath: WritableKeyPath<SomeStruct, Value>, to value: Value) -> SomeStruct {
        var copy = self
        copy[keyPath: keyPath] = value
        return copy
    }
}
Run Code Online (Sandbox Code Playgroud)

Swift是否做了类似......的任何优化

let myStruct = SomeStruct()
    .setting(\.name, to: "Fogmeister")
    .setting(\.number, to: 42)
    .setting(\.date, to: yesterday)
    .setting(\.otherProperty, to: value)
    ...etc
    ...etc
Run Code Online (Sandbox Code Playgroud)

因为该setting函数创建一个副本并在每次基本上创建一个新的Struct时更改副本,然后丢弃除了其中一个之外的所有结构.

在执行此操作时是否需要考虑任何开销注意事项,或者Swift在编译时优化掉所有这些未使用的值?

Rob*_*ier 6

不,这没有优化.它将为每个呼叫创建一个新副本.很难想象优化器会如何解决这个问题以避免副本,但编写优化器的向导之前已经欺骗过我.但与大多数优化器问题一样,我们不必猜测它的作用.我们可以看看.

我稍微重写了这段代码以摆脱选项(这只是稍微复杂化而不改变问题).

struct SomeStruct {
    var name: String = ""
    var number: Int = 0
    var date: String = ""

    func setting<Value>(_ keyPath: WritableKeyPath<SomeStruct, Value>, to value: Value) -> SomeStruct {
        var copy = self
        copy[keyPath: keyPath] = value
        return copy
    }
}

let myStruct = SomeStruct()
    .setting(\.name, to: "Fogmeister")
    .setting(\.number, to: 42)
    .setting(\.date, to: "yesterday")
Run Code Online (Sandbox Code Playgroud)

然后通过优化将其编译为SIL:

swiftc -O -emit-sil x.swift
Run Code Online (Sandbox Code Playgroud)

setting方法变为:

// SomeStruct.setting<A>(_:to:)
sil hidden @$S1x10SomeStructV7setting_2toACs15WritableKeyPathCyACxG_xtlF : $@convention(method) <Value> (@guaranteed WritableKeyPath<SomeStruct, Value>, @in_guaranteed Value, @guaranteed SomeStruct) -> @owned SomeStruct {
// %0                                             // users: %26, %17, %18, %3
// %1                                             // users: %11, %4
// %2                                             // users: %8, %7, %9, %5
bb0(%0 : $WritableKeyPath<SomeStruct, Value>, %1 : $*Value, %2 : $SomeStruct):
  debug_value %0 : $WritableKeyPath<SomeStruct, Value>, let, name "keyPath", argno 1 // id: %3
  debug_value_addr %1 : $*Value, let, name "value", argno 2 // id: %4
  debug_value %2 : $SomeStruct, let, name "self", argno 3 // id: %5
  %6 = alloc_stack $SomeStruct, var, name "copy"  // users: %12, %28, %9, %29
  %7 = struct_extract %2 : $SomeStruct, #SomeStruct.name // user: %15
  %8 = struct_extract %2 : $SomeStruct, #SomeStruct.date // user: %16
  store %2 to %6 : $*SomeStruct                   // id: %9
  %10 = alloc_stack $Value                        // users: %27, %24, %11
  copy_addr %1 to [initialization] %10 : $*Value  // id: %11
  %12 = address_to_pointer %6 : $*SomeStruct to $Builtin.RawPointer // user: %13
  %13 = struct $UnsafeMutablePointer<SomeStruct> (%12 : $Builtin.RawPointer) // user: %18
  // function_ref _projectKeyPathWritable<A, B>(root:keyPath:)
  %14 = function_ref @$Ss23_projectKeyPathWritable4root03keyC0Spyq_G_yXlSgtSpyxG_s0dbC0Cyxq_Gtr0_lF : $@convention(thin) <?_0_0, ?_0_1> (UnsafeMutablePointer<?_0_0>, @guaranteed WritableKeyPath<?_0_0, ?_0_1>) -> (UnsafeMutablePointer<?_0_1>, @owned Optional<AnyObject>) // user: %18
  retain_value %7 : $String                       // id: %15
  retain_value %8 : $String                       // id: %16
  strong_retain %0 : $WritableKeyPath<SomeStruct, Value> // id: %17
  %18 = apply %14<SomeStruct, Value>(%13, %0) : $@convention(thin) <?_0_0, ?_0_1> (UnsafeMutablePointer<?_0_0>, @guaranteed WritableKeyPath<?_0_0, ?_0_1>) -> (UnsafeMutablePointer<?_0_1>, @owned Optional<AnyObject>) // users: %25, %19, %20
  %19 = tuple_extract %18 : $(UnsafeMutablePointer<Value>, Optional<AnyObject>), 0 // user: %21
  %20 = tuple_extract %18 : $(UnsafeMutablePointer<Value>, Optional<AnyObject>), 1 // user: %23
  %21 = struct_extract %19 : $UnsafeMutablePointer<Value>, #UnsafeMutablePointer._rawValue // user: %22
  %22 = pointer_to_address %21 : $Builtin.RawPointer to [strict] $*Value // user: %23
  %23 = mark_dependence %22 : $*Value on %20 : $Optional<AnyObject> // user: %24
  copy_addr [take] %10 to %23 : $*Value           // id: %24
  release_value %18 : $(UnsafeMutablePointer<Value>, Optional<AnyObject>) // id: %25
  strong_release %0 : $WritableKeyPath<SomeStruct, Value> // id: %26
  dealloc_stack %10 : $*Value                     // id: %27
  %28 = load %6 : $*SomeStruct                    // user: %30
  dealloc_stack %6 : $*SomeStruct                 // id: %29
  return %28 : $SomeStruct                        // id: %30
} // end sil function '$S1x10SomeStructV7setting_2toACs15WritableKeyPathCyACxG_xtlF'
Run Code Online (Sandbox Code Playgroud)

特别感兴趣的是这一部分:

%6 = alloc_stack $SomeStruct, var, name "copy"  // users: %12, %28, %9, %29
%7 = struct_extract %2 : $SomeStruct, #SomeStruct.name // user: %15
%8 = struct_extract %2 : $SomeStruct, #SomeStruct.date // user: %16
store %2 to %6 : $*SomeStruct                   // id: %9
Run Code Online (Sandbox Code Playgroud)

正如所料,每次调用时都会创建一个新副本setting.

IMO,Swift中更好的方法是:

let myStruct: SomeStruct = { 
    var s = SomeStruct()
    s.name = "Fogmeister"
    s.number = 42
    s.date = "yesterday"
    return s
}()
Run Code Online (Sandbox Code Playgroud)

这优化到以下(加上我的注释):

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):

  # allocate some storage for myStruct as a global
  alloc_global @$S1x8myStructAA04SomeB0Vvp        // id: %2
  %3 = global_addr @$S1x8myStructAA04SomeB0Vvp : $*SomeStruct // user: %23

  # Construct the tagged string value for "Fogmeister"
  %4 = integer_literal $Builtin.Int64, 8391166415069474630 // user: %9
  %5 = integer_literal $Builtin.Int64, -1585267068834385307 // user: %6
  %6 = struct $UInt (%5 : $Builtin.Int64)         // user: %7
  %7 = value_to_bridge_object %6 : $UInt          // user: %8
  %8 = struct $_StringObject (%7 : $Builtin.BridgeObject) // user: %10
  %9 = struct $UInt (%4 : $Builtin.Int64)         // user: %10
  %10 = struct $_StringGuts (%8 : $_StringObject, %9 : $UInt) // user: %11
  %11 = struct $String (%10 : $_StringGuts)       // user: %22

  # Construct the 42
  %12 = integer_literal $Builtin.Int64, 42        // user: %13
  %13 = struct $Int (%12 : $Builtin.Int64)        // user: %22

  # Construct the tagged string for "yesterday"
  %14 = integer_literal $Builtin.Int64, -1657324662872342407 // user: %15
  %15 = struct $UInt (%14 : $Builtin.Int64)       // user: %16
  %16 = value_to_bridge_object %15 : $UInt        // user: %18
  %17 = integer_literal $Builtin.Int64, 7017859899421058425 // user: %19
  %18 = struct $_StringObject (%16 : $Builtin.BridgeObject) // user: %20
  %19 = struct $UInt (%17 : $Builtin.Int64)       // user: %20
  %20 = struct $_StringGuts (%18 : $_StringObject, %19 : $UInt) // user: %21
  %21 = struct $String (%20 : $_StringGuts)       // user: %22

  # init SomeStruct and store it in our global
  %22 = struct $SomeStruct (%11 : $String, %13 : $Int, %21 : $String) // user: %23
  store %22 to %3 : $*SomeStruct                  // id: %23

  # Return 0 (cause it's main)
  %24 = integer_literal $Builtin.Int32, 0         // user: %25
  %25 = struct $Int32 (%24 : $Builtin.Int32)      // user: %26
  return %25 : $Int32                             // id: %26
} // end sil function 'main'
Run Code Online (Sandbox Code Playgroud)

你在这里会注意到,闭包执行已经完全优化了.编译器能够将"Fogmeister"和"昨天"减少到它们的标记字符串值,并将整个块减少为单个init调用(在%22),因为它注意到我正在设置所有值.棒极了.

  • 太棒了,谢谢:DI 不知道如何以这种方式编译,但我自己做了一下,以便进一步了解它。有趣,谢谢。 (2认同)