Я реализовывал алгоритм в 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++, но не 10-кратное замедление по сравнению с чистым Python.
Изменить: mweathers заметил, что изменение -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 уже проверяет границы массивов, и в типичных случаях замедление в -ftrapv
раза меньше. А в 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]
}
}
Здесь нет арифметики, поэтому нам не нужно беспокоиться о целочисленных переполнениях. Единственное, что мы делаем, это просто множество ссылок на массивы. И результаты здесь - Свифт -O3 теряет почти в 500 раз по сравнению с -Ofast:
- C++ -O3: 0,05 с
- C++ -O0: 0,4 с
- Ява: 0,2 с
- Питон с PyPy: 0,5 с
- Питон: 12 с
- Свифт -Ofast: 0,05 с
- Свифт -O3: 23 с
- Свифт -O0: 443 с
(Если вы обеспокоены тем, что компилятор может полностью оптимизировать бессмысленные циклы, вы можете изменить его, например, на x[i] ^= x[j]
, и добавить оператор print, который выводит x[0]
. Это ничего не меняет; сроки будут очень похожи.)
И да, здесь реализация Python была глупой чистой реализацией Python со списком целых и вложенных в циклы. Это должно быть намного медленнее, чем неоптимизированный Свифт. Кажется, что-то серьезно сломано с помощью Swift и индексации массивов.
Редактировать 4: Эти проблемы (а также некоторые другие проблемы с производительностью), кажется, были исправлены в Xcode 6 beta 5.
Для сортировки у меня теперь есть следующие тайминги:
- clang++ -O3: 0,06 с
- swiftc -Ofast: 0,1 с
- swiftc -O: 0,1 с
- swiftc: 4 с
Для вложенных циклов:
- clang++ -O3: 0,06 с
- swiftc -Ofast: 0,3 с
- swiftc -O: 0,4 с
- swiftc: 540 с
Кажется, что больше нет причин использовать небезопасный -Ofast
(он же -Ounchecked
); -O
производит одинаково хороший код.