Estaba implementando un algoritmo en Swift Beta y noté que el rendimiento era muy pobre. Después de profundizar más, me di cuenta de que uno de los cuellos de botella era algo tan simple como clasificar los arreglos. La parte relevante está aquí:
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
En C ++, una operación similar toma 0.06s en mi computadora.
En Python, se necesitan 0.6s (sin trucos, solo y = ordenado (x) para obtener una lista de enteros).
En Swift toma 6 segundos si lo compilo con el siguiente comando:
xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`
Y toma tanto como 88s si lo compilo con el siguiente comando:
xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx`
Los tiempos en Xcode con las versiones "Release" vs. "Debug" son similares.
¿Que esta mal aquí? Podría entender alguna pérdida de rendimiento en comparación con C ++, pero no una desaceleración de 10 veces en comparación con Python puro.
Editar: el clima notó que cambiar -O3
a -Ofast
hace que este código se ejecute casi tan rápido como la versión C ++. Sin embargo, -Ofast
cambia mucho la semántica del lenguaje: en mis pruebas, deshabilitó las comprobaciones de desbordamientos de enteros y desbordamientos de indexación de matriz . Por ejemplo, con -Ofast
el siguiente código Swift se ejecuta silenciosamente sin fallar (e imprime algo de basura):
let n = 10000000
print(n*n*n*n*n)
let x = [Int](repeating: 10, count: n)
print(x[n])
Entonces -Ofast
no es lo que queremos; El punto de Swift es que tenemos las redes de seguridad en su lugar. Por supuesto, las redes de seguridad tienen cierto impacto en el rendimiento, pero no deberían hacer que los programas sean 100 veces más lentos. Recuerde que Java ya verifica los límites de la matriz, y en casos típicos, la desaceleración es por un factor mucho menor que 2. Y en Clang y GCC tenemos -ftrapv
para verificar desbordamientos de enteros (firmados), y tampoco es tan lento.
De ahí la pregunta: ¿cómo podemos obtener un rendimiento razonable en Swift sin perder las redes de seguridad?
Edición 2: hice más benchmarking, con bucles muy simples a lo largo de las líneas de
for i in 0..<n {
x[i] = x[i] ^ 12345678
}
(Aquí la operación xor está ahí solo para que pueda encontrar más fácilmente el bucle relevante en el código de ensamblaje. Intenté elegir una operación que sea fácil de detectar pero también "inofensiva" en el sentido de que no debería requerir ninguna verificación relacionada a desbordamientos enteros.)
Nuevamente, hubo una gran diferencia en el rendimiento entre -O3
y -Ofast
. Así que eché un vistazo al código de ensamblaje:
Con
-Ofast
obtengo más o menos lo que esperaría. La parte relevante es un bucle con 5 instrucciones de lenguaje de máquina.Con
-O3
eso consigo algo que estaba más allá de mi imaginación más salvaje. El bucle interno abarca 88 líneas de código de ensamblaje. No intenté entenderlo todo, pero las partes más sospechosas son 13 invocaciones de "callq _swift_retain" y otras 13 invocaciones de "callq _swift_release". Es decir, ¡ 26 llamadas de subrutina en el bucle interno !
Edición 3: En los comentarios, Ferruccio solicitó puntos de referencia que sean justos en el sentido de que no se basan en funciones integradas (por ejemplo, ordenar). Creo que el siguiente programa es un buen ejemplo:
let n = 10000
var x = [Int](repeating: 1, count: n)
for i in 0..<n {
for j in 0..<n {
x[i] = x[j]
}
}
No hay aritmética, por lo que no debemos preocuparnos por los desbordamientos de enteros. Lo único que hacemos es solo un montón de referencias de matriz. Y los resultados están aquí: Swift -O3 pierde en un factor casi 500 en comparación con -Ofast:
- C ++ -O3: 0.05 s
- C ++ -O0: 0.4 s
- Java: 0.2 s
- Python con PyPy: 0.5 s
- Python: 12 s
- Rápido - Rápido: 0.05 s
- Swift -O3: 23 s
- Swift -O0: 443 s
(Si le preocupa que el compilador pueda optimizar los bucles sin sentido por completo, puede cambiarlo a x[i] ^= x[j]
, por ejemplo , y agregar una declaración de impresión que salga x[0]
. Esto no cambia nada; los tiempos serán muy similares).
Y sí, aquí la implementación de Python fue una implementación de Python pura y estúpida con una lista de entradas y anidados para bucles. Debería ser mucho más lento que Swift no optimizado. Algo parece estar seriamente roto con Swift y la indexación de matrices.
Edición 4: estos problemas (así como algunos otros problemas de rendimiento) parecen haberse solucionado en Xcode 6 beta 5.
Para ordenar, ahora tengo los siguientes horarios:
- clang ++ -O3: 0.06 s
- swiftc -Ofast: 0.1 s
- swiftc -O: 0.1 s
- swiftc: 4 s
Para bucles anidados:
- clang ++ -O3: 0.06 s
- swiftc -Ofast: 0.3 s
- swiftc -O: 0.4 s
- swiftc: 540 s
Parece que ya no hay razón para usar el inseguro -Ofast
(aka -Ounchecked
); plain -O
produce un código igualmente bueno.
xcrun --sdk macosx swift -O3
. Es mas corto.