Создает ли мутирующая функция структуры в 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)
:
Существующая структура видоизменяется на месте:
Наша структура в памяти будет выглядеть так:
00000000 00000100 00000010 00000000 <------^ ^------^ ^------^ ^-----> other | self.x | self.y | other memory ^----------------^ the p struct
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
Учитывая, что адрес памяти не изменился, я уверен, что структура была видоизменена, а не заменена.
Чтобы полностью заменить что-либо, вы должны использовать другой адрес памяти, поэтому бессмысленно копировать объект обратно в исходный адрес памяти.