Размер быстрого перечисления, когда связанное значение является ссылочным типом - PullRequest
4 голосов
/ 28 мая 2020
• 1000 *, medium = 1 и так далее. Итак, размер Size составляет 1 байт, что можно проверить с помощью MemoryLayout<Size>.size. Я также отметил, что если перечисление имеет более 255 случаев, очевидно, что размер тега увеличивается до 2 байтов.
enum Size {
    case small
    case medium
    case large
}

Второй случай, если перечисление имеет связанные значения, оно ведет себя как объединение. В этом случае размер перечисления - это размер тега плюс размер наибольшего связанного значения. В следующем примере размер составляет 1 байт + 16 байтов (String), то есть 17 байтов, что также можно проверить с помощью MemoryLayout.

enum Value {
    case int(Int)
    case double(Double)
    case string(String)
    case bool(Bool)
}

Последний случай, поскольку Swift является безопасным языком, ссылки всегда действителен с использованием стандартного небезопасного кода Swift, т.е. всегда указывает на значение в памяти. Это позволяет компилятору оптимизировать такое перечисление, когда T является ссылочным типом:

enum Opt<T> {
    case none
    case some(T)
}

Здесь экземпляр типа T не может быть nil (NULL), поэтому компилятор использует это специальное значение для случай none, следовательно, Opt имеет размер 8 байтов вместо 9 байтов, когда T является ссылочным типом. Эта оптимизация поднята в this SO вопрос о Rust, который, как я считаю, имеет такое же поведение, как Swift в отношении перечислений.

Например, с этим простым ссылочным типом MemoryLayout возвращает размер 8 байтов:

class Person {
    var name: String

    init(name: String) {
        self.name = name
    }
}

let p = Opt.some(Person(name: "Bob"))  // 8 bytes

Вопрос

То, что я не могу понять, так это размер этого перечисления (все еще когда T является ссылочным типом):

enum Opt<T> {
    case none
    case secondNone
    case some(T)
}

Почему это тоже 8 байтов, согласно MemoryLayout?

В моем понимании это должно быть 9 байтов. Оптимизация NULL возможна только потому, что none может быть представлено NULL, но в моем примере нет «второго» значения NULL для secondNone, поэтому здесь должен потребоваться тег для различения случаев.

Компилятор автоматически превращает это перечисление в ссылочный тип (аналогично перечислению indirect) из-за этого? Это объяснило бы размер 8 байтов. Как я могу проверить эту последнюю гипотезу?

1 Ответ

1 голос
/ 28 мая 2020

From Тип макета: перечисления с одиночной полезной нагрузкой :

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

Ваш пример с большим количеством случаев:

enum Opt<T> {
    case a, b, c, d, e, f, g, h, i, j, k
    case l, m, n, o, p, q, r, s, t, u, v
    case some(T)
}

class Person {
    var name: String
    init(name: String) { self.name = name }
}

print(unsafeBitCast(Opt<Person>.a, to: UnsafeRawPointer.self))
// 0x0000000000000000

print(unsafeBitCast(Opt<Person>.b, to: UnsafeRawPointer.self))
// 0x0000000000000002

print(unsafeBitCast(Opt<Person>.v, to: UnsafeRawPointer.self))
// 0x000000000000002a

let p = Person(name: "Bob")
print(unsafeBitCast(Opt.some(p), to: UnsafeRawPointer.self))
// 0x00006030000435d0

По-видимому, 0x0, 0x2, ..., 0x2a являются недопустимыми битовыми шаблонами для указателя и поэтому используются для дополнительных случаев.

Точный алгоритм кажется недокументированным, вероятно, придется проверить исходный код компилятора Swift.

...