Пост основан на статье medium.com/@RobertGummesson/regarding-swift-build-time-optimizations-fc92cdd91e31 + небольшие изменения/дополнения, то есть текст идет от моего лица, не третьего.

Всем знакома ситуация после изменения файла(ов) или просто переоткрытия проекта:






Нужно сказать, что какой-то особой проблемы с компиляцией я не замечал когда писал на Objective-C, но всё изменилось с приходом в массы swift. Я частично отвечал за CI сервер по сборке iOS проектов. Так вот, проекты на swift собирались невыносимо медленно. Еще разработчики любят тащить поды (cocoapods зависимости) на каждый чих в свои swift проекты, иногда, в безумном количестве. Так вот, компиляция всей этой смеси могла продолжаться несколько минут, хотя сам проект состоял буквально из пары десятков классов. Ну ладно, как бы ясно, язык новый, на этапе компиляции происходит намного больше проверок, чем в том же ObjC, глупо ожидать, что она будет работать быстрее (да, swift это строго типизированный язык и всё должно быть максимально явно объявлено (как в Java, например), в отличии от ObjC, где не всё так строго). Разработчики swift с каждым релизом обещают в x раз ускорения скорости компиляции. Ну вроде, действительно, сборка 2.0 и уже потом 2.2 стала работать быстрее, вот вот на носу уже 3.0 версия (конец года).

Компилятор компилятором, но оказывается, что время сборки проекта можно катастрофически увеличить буквально несколькими строчками кода. Часть примеров взято из вышеупомянутой статьи, часть самописные (не поверил автору и хотелось самому проверить). Время замерял через

time swiftc -Onone file.swift


Список того, что может замедлить ваш компилятор



Неосторожное использование Nil Coalescing Operator



func defIfNil(string: String?) -> String {
	return string ?? "default value"
}


Оператор ?? разворачивает Optional, если там что-то есть, то возвращает значение иначе выполняется выражение после ??. Если связать подряд несколько этих операторов, то можем получить увеличение времени компиляции на порядок (не ошибся, на порядок :) ).

// Без оператора компиляция занимает 0,09 секунд
func fn() -> Int {
    
    let a: Int? = nil
    let b: Int? = nil
    let c: Int? = nil
    
    var res: Int = 999
    
    if let a = a {
        res += a
    }
    
    if let b = b {
        res += b
    }
    
    if let c = c {
        res += c
    }
    
    return res
}  


// С операторами 3,65 секунд
func fn() -> Int {
    
    let a: Int? = nil
    let b: Int? = nil
    let c: Int? = nil
    
    return 999 + (a ?? 0) + (b ?? 0) + (c ?? 0)
}


Да, в 40 раз :). Но мои наблюдения показали, что проблема возникает, только если использовать больше двух таких операторов в одном выражении, то есть:

return 999 + (a ?? 0) + (b ?? 0)

уже будет ок.

Объединение массивов



Я иногда пишу скрипты на руби, руби потрясающий язык, где большое количество операций можно уложить в пару строк кода, без потери читабельности. Особенно нравится работать с массивами. То есть, это вполне обычный код на руби для меня:

res = ar1.filter { ... } + ar2.map { ... } + ar3.flatMap { ... }


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

// Время 0,15 секунд
func fn() -> [Int] {
    let ar1 = (1...50).map { $0 }.filter { $0 % 2 == 0 }
    let ar2 = [4, 8, 15, 16, 23, 42].map { $0 * 2 }
    
    var ar3 = (1..<20).map { $0 }
    
    ar3.appendContentsOf(ar1)
    ar3.appendContentsOf(ar2)
    
    return ar3
}


// Время 2,86 секунд
func fn() -> [Int] {
    let ar1 = (1...50).map { $0 }
    let ar2 = [4, 8, 15, 16, 23, 42]
    
    return (1..<20).map { $0 } + ar1.filter { $0 % 2 == 0 } + ar2.map { $0 * 2 }
}


Разница почти в 20 раз. А ведь с массивами мы работаем в каждом втором методе. Но стоит отметить, такое поведение компилятора я получил, если суммирую больше чем два массива в одном выражении, то есть:

return ar1.filter { $0 % 2 == 0 } + ar2.map { $0 * 2 }

уже ок.

Ternary operator



Автор оригинального поста приводит пример, из которого следует, что не стоит использовать сложные выражения вместе с тернарным оператором:

// Build time: 0,24 секунды
let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}

// Build time: 0,017 секунд
var labelNames: [String]
if type == 0 {
    labelNames = (1...5).map{type0ToString($0)}
} else {
    labelNames = (0...2).map{type1ToString($0)}
}


Избыточные касты



Тут автор просто убрал лишние касты в CGFloat и получил существенную скорость в компиляции файла:

// Build time: 3,43 секунды
return CGFloat(M_PI) * (CGFloat((hour + hourDelta + CGFloat(minute + minuteDelta) / 60) * 5) - 15) * unit / 180

// Build time: 0,003 секунды
return CGFloat(M_PI) * ((hour + hourDelta + (minute + minuteDelta) / 60) * 5 - 15) * unit / 180


Сложные выражения



Тут автор имеет ввиду, смесь локальных переменных, переменных класса и инстансов класса вместе с функциями:

// Build time: 1,43 секунды
let expansion = a - b - c + round(d * 0.66) + e

// Build time: 0,035 секунд
let expansion = a - b - c + d * 0.66 + e


Правда тут автор оригинального поста так и не понял почему так.




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

пс.

Один из разработчиков swift языка дал небольшой ответ на этот пост оригинальному автору, что в swift 3 сделано много улучшений в эту сторону:
Поделиться с друзьями
-->

Комментарии (27)


  1. a553
    07.05.2016 13:23
    +7

    А как же любимый вариант:

    let myCompany = [
       "employees": [
            "employee 1": ["attribute": "value"],
            "employee 2": ["attribute": "value"],
            "employee 3": ["attribute": "value"],
            "employee 4": ["attribute": "value"],
            "employee 5": ["attribute": "value"],
            "employee 6": ["attribute": "value"],
            "employee 7": ["attribute": "value"],
            "employee 8": ["attribute": "value"],
            "employee 9": ["attribute": "value"],
            "employee 10": ["attribute": "value"],
            "employee 11": ["attribute": "value"],
            "employee 12": ["attribute": "value"],
            "employee 13": ["attribute": "value"],
            "employee 14": ["attribute": "value"],
            "employee 15": ["attribute": "value"],
            "employee 16": ["attribute": "value"],
            "employee 17": ["attribute": "value"],
            "employee 18": ["attribute": "value"],
            "employee 19": ["attribute": "value"],
            "employee 20": ["attribute": "value"],
        ]
    ]
    


    1. sleeply4cat
      07.05.2016 16:43

      А что не так с этим вариантом в плане времени компиляции?..


      1. a553
        07.05.2016 16:46
        +14

        Ну например то, что этот кусок кода компилируется 12 часов.


        1. int00h
          08.05.2016 10:44
          +1

          > Ну например то, что этот кусок кода компилируется 12 часов.
          Точнее компилировался Resolved: 26 Apr 2016


          1. Speakus
            08.05.2016 20:56
            +1

            Точнее компилируется. Xcode 7.3.1 (последняя на данный момент версия, опубликована 3 мая) всё ещё зависает на компиляции такого кода. А откуда у Вас инфа про resolved?


            1. encyclopedist
              08.05.2016 21:13

              https://github.com/apple/swift/commit/2cdd7d64e1e2add7bcfd5452d36e7f5fc6c86a03


              Если пройти по ссылке в комментарии a553, то там вся нужная информация.


              1. encyclopedist
                08.05.2016 21:17

    1. house2008
      07.05.2016 16:47

      Да) был подобный случай, только на ObjC, на CI сервере кланг завис минут на 30, потом в сегфолт по памяти свалился :) А самое интересное было, что это только в релиз режиме (xcodebuild -configuration Release) происходило, то есть у разработчика на машине работает всё окей, но на сервере падает.


  1. EGlaz
    07.05.2016 15:55
    +2

    Интересно как влияет синтаксис на время работы приложения. Если компиляция дольше, но работа быстрее — оно того стоит.


    1. khim
      08.05.2016 17:02
      +4

      Когда как. Я как-то занимался тем, что переделал компонент так, что он стал работать в 10 раз быстрее — но стал компилироваться 30 минут. Всё бы ещё ничего и можно было бы с этим жить, но выяснилось, что «малые шевеления» кода могут его ускорять/замедлять вдвое-вдрое (естественно без выдачи каких-либо сообщений куда либо… лишнее это), а заметить это можно только по резкому ускорению времени компиляции (похоже после перехода через некий внутренний порог MSVC просто решал «а, фиг с ним — к чёрту оптимизации, они только мешают»)!

      В общем пришлось всё переделать и замедлить код в 1.5 раза — но сделать так, чтобы он и с MSVC нормально работал, а не только с GCC/Clang'ом. Было обидно, да.

      P.S. Сейчас я бы, наверное, постарался убедить всех просто использовать clang, но несколько лет назад это выходом не было: он был тогда ещё слишком сырым под Win64…


    1. alien007
      09.05.2016 15:12
      +1

      Смысл тут наверное не столько в скорости работы приложения, сколько в обспечиваемой безопасности разрабатываемых на них программ (с малым ущербом для производительности). Rust, Go и Swift относятся к группе нового поколения memory-safe языков, которые обеспечивают дополнительный уровень безопасности во время компиляции (нарушение иммутабельности, spatial и temporal memory access violation), чего не было реализовано в том же C++. Время компиляции последнего кстати от этого тоже сильно страдает на больших проектах.


  1. stargazr
    07.05.2016 16:43
    +5

    Они наверняка на С++ ничего не писали… :)

    Какая-то сомнительная идея жертвовать выразительностью кода ради пары минут.
    Пример «гора if против ??» ужасен. Лучше покомпилировать еще минуту, чем наблюдать такое.

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


  1. vsb
    07.05.2016 17:48
    +3

    Скорее всего это баги компилятора. Нет никаких разумных причин так замедлять компиляцию в этих местах. Вообще Swift мне показался сделанным наспех. Концепция хороша, а реализация как у студенческой курсовой. Тот же Xcode вначале на некоторых участках кода падал каждые несколько секунд. Постепенно вылизывают. Думаю пока не время на него переходить, сырой продукт, лучше подождать лет 5, тем более, что Objective C с каждым релизом становится всё лучше.


    1. encyclopedist
      07.05.2016 20:44
      +2

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


    1. nickolaym
      07.05.2016 22:02

      Может, баги компилятора, а может, спецификация языка такая, что прямо заставляет компилятор налетать на какой-нибудь комбинаторный взрыв. Скажем, не тупо выводить тип выражения по правилам (как в С++ либо как в ML / Haskell), а пытаться найти сочетание, которое удовлетворит всех наилучшим образом — ну, не знаю, перебирать все известные числовые (в т.ч. плавающие) типы для каждого целочисленного литерала.

      (Теоретизирую, потому что сам в свифт нос не засовывал).


  1. AcidLynx
    07.05.2016 23:19
    -11

    Как и обычно. Чем проще контрукции использовать (if-else вместо тернарного оператора, например), то и компиляция быстрее происходит, и понятнее код становится.

    Вывод — пишите понятнее для себя и других. Тогда и компилятор не будет «зависать».


    1. Prototik
      07.05.2016 23:27
      +13

      Вывод здесь один — компилятор Swift дерьмо. Никто не должен как либо ограничивать меня или кого-то ещё в стиле программирования. Как хочу — так и пишу. Если время компиляции растёт в десятки раз от простейших вещей — то проблема не в этих простейших вещах, очевидно же.


      1. 6opoDuJIo
        08.05.2016 13:09

        Даже не так: если фича сделана через жопу, то и толк от этой фичи малоощутим. Что и наблюдается.


        1. khim
          08.05.2016 21:29
          +2

          Очень хорошо перекликается с соседней статьёй: разработчики компилятора Swift'а оправдываются тем, что у них гораздо более сложная задача и сделать её быстро — не так-то просто.

          Возможно. Но. Кто их просил делать такие фичи в языке, что использование простейших, совершенно не «узободробительных» конструкций выливается во многочасовою компиляцию? Марсиане?

          С++ тоже этим страдает во многом, но там всё-таки уж самые ужасные болячки победили, а тут… просто праздник какой-то…


  1. sergeylanz
    08.05.2016 12:39
    -4

    я думал swift когда нибуть на сервер придёт… но таким времени компиляции спасибо не надо, буду дальше на GO писать


  1. korniltsev
    08.05.2016 13:23
    +2

    Большие android проекты собираются в разы дольше. :( мои последние 2 проекта собираются(инкрементально — изменение одного java файла) 1м13с и 47с. И еще секунд 7 устанавливаются на девайс. И это на топовом макбуке.


    1. 6opoDuJIo
      08.05.2016 15:34

      Объём бы знать.


      1. korniltsev
        08.05.2016 16:22
        +1

        1м13с: 2560 классов не считая библиотек, Всего: 160k методов, 7.9M classes.dex, 8.5M classes2.dex, 3.4M classes3.dex.


        1. 6opoDuJIo
          08.05.2016 17:49

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


    1. DevAndrew
      09.05.2016 14:23

      Вы пробовали использовать Instant Run?
      В скорости компиляции моих проектов это очень помогло.


      1. korniltsev
        09.05.2016 14:44

        2 месяца назад не работал совсем. Сейчас проверил — работает. Изменение тела метода -> 17 секунд. Добавление метода — 27 секунд.


  1. XanKraegor
    08.05.2016 16:14

    Swift развивается, притом усилиями не только самой Apple, но и сообщества, и это хорошо. Например, до версии 2.2 даже простая арифметика типа
    let sunTrueLongitude: Double = meanAnomaly + 1.916 * sin(meanAnomaly * d2r) + 0.020 * sin(2 * meanAnomaly * d2r) + 282.634

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