Я реализовывал алгоритм в Swift Beta и заметил, что производительность была очень плохой. Покопавшись глубже, я понял, что одним из узких мест является нечто такое же простое, как сортировка массивов. Соответствующая часть здесь:
let n = 1000000
var x = [Int](repeating: 0, count: n)
for i in 0..<n {
x[i] = random()
}
// start clock here
let y = sort(x)
// stop clock here
В C ++ аналогичная операция занимает 0.06 с на моем компьютере.
В Python требуется 0,6 с (без трюков, просто y = sorted (x) для списка целых чисел).
В Swift это займет 6 секунд, если я скомпилирую его с помощью следующей команды:
xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`
И это займет целых 88 секунд, если я скомпилирую его с помощью следующей команды:
xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx`
Синхронизация в Xcode со сборками «Release» и «Debug» аналогична.
Что здесь не так? Я мог понять некоторую потерю производительности по сравнению с C ++, но не десятикратное замедление по сравнению с чистым Python.
Редактировать: погода заметила, что переход -O3
на -Ofast
этот код заставляет работать этот код почти так же быстро, как версия C ++! Однако -Ofast
семантика языка сильно меняется - в моем тестировании он отключил проверки на целочисленные переполнения и переполнения индексации массивов . Например, -Ofast
следующий код Swift работает без сбоев (и выводит мусор):
let n = 10000000
print(n*n*n*n*n)
let x = [Int](repeating: 10, count: n)
print(x[n])
Так -Ofast
не то, что мы хотим; Суть Swift в том, что у нас есть защитные сетки. Конечно, системы безопасности оказывают некоторое влияние на производительность, но они не должны делать программы в 100 раз медленнее. Помните, что Java уже проверяет границы массивов, и в типичных случаях замедление в два раза меньше. И в Clang и GCC мы получили -ftrapv
проверку целочисленных переполнений (со знаком), и это тоже не так медленно.
Отсюда вопрос: как мы можем получить разумную производительность в Swift, не теряя сетей безопасности?
Редактировать 2: я сделал еще несколько тестов, с очень простыми циклами по линии
for i in 0..<n {
x[i] = x[i] ^ 12345678
}
(Здесь операция xor есть просто для того, чтобы мне было легче найти соответствующий цикл в коде сборки. Я попытался выбрать операцию, которую легко обнаружить, но при этом «безвредную» в том смысле, что она не должна требовать каких-либо проверок, связанных с к целочисленным переполнениям.)
Опять же, была огромная разница в производительности между -O3
и -Ofast
. Итак, я посмотрел на код сборки:
С
-Ofast
я получаю в значительной степени то, что я ожидаю. Соответствующая часть представляет собой цикл с 5 инструкциями на машинном языке.Со
-O3
мной я получаю то, что было за пределами моего самого дикого воображения. Внутренний цикл занимает 88 строк кода сборки. Я не пытался понять все это, но наиболее подозрительными частями являются 13 вызовов "callq _swift_retain" и еще 13 вызовов "callq _swift_release". То есть 26 вызовов подпрограммы во внутреннем цикле !
Редактировать 3: В комментариях Ферруччо попросил критерии, которые являются справедливыми в том смысле, что они не полагаются на встроенные функции (например, сортировка). Я думаю, что следующая программа является довольно хорошим примером:
let n = 10000
var x = [Int](repeating: 1, count: n)
for i in 0..<n {
for j in 0..<n {
x[i] = x[j]
}
}
Здесь нет арифметики, поэтому нам не нужно беспокоиться о целочисленных переполнениях. Единственное, что мы делаем, это просто множество ссылок на массивы. И результаты здесь - Swift -O3 проигрывает почти в 500 раз по сравнению с -Ofast:
- C ++ -O3: 0,05 с
- C ++ -O0: 0,4 с
- Ява: 0,2 с
- Python с PyPy: 0,5 с
- Питон: 12 с
- Быстрое-быстрое: 0,05 с
- Swift -O3: 23 с
- Swift -O0: 443 с
(Если вы обеспокоены тем, что компилятор может полностью оптимизировать бессмысленные циклы, вы можете изменить его на eg x[i] ^= x[j]
и добавить оператор вывода print, который x[0]
ничего не изменит; время будет очень похоже.)
И да, здесь реализация Python была глупой чистой реализацией Python со списком целых и вложенных в циклы. Это должно быть намного медленнее, чем неоптимизированный Swift. Кажется, что-то серьезно сломано с помощью Swift и индексации массивов.
Редактировать 4: Эти проблемы (а также некоторые другие проблемы с производительностью), кажется, были исправлены в Xcode 6 бета 5.
Для сортировки у меня теперь есть следующие тайминги:
- лязг ++ -O3: 0,06 с
- swiftc -быстрый: 0,1 с
- swiftc -O: 0,1 с
- swiftc: 4 с
Для вложенных циклов:
- лязг ++ -O3: 0,06 с
- swiftc -быстрый: 0,3 с
- swiftc -O: 0,4 с
- swiftc: 540 с
Кажется, что больше нет причин использовать небезопасные -Ofast
(иначе -Ounchecked
); Обычный -O
выдает одинаково хороший код.
xcrun --sdk macosx swift -O3
. Это короче.