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.
fuente
xcrun --sdk macosx swift -O3
. Es mas corto.Respuestas:
tl; dr Swift 1.0 ahora es tan rápido como C en este punto de referencia utilizando el nivel de optimización de lanzamiento predeterminado [-O].
Aquí hay un resumen rápido en el lugar en Swift Beta:
Y lo mismo en C:
Ambos trabajan:
Ambos se llaman en el mismo programa que el escrito.
Esto convierte los tiempos absolutos en segundos:
Aquí hay un resumen de los niveles de optimización del compilador:
Tiempo en segundos con [-Onone] para n = 10_000 :
Aquí está el tipo incorporado () de Swift para n = 10_000 :
Aquí está [-O] para n = 10_000 :
Como puede ver, el rendimiento de Swift mejoró en un factor de 20.
Según la respuesta de mweathers , establecer [-Ofast] hace la diferencia real, resultando en estos tiempos para n = 10_000 :
Y para n = 1_000_000 :
A modo de comparación, esto es con [-Onone] para n = 1_000_000 :
Entonces Swift sin optimizaciones fue casi 1000 veces más lento que C en este punto de referencia, en esta etapa de su desarrollo. Por otro lado, con ambos compiladores configurados en [-Ofast] Swift en realidad funcionó al menos tan bien, si no un poco mejor que C.
Se ha señalado que [-Ofast] cambia la semántica del lenguaje, haciéndolo potencialmente inseguro. Esto es lo que Apple afirma en las notas de lanzamiento de Xcode 5.0:
Todos menos lo defienden. Si eso es sabio o no, no podría decirlo, pero por lo que puedo decir, parece lo suficientemente razonable como para usar [-Ofast] en una versión si no está haciendo aritmética de punto flotante de alta precisión y está seguro de que no hay un número entero o los desbordamientos de matriz son posibles en su programa. Si necesita un alto rendimiento y comprobaciones de desbordamiento / aritmética precisa, elija otro idioma por ahora.
ACTUALIZACIÓN BETA 3:
n = 10_000 con [-O] :
Swift en general es un poco más rápido y parece que el tipo incorporado de Swift ha cambiado significativamente.
ACTUALIZACIÓN FINAL:
[-Uno] :
[-O] :
[-No comprobado] :
fuente
TL; DR : Sí, la única implementación del lenguaje Swift es lenta, en este momento . Si necesita un código rápido, numérico (y otros tipos de código, presumiblemente), simplemente vaya con otro. En el futuro, debe reevaluar su elección. Sin embargo, podría ser lo suficientemente bueno para la mayoría de los códigos de aplicaciones que se escriben en un nivel superior.
Por lo que veo en SIL y LLVM IR, parece que necesitan un montón de optimizaciones para eliminar las retenciones y lanzamientos, que podrían implementarse en Clang (para Objective-C), pero aún no las han portado. Esa es la teoría a la que me refiero (por ahora ... todavía necesito confirmar que Clang hace algo al respecto), ya que un generador de perfiles en el último caso de prueba de esta pregunta arroja este resultado "bonito":
Como dijeron muchos otros,
-Ofast
es totalmente inseguro y cambia la semántica del lenguaje. Para mí, está en la etapa "Si vas a usar eso, solo usa otro idioma". Volveré a evaluar esa elección más tarde, si cambia.-O3
nos da un montón de llamadasswift_retain
yswift_release
, sinceramente, no parece que deberían estar allí para este ejemplo. El optimizador debería haberlos eludido (la mayoría de ellos) AFAICT, ya que conoce la mayor parte de la información sobre la matriz y sabe que tiene (al menos) una fuerte referencia a ella.No debería emitir más retenciones cuando ni siquiera está llamando a funciones que podrían liberar los objetos. No creo que un constructor de matrices pueda devolver una matriz que sea más pequeña de lo que se solicitó, lo que significa que muchas comprobaciones emitidas son inútiles. También sabe que el número entero nunca estará por encima de 10k, por lo que las comprobaciones de desbordamiento pueden optimizarse (no por
-Ofast
rareza, sino por la semántica del lenguaje (nada más está cambiando esa variable ni puede acceder a ella, y sumando hasta 10k es seguro para el tipoInt
).Sin embargo, es posible que el compilador no pueda desempaquetar la matriz o los elementos de la matriz, ya que se los pasa
sort()
, que es una función externa y tiene que obtener los argumentos que espera. Esto nos hará tener que usar losInt
valores indirectamente, lo que haría que fuera un poco más lento. Esto podría cambiar si lasort()
función genérica (no en la forma de métodos múltiples) estuviera disponible para el compilador y se insertara.Este es un lenguaje muy nuevo (públicamente), y está pasando por lo que supongo que son muchos cambios, ya que hay personas (muy) involucradas con el lenguaje Swift que solicitan comentarios y todos dicen que el idioma no está terminado y lo hará. cambio.
Código usado:
PD: No soy un experto en Objective-C ni en todas las instalaciones de Cocoa , Objective-C o los tiempos de ejecución Swift. También podría estar asumiendo algunas cosas que no escribí.
fuente
-Ofast
"totalmente inseguro"? Suponiendo que sepa cómo probar su código y descartar desbordamientos.-Ofast
también cambia la semántica del idioma, pero no puedo financiar ningún documento para ello. ¿Cómo puede estar seguro de saber lo que está haciendo?Int[]
' ya que eso depende del nivel que el compilador pueda alinear y debería poder alinear los bucles cerrados con / después de alguna orientación.Decidí echarle un vistazo a esto por diversión, y aquí están los horarios que obtengo:
Rápido
Resultados:
Swift 1.1
Swift 1.2
Swift 2.0
Parece ser el mismo rendimiento si compilo con
-Ounchecked
.Swift 3.0
Parece que ha habido una regresión de rendimiento de Swift 2.0 a Swift 3.0, y también veo una diferencia entre
-O
y-Ounchecked
por primera vez.Swift 4.0
Swift 4 mejora el rendimiento nuevamente, mientras mantiene una brecha entre
-O
y-Ounchecked
.-O -whole-module-optimization
no parecía hacer la diferenciaC ++
Resultados:
Apple Clang 6.0
Apple Clang 6.1.0
Apple Clang 7.0.0
Apple Clang 8.0.0
Apple Clang 9.0.0
Veredicto
Al momento de escribir esto, la clasificación de Swift es rápida, pero no tan rápida como la clasificación de C ++ cuando se compila
-O
con los compiladores y bibliotecas anteriores. Con-Ounchecked
, parece ser tan rápido como C ++ en Swift 4.0.2 y Apple LLVM 9.0.0.fuente
De
The Swift Programming Language
:La
sort
función tiene dos declaraciones.La declaración predeterminada que le permite especificar un cierre de comparación:
Y una segunda declaración que solo toma un único parámetro (la matriz) y está "codificado para usar el comparador menor que".
Probé una versión modificada de su código en un patio de juegos con el cierre agregado para poder monitorear la función un poco más de cerca, y descubrí que con n configurado en 1000, el cierre se llamaba unas 11,000 veces.
No es una función eficiente, y recomendaría usar una mejor implementación de la función de clasificación.
EDITAR:
Eché un vistazo a la página de Wikipedia de Quicksort y escribí una implementación Swift para ello. Aquí está el programa completo que utilicé (en un patio de recreo)
Usando esto con n = 1000, encontré que
Parece que el método de ordenamiento incorporado es (o está cerca) de ordenamiento rápido, y es realmente lento ...
fuente
2*n*log(n)
. Eso es 13815 comparaciones para ordenar n = 1000 elementos, por lo que si la función de comparación se llama aproximadamente 11000 veces, eso no parece tan malo.log(n)
para la complejidad algorítmica convencionalmente se refiere a log base-2. La razón para no indicar la base es que la ley de cambio de base para logaritmos solo introduce un multiplicador constante, que se descarta a los efectos de la notación O.C(n) = 2n ln n ≈ 1.39n log₂ n
. Para n = 1000 esto da C (n) = 13.815, y es no una "notación de orden O".A partir de Xcode 7 puede activar
Fast, Whole Module Optimization
. Esto debería aumentar su rendimiento de inmediato.fuente
Rendimiento de Swift Array revisitado:
Escribí mi propio punto de referencia comparando Swift con C / Objective-C. Mi punto de referencia calcula números primos. Utiliza la matriz de números primos anteriores para buscar factores primos en cada nuevo candidato, por lo que es bastante rápido. Sin embargo, hace TONELADAS de lectura de matriz y menos escritura en matrices.
Originalmente hice este punto de referencia contra Swift 1.2. Decidí actualizar el proyecto y ejecutarlo contra Swift 2.0.
El proyecto le permite seleccionar entre usar matrices Swift normales y usar memorias intermedias de memoria inseguras Swift usando semántica de matrices.
Para C / Objective-C, puede optar por usar matrices NSA o matrices mal asignadas en C.
Los resultados de la prueba parecen ser bastante similares con la optimización de código más pequeña y rápida ([-0s]) o la optimización más rápida y agresiva ([-0fast]).
El rendimiento de Swift 2.0 sigue siendo horrible con la optimización del código desactivada, mientras que el rendimiento de C / Objective-C es solo moderadamente más lento.
La conclusión es que los cálculos basados en la matriz C malloc'd son los más rápidos, por un margen modesto
Swift con búferes inseguros toma alrededor de 1.19X - 1.20X más tiempo que las matrices mal asignadas en C cuando se utiliza la optimización de código más rápida y más pequeña. la diferencia parece un poco menor con una optimización rápida y agresiva (Swift tarda más como 1.18x a 1.16x más que C.
Si usa matrices Swift regulares, la diferencia con C es ligeramente mayor. (Swift tarda ~ 1.22 a 1.23 más tiempo).
Las matrices Swift regulares son
DRAMATICALLY
más rápidas de lo que eran en Swift 1.2 / Xcode 6. Su rendimiento es tan cercano a las matrices basadas en el buffer inseguro de Swift que el uso de buffers de memoria inseguros ya no parece valer la pena, lo cual es grande.Por cierto, el rendimiento de Objective-C NSArray apesta. Si va a utilizar los objetos contenedores nativos en ambos idiomas, Swift es DRAMÁTICAMENTE más rápido.
Puedes ver mi proyecto en github en SwiftPerformanceBenchmark
Tiene una interfaz de usuario simple que hace que recopilar estadísticas sea bastante fácil.
Es interesante que la clasificación parece ser un poco más rápida en Swift que en C ahora, pero que este algoritmo de número primo es aún más rápido en Swift.
fuente
El problema principal que otros mencionan pero que no se mencionan lo suficiente es que
-O3
no hace nada en Swift (y nunca lo ha hecho), por lo que, cuando se compila, efectivamente no está optimizado (-Onone
).Los nombres de las opciones han cambiado con el tiempo, por lo que algunas otras respuestas tienen marcas obsoletas para las opciones de compilación. Las opciones actuales correctas (Swift 2.2) son:
La optimización de todo el módulo tiene una compilación más lenta, pero puede optimizarse a través de archivos dentro del módulo, es decir, dentro de cada marco y dentro del código de aplicación real, pero no entre ellos. Debe usar esto para cualquier cosa crítica de rendimiento)
También puede deshabilitar las comprobaciones de seguridad para obtener aún más velocidad, pero con todas las afirmaciones y condiciones previas no solo deshabilitadas sino optimizadas en función de que sean correctas. Si alguna vez llega a una afirmación, esto significa que tiene un comportamiento indefinido. Úselo con extrema precaución y solo si determina que el aumento de velocidad vale la pena (mediante pruebas). Si lo encuentra valioso para algún código, recomiendo separar ese código en un marco separado y solo deshabilitar las verificaciones de seguridad para ese módulo.
fuente
Este es mi blog sobre Quick Sort- Github sample Quick-Sort
Puede echar un vistazo al algoritmo de partición de Lomuto en Particionar la lista. Escrito en Swift.
fuente
Swift 4.1 presenta un nuevo
-Osize
modo de optimización.Lectura adicional: https://swift.org/blog/osize/
fuente