Как доказать копирование при записи для типа String в Swift

Как сказано в заголовке, я пытался доказать себе, что COW (копирование при записи) поддерживается для String в Swift. Но не могу найти доказательства. Я доказал COW на массиве и словаре, попробовав следующие коды:

func address(of object: UnsafeRawPointer) -> String {
    let addr = Int(bitPattern: object)
    return String(format: "%p", addr)
}

var xArray = [20, 30, 40, 50, 60]
var yArray = xArray

// These two addresses were the same
address(of: xArray) 
address(of: yArray)

yArray[0] = 200
// The address of yArray got changed
address(of: yArray)

Но для типа String это не сработало.

var xString = "Hello World"
var yString = xString

// These two addresses were different
address(of: xString)
address(of: yString)

И я выгрузил тестовую функцию из официального репозитория кода Swift.

func _rawIdentifier(s: String) -> (UInt, UInt) {
    let tripe = unsafeBitCast(s, to: (UInt, UInt, UInt).self)
    let minusCount = (tripe.0, tripe.2)
    return minusCount
}

Но эта функция, похоже, передает только фактическое значение, указанное на адрес, а не на него. Таким образом, две разные строковые переменные с одинаковым значением будут иметь один и тот же rawIdentifier. До сих пор не могу доказать мне COW.

var xString = "Hello World"
var yString = "Hello" + " World" 

// These two rawIdentifiers were the same
_rawIdentifier(s: xString)
_rawIdentifier(s: yString)

Так как же COW работает со строковым типом в Swift?


person Wu_    schedule 14.10.2017    source источник
comment
Вы можете просто посмотреть исходный код: github.com /apple/swift/blob/master/stdlib/public/core/   -  person Palle    schedule 14.10.2017
comment
По-видимому, компилятор распознает, что Hello World и Hello + World — это один и тот же строковый литерал, и создает для них только одно хранилище. Попробуйте сделать то же самое с другими строками.   -  person Martin R    schedule 14.10.2017
comment
Компилятор выполняет свертывание констант даже в неоптимизированных сборках (это так называемая гарантированную оптимизацию), поэтому, как говорит Мартин, "Hello" + " World" складывается в "Hello World". Вы также можете сделать var yString = "Hello"; yString += " World", чтобы заметить разницу (в этом случае буфер также получит владельца, поскольку теперь он распределяется динамически, а не статически)   -  person Hamish    schedule 14.10.2017
comment
Предупреждение о _rawIdentifier: использование обходов unsafeBitCast сохраняет операции подсчета, поэтому вполне возможно, что вы будете смотреть на оборванные указатели (т. е. строковый буфер освобождается, пока вы все еще смотрите на строковое значение) — пытаясь разыменовать тогда будет неопределенное поведение. В коде, из которого вы это взяли, вызывающие использовали _fixLifetime, чтобы гарантировать, что этого не произойдет. Вы также можете использовать withExtendedLifetime(_:_:), чтобы убедиться в этом (или использовать UnsafeMutablePointer и перепривязать память).   -  person Hamish    schedule 14.10.2017
comment
@Hamish: Но _rawIdentifier() не разыменовывает указатель или делает?   -  person Martin R    schedule 14.10.2017
comment
@MartinR Это не так; но OP должен быть осторожен в том, что он делает с результатом (т.е. не использовать значения битовых шаблонов для UnsafePointers, а затем пытаться проверить указатели). Это было скорее заблаговременное предупреждение :)   -  person Hamish    schedule 14.10.2017
comment
@Palle, темно-синий, см., не могли бы вы указать, какой код искать?   -  person maddy    schedule 18.09.2018


Ответы (1)


Компилятор создает только одно хранилище для "Hello World" и "Hello" + " World".

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

swiftc -emit-assembly cow.swift

который определяет только один строковый литерал

    .section    __TEXT,__cstring,cstring_literals
L___unnamed_1:
    .asciz  "Hello World"

Как только строка видоизменяется, адрес буфера хранения строк (первый член этого «волшебного» кортежа, на самом деле _baseAddress из struct _StringCore, определенный в StringCore.swift):

var xString = "Hello World"
var yString = "Hello" + " World"

print(_rawIdentifier(s: xString)) // (4300325536, 0)
print(_rawIdentifier(s: yString)) // (4300325536, 0)

yString.append("!")
print(_rawIdentifier(s: yString)) // (4322384560, 4322384528)

И почему ваш

func address(of object: UnsafeRawPointer) -> String

показывает те же значения для xArray и yArray, но не для xString и yString?

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

При передаче строки функции, принимающей небезопасный указатель, передается указатель на временное представление строки UTF-8. Этот адрес может быть разным в каждом вызове, даже для одной и той же строки.

Это поведение задокументировано в справочнике «Использование Swift с Cocoa и Objective-C» для UnsafePointer<T> аргументов, но, по-видимому, работает так же и для UnsafeRawPointer аргументов.

person Martin R    schedule 14.10.2017
comment
Для всех, кого интересует второе число в кортеже и почему оно изменяется при добавлении: это указатель на владельца строкового буфера. У буфера есть владелец, когда он выделяется динамически (но не когда выделяется статически, как в случае со строковым литералом). Владелец — это просто экземпляр класса, который заботится о подсчете ссылок для буфера. Когда владелец освобождается, буфер освобождается. - person Hamish; 14.10.2017
comment
Я считаю, что под капотом собственного хранилища один экземпляр ManagedBuffer используется для буфера динамических строк; поэтому необработанный указатель буфера (первый элемент кортежа) просто указывает на начало тела этого экземпляра, а владелец указывает на начало заголовка. - person Hamish; 14.10.2017