Создает ли мутирующая функция структуры в swift новую копию себя?

Мне нравится семантика значений в swift, но я беспокоюсь о производительности изменяющихся функций. Предположим, у нас есть следующее struct

struct Point {
   var x = 0.0
   mutating func add(_ t:Double){
      x += t
   }
}

Теперь предположим, что мы создаем Point и мутировать это так:

var p = Point()
p.add(1)

Теперь мутирует ли существующая структура в памяти или self заменен новым экземпляром, как в

self = Point(x:self.x+1)

3 ответа

Решение

Теперь мутирует ли существующая структура в памяти или заменяется новым экземпляром?

Концептуально эти два варианта абсолютно одинаковы. Я буду использовать этот пример структуры, которая использует UInt8 вместо Double (потому что его биты легче визуализировать).

struct Point {
    var x: UInt8
    var y: UInt8

    mutating func add(x: UInt8){
       self.x += x
    }
}

и предположим, что я создаю новый экземпляр этой структуры:

var p = Point(x: 1, y: 2)

Это статически выделяет некоторую память в стеке. Это будет выглядеть примерно так:

00000000  00000001  00000010  00000000
<------^  ^------^  ^------^ ^----->
other    | self.x | self.y | other memory
          ^----------------^
          the p struct

Посмотрим, что произойдет в обеих ситуациях, когда мы позвоним p.add(x: 3):

  1. Существующая структура видоизменяется на месте:

    Наша структура в памяти будет выглядеть так:

    00000000  00000100  00000010  00000000
    <------^  ^------^  ^------^ ^----->
    other    | self.x | self.y | other memory
            ^----------------^
            the p struct
    
  2. Self заменяется новым экземпляром:

    Наша структура в памяти будет выглядеть так:

    00000000  00000100  00000010  00000000
    <------^  ^------^  ^------^ ^----->
    other    | self.x | self.y | other memory
            ^----------------^
            the p struct
    

Обратите внимание, что нет никакой разницы между двумя сценариями. Это потому, что присвоение нового значения self вызывает мутацию на месте. p всегда одни и те же два байта памяти в стеке. Присвоение себе нового значения p будет заменять только содержимое этих 2 байтов, но это будут те же два байта.

Теперь между этими двумя сценариями может быть одно различие, которое касается любых возможных побочных эффектов инициализатора. Предположим, что это наша структура:

struct Point {
    var x: UInt8
    var y: UInt8

    init(x: UInt8, y: UInt8) {
        self.x = x
        self.y = y
        print("Init was run!")
    }

    mutating func add(x: UInt8){
       self.x += x
    }
}

Когда ты бежишь var p = Point(x: 1, y: 2)вы увидите, что Init was run! печатается (как и ожидалось). Но когда ты бежишь p.add(x: 3)вы увидите, что больше ничего не печатается. Это говорит нам о том, что инициализатор не заново.

Я чувствую, что стоит взглянуть (с достаточно высокого уровня) на то, что компилятор делает здесь. Если мы посмотрим на канонический SIL, испускаемый для:

struct Point {
    var x = 0.0
    mutating func add(_ t: Double){
        x += t
    }
}

var p = Point()
p.add(1)

Мы видим, что add(_:) метод получает как:

// Point.add(Double) -> ()
sil hidden @main.Point.add (Swift.Double) -> () :
           $@convention(method) (Double, @inout Point) -> () {
// %0                                             // users: %7, %2
// %1                                             // users: %4, %3
bb0(%0 : $Double, %1 : $*Point):

  // get address of the property 'x' within the point instance.
  %4 = struct_element_addr %1 : $*Point, #Point.x, loc "main.swift":14:9, scope 5 // user: %5

  // get address of the internal property '_value' within the Double instance.
  %5 = struct_element_addr %4 : $*Double, #Double._value, loc "main.swift":14:11, scope 5 // users: %9, %6

  // load the _value from the property address.
  %6 = load %5 : $*Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // user: %8

  // get the _value from the double passed into the method.
  %7 = struct_extract %0 : $Double, #Double._value, loc "main.swift":14:11, scope 5 // user: %8

  // apply a builtin floating point addition operation (this will be replaced by an 'fadd' instruction in IR gen).
  %8 = builtin "fadd_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // user: %9

  // store the result to the address of the _value property of 'x'.
  store %8 to %5 : $*Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // id: %9

  %10 = tuple (), loc "main.swift":14:11, scope 5
  %11 = tuple (), loc "main.swift":15:5, scope 5  // user: %12
  return %11 : $(), loc "main.swift":15:5, scope 5 // id: %12
} // end sil function 'main.Point.add (Swift.Double) -> ()'

(запустив xcrun swiftc -emit-sil main.swift | xcrun swift-demangle > main.silgen )

Здесь важно то, как Swift относится к неявному self параметр. Вы можете видеть, что он был испущен как @inout параметр, означающий, что он будет передан по ссылке в функцию.

Для того, чтобы выполнить мутацию x собственность, struct_element_addrИнструкция SIL используется для поиска своего адреса, а затем _value собственность Double, Результирующий double затем просто сохраняется по этому адресу с store инструкция.

Это означает, что add(_:) Метод может напрямую изменить значение p "s x свойство в памяти без создания каких-либо промежуточных экземпляров Point,

Я сделал это:

import Foundation

struct Point {
  var x = 0.0
  mutating func add(_ t:Double){
    x += t
  }
}

var p = Point()

withUnsafePointer(to: &p) {
  print("\(p) has address: \($0)")
}

p.add(1)

withUnsafePointer(to: &p) {
  print("\(p) has address: \($0)")
}

и получается в результате:

Точка (x: 0.0) имеет адрес: 0x000000010fc2fb80

Точка (x: 1.0) имеет адрес: 0x000000010fc2fb80

Учитывая, что адрес памяти не изменился, я уверен, что структура была видоизменена, а не заменена.

Чтобы полностью заменить что-либо, вы должны использовать другой адрес памяти, поэтому бессмысленно копировать объект обратно в исходный адрес памяти.

Другие вопросы по тегам