Wdrażałem algorytm w Swift Beta i zauważyłem, że wydajność była bardzo słaba. Po głębszym kopaniu zdałem sobie sprawę, że jednym z wąskich gardeł było coś tak prostego, jak sortowanie tablic. Odpowiednia część znajduje się tutaj:
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
W C ++ podobna operacja zajmuje 0,06 s na moim komputerze.
W Pythonie zajmuje 0,6 s (bez lew, tylko y = sorted (x) dla listy liczb całkowitych).
W Swift zajmuje 6s, jeśli skompiluję go za pomocą następującego polecenia:
xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`
A kompilacja za pomocą następującego polecenia zajmuje aż 88 sekund :
xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx`
Czasy w Xcode z kompilacjami „Release” vs. „Debug” są podobne.
Co tu jest nie tak? Mogłem zrozumieć pewną utratę wydajności w porównaniu z C ++, ale nie 10-krotne spowolnienie w porównaniu z czystym Pythonem.
Edycja: pogoda zauważyła, że zmiana -O3
na -Ofast
ten powoduje, że ten kod działa prawie tak szybko, jak wersja C ++! Jednak bardzo -Ofast
zmienia semantykę języka - w moich testach wyłączyłem sprawdzanie przepełnień liczb całkowitych i przepełnienia indeksowania tablic . Na przykład -Ofast
następujący kod Swift działa w trybie cichym bez awarii (i drukuje śmieci):
let n = 10000000
print(n*n*n*n*n)
let x = [Int](repeating: 10, count: n)
print(x[n])
Więc -Ofast
nie tego chcemy; chodzi o to, że mamy siatki bezpieczeństwa. Oczywiście siatki bezpieczeństwa mają pewien wpływ na wydajność, ale nie powinny powodować, że programy będą 100 razy wolniejsze. Pamiętaj, że Java już sprawdza granice tablic, a w typowych przypadkach spowolnienie jest znacznie mniejsze niż 2. A w Clang i GCC mamy -ftrapv
do sprawdzania (podpisanych) przepełnień liczb całkowitych, i to nie jest tak powolne.
Stąd pytanie: w jaki sposób możemy uzyskać rozsądną wydajność w Swift bez utraty sieci bezpieczeństwa?
Edycja 2: Zrobiłem trochę więcej testów porównawczych, z bardzo prostymi pętlami wzdłuż linii
for i in 0..<n {
x[i] = x[i] ^ 12345678
}
(Tutaj jest operacja xor, aby łatwiej znaleźć odpowiednią pętlę w kodzie asemblera. Próbowałem wybrać operację, która jest łatwa do wykrycia, ale także „nieszkodliwa” w tym sensie, że nie powinna wymagać żadnych kontroli związanych z do przelewów liczb całkowitych).
Ponownie, była ogromna różnica w wydajności pomiędzy -O3
i -Ofast
. Spojrzałem więc na kod asemblera:
Z
-Ofast
tym dostaję prawie to, czego bym się spodziewał. Odpowiednia część to pętla z 5 instrukcjami języka maszynowego.Z
-O3
I dostać coś, co było poza moim najdzikszych fantazji. Pętla wewnętrzna obejmuje 88 linii kodu asemblera. Nie próbowałem tego wszystkiego zrozumieć, ale najbardziej podejrzane są 13 wywołań „callq _swift_retain” i kolejne 13 wywołań „callq _swift_release”. To znaczy, 26 wywołań podprogramów w wewnętrznej pętli !
Edycja 3: W komentarzach Ferruccio poprosił o testy porównawcze, które są uczciwe w tym sensie, że nie polegają na wbudowanych funkcjach (np. Sortowanie). Myślę, że następujący program jest dość dobrym przykładem:
let n = 10000
var x = [Int](repeating: 1, count: n)
for i in 0..<n {
for j in 0..<n {
x[i] = x[j]
}
}
Nie ma arytmetyki, więc nie musimy się martwić o przepełnienie liczb całkowitych. Jedyne, co robimy, to po prostu wiele odniesień do tablicy. A wyniki są tutaj - Swift -O3 traci prawie 500 razy w porównaniu z opcją -Ofast:
- C ++ -O3: 0,05 s
- C ++ -O0: 0,4 s
- Java: 0,2 s
- Python z PyPy: 0,5 s
- Python: 12 s
- Szybki - szybki: 0,05 s
- Swift -O3: 23 s
- Swift -O0: 443 s
(Jeśli obawiasz się, że kompilator może całkowicie zoptymalizować niepotrzebne pętle, możesz go zmienić na np. x[i] ^= x[j]
I dodać instrukcję print, która wypisuje wynik x[0]
. To niczego nie zmienia; czasy będą bardzo podobne.)
I tak, tutaj implementacja Pythona była głupią czystą implementacją Pythona z listą liczb int i zagnieżdżoną dla pętli. Powinien być znacznie wolniejszy niż niezoptymalizowany Swift. Wydaje się, że coś jest poważnie zepsute w Swift i indeksowaniu tablic.
Edycja 4: Te problemy (podobnie jak niektóre inne problemy z wydajnością) zostały prawdopodobnie rozwiązane w Xcode 6 beta 5.
Do sortowania mam teraz następujące czasy:
- klang ++ -O3: 0,06 s
- swiftc -Ofast: 0,1 s
- swiftc -O: 0,1 s
- swiftc: 4 s
W przypadku zagnieżdżonych pętli:
- klang ++ -O3: 0,06 s
- swiftc -Ofast: 0,3 s
- swiftc -O: 0,4 s
- swiftc: 540 s
Wydaje się, że nie ma już powodu, aby używać tego niebezpiecznego -Ofast
(aka -Ounchecked
); zwykły -O
tworzy równie dobry kod.
xcrun --sdk macosx swift -O3
. Jest krótszy