Какое копирование-элизия делает Swift для структур?

Общий консенсус в отношении программирования на Swift (по состоянию на май 2018 г., Swift 4.1, Xcode 9.3) заключается в том, что следует отдавать предпочтение структурам, если только ваша логика явно не требует общей ссылки на объект.

Как мы знаем, проблема со структурами заключается в том, что они передаются по значению, поэтому при передаче структуры в функцию или возврате из нее создается копия. Если у вас большая структура (скажем, с 12 свойствами), то такое копирование может дорого обойтись.

Это обычно защищают люди, говорящие, что быстрый компилятор и/или LLVM могут исключать копии (т. е. передавать ссылку на структуру, а не копировать ее) и должны делать копию только в том случае, если вы действительно изменяете структуру.

Это все хорошо, но об этом всегда говорят в теоретических терминах: «В качестве оптимизации LLVM может исключить копии» и тому подобное.

Мой вопрос: кто-нибудь может сказать нам, что на самом деле происходит? Действительно ли компилятор игнорирует копии или это просто теоретическая будущая оптимизация, которая может когда-нибудь появиться? (Например, компилятор C# теоретически может исключать копии структур, но на самом деле он никогда этого не делает, и Microsoft рекомендует не использовать структуры для объектов размером более 16 байт [1])

Если Swift действительно удаляет копии структур, есть ли какое-то объяснение или эвристика относительно того, делает ли это и когда?

Примечание. Я говорю о пользовательских структурах, а не о встроенных в stdlib вещах, таких как массивы и словари.

[1] https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct


person Orion Edwards    schedule 16.05.2018    source источник
comment
Поскольку здесь вы имеете в виду термин С++, я укажу, что копии С++ могут быть на несколько порядков больше, чем копии Swift. Стоимость копирования вектора 10 000 нетривиальных объектов в C++ превысит стоимость копирования большой структуры из 12 свойств.   -  person zneak    schedule 16.05.2018
comment
Компилятор также может специализировать функции, которые принимают параметры типа значения, так что передаются только те свойства, которые используются в теле функции, сравните stackoverflow.com /q/43486408/2976878   -  person Hamish    schedule 16.05.2018


Ответы (1)


Во-первых, Swift не использует соглашение о вызовах платформы. В macOS C, C++ и Objective-C используют x86_64 System V ABI, а Swift — нет. Заметным изменением является то, что CC Swift имеет четыре обратных GPR (rax, rdx, rcx, r8) вместо двух.

Это почти наверняка становится более сложным, когда вы смешиваете числа с плавающей запятой, но если вы используете все целые и целочисленные типы (например, указатели), структуры передаются и возвращаются регистром, копированием, если они помещаются в ширину at большинство 4 регистров. Кроме того, структуры передаются и возвращаются по адресу. В случае возвращаемого значения вызывающий объект отвечает за настройку пространства стека и передачу адреса этого пространства вызываемому в качестве скрытого параметра.

Поскольку Swift ABI не доработан, возможно, он все еще может быть изменен.

Однако простая передача указателей не означает, что копий не происходит. Например:

public class Let {
    let large: Large

    init(large: Large) {
        self.large = large
    }
}

public func withLet(l: Let) {
    doSomething(foo: l.large)
}

В этом примере в -O на Swift 4.1 withLet идет на следующий компромисс:

  • l.large копируется в локальную временную
  • l освобождается после копирования и до вызова doSomething

Копия была бы неизбежна с изменяемым или вычисляемым свойством (поскольку их значение может меняться в течение продолжительности вызова), но я полагаю, что let константы могут быть переданы по адресу напрямую. Однако в этом случае l придется остаться в живых до тех пор, пока doSomething не вернется.

person zneak    schedule 16.05.2018
comment
Ваш ответ подразумевает, что для неизменяемых структур размером более 4 регистров они не копируются при передаче в функцию, но прямо не говорит об этом - я правильно понимаю? - person Orion Edwards; 17.05.2018
comment
@OrionEdwards, я этого не говорил, потому что вторая половина ответа сильно модулирует его. На практике, хотя есть свидетельства того, что он пытается минимизировать их, трудно предсказать, сделает ли компилятор копию или нет. В в этом примере в Swift 4.1 large создается один раз и передается по адресу непосредственно foo, что не не копировать его. Однако в другом подобном примере компилятор создает новую копию large, просто изменить первое поле перед передачей его второму вызову foo. - person zneak; 17.05.2018
comment
(Я должен уточнить, что вторая копия создается из константного кортежа (5, 6, 5, 4, 3, 2, 1, 0), а не из другой структуры. Это наводит меня на мысль, что это следствие формы SSA, которую использует LLVM, но для меня она по-прежнему считается копией.) - person zneak; 17.05.2018