Hola, me llamo Luis y en esta ocasión les traigo un post.
Índice
Introducción
Las aplicaciones son súper rápidas y muy receptivas … excepto cuando no lo son. Siempre que esté lidiando con cálculos masivos en su código, habrá un punto en el que sus funciones se ralentizarán y se encontrará mirando su código preguntándose por qué esta simple suma, multiplicación o división está deteniendo todo.
¿No se suponía que teníamos supercomputadoras en nuestros bolsillos?
Afortunadamente para nosotros, las CPU modernas son bastante rápidas. Pero usted, el codificador, también necesita saber cómo presentar su código matemático de tal manera que se pueda ejecutar de manera muy eficiente.
Para hacer eso, haremos uso de (partes de) el marco Accelerate, que existe desde 2003
.
Un poco de historia
Por supuesto, Swift no existía en 2003, pero lo que sí existía eran los Power Mac G4
y G5
. Y una de las cosas que el rallador de queso OG
y el venerable Windtunnel G4
trajeron a la mesa fue AltiVec (también apodado Velocity Engine
por Apple).
El soporte para desarrolladores para esto se incluyó en Mac OS X 10.3
(Panther) para que todos pudieran aprovecharlo utilizando el marco Accelerate. ¡Todos empezaron a jugar con las matemáticas y hubo mucho regocijo!
Luego, en 2005
, Apple anunció que cambiaría de IBM
(PowerPC) a Intel (x86)
. Intel, afortunadamente, también tenía algunas de estas extensiones SIMD
, denominadas SSE
(2, 3
y luego 4
), por lo que era bueno que ya existiera una biblioteca de soporte (Accelerate) para encargarse del trabajo pesado.
Los desarrolladores que utilizan esta biblioteca pueden ejecutar su código con bastante facilidad en la nueva arquitectura y no preocuparse por tener que reescribir su código para que coincida con un nuevo conjunto de instrucciones o arquitectura.
Un par de años más tarde, en 2007
, Apple presentó el iPhone, nuevamente con una arquitectura de CPU diferente (ARM).
No fue hasta iOS 4
que Accelerate
estuvo disponible para esta plataforma, pero, nuevamente, fue bueno que hubiera un marco abstracto en ese momento y nosotros, como desarrolladores, no tuvimos que apuntar a las especificaciones de la CPU
.
Todo esto dio como resultado un marco único que se usa en múltiples arquitecturas de CPU
y proporciona una capa de abstracción sólida y rápida para que no tengamos que escribir código específico para arquitecturas específicas.
Acelerando por delante
Así que su código avanza lentamente, pero queremos arreglarlo y hacerlo rápido. Muchos desarrolladores de inmediato comienzan a mover cosas a las colas en segundo plano, porque eso es lo que nos enseñan, ¿verdad?
Nunca bloquee la cola principal y realice operaciones costosas en una cola en segundo plano. Es un consejo sensato, pero el código lento en una cola en segundo plano seguirá siendo… lento.
Lo programarás. Parece que se ejecuta en un período de tiempo razonable. Estás muy orgulloso porque estás usando cosas elegantes de Swifty como map
o zip
o reduce
, y todo encaja en una sola línea ordenada (funcional) o en un bucle for
súper ajustado.
Entonces, alguien decide que no deberían ser 50 números
, sino 50 000 números
. Actualizas el número, pero ahora, vaya, la aplicación no responde en absoluto.
No se preocupe, simplemente moverá el cálculo a una cola en segundo plano. ¡Problema resuelto! Pero tener que esperar cinco segundos antes de que el resultado regrese de alguna manera no se siente muy satisfactorio.
La complejidad algorítmica, para ser justos, no siempre es la culpable aquí. Incluso las operaciones muy básicas en grandes conjuntos de números pueden parecer una eternidad en Swift.
Swift debe ser lento, ¿verdad? Bueno no. Pero Swift hace un trabajo muy completo para protegerlo de los errores de tiempo de ejecución relacionados, por ejemplo, con el acceso no válido a la memoria.
El acceso a subíndices y los métodos funcionales para estructuras map
de datos completos son, lamentablemente, lentos en Swift. Seguro (más o menos) … pero lento.
¿Por qué vDSP
?
vDSP es uno de los componentes que conforman todo el marco de Accelerate
. También es una introducción relativamente suave a esta área, ya que hay muchas funciones de conveniencia a las que se puede acceder directamente desde Swift sin tener que recurrir a la administración de memoria manual y al acceso inseguro.
Entonces veamos un ejemplo. Supongamos que tenemos una matriz de 50 000 elementos
de tipo Double
y queremos calcular el valor medio. Una solución sencilla y sencilla sería sumar la matriz y dividirla por el número de elementos:
let array = Array (stride (desde: 0.0, hasta: 50000.0, por: 1))
let mean = array.reduce (0.0, +) / Double (array.count)
Es bastante justo, y si midiera esto en un patio de recreo, obtendría algo así como, dependiendo de su máquina, un tiempo de cálculo promedio de 0.0014 segundos
(14 milisegundos
).
No está mal, definitivamente no es lento. Sin embargo, si comparamos esto con la alternativa de vDSP
, obtenemos algo diferente:
importar Acelerarlet array = Array (stride (desde: 0.0, hasta: 50000.0, por: 1)) let mean = vDSP.mean (array)
Esto se ejecuta en un tiempo de cálculo promedio de menos de un milisegundo. Podría argumentar que esto no es justo ya que vDSP
tiene una función mean
dedicada , por lo que cambiaremos a una simple multiplicación de dos matrices:
let array = Array (stride (from: 0.0, to: 50000.0, by: 1))
let result = array.map {$ 0 * $ 0}
versus
let array = Array (stride (desde: 0.0, hasta: 50000.0, por: 1))
let result = vDSP.multiply (array, array)
La diferencia de velocidad aquí se vuelve aún más notable, con el código Swifty que tarda unos 350 ms
y el código vDSP
, de nuevo, menos de un milisegundo.
Como referencia: todos los puntos de referencia se ejecutaron en mi MacBook Pro
(2018
) usando un Xcode Playground
.
Podrías decir que estas son funciones muy básicas y nadie usa ese tipo de cálculo en sus aplicaciones. Pero si está interesado en el procesamiento de señales (por ejemplo, procesamiento de audio) o en realizar análisis de datos en conjuntos grandes, es probable que realice operaciones muy básicas y repetitivas en matrices de números muy grandes.
Especialmente si está trabajando en un dominio (casi) en tiempo real, como el procesamiento de audio, cada milisegundo puede marcar una gran diferencia.
Los documentos de Apple tienen una lista muy buena de las funciones más simples que se ocupan de la suma / resta / multiplicación / división
e incluso incluyen combinaciones en las que podría, por ejemplo, sumar dos vectores antes de multiplicarlo por un tercero.
Esto nuevamente conduce a aceleraciones donde la implementación de Swifty toma ± 390 ms
, y el código vDSP
toma menos de un milisegundo en promedio.
Terminando
Una vez que tenga una idea del tipo de operaciones que puede realizar vDSP
, comprenderá mejor cómo puede reescribir sus algoritmos y dividirlos en componentes que pueden ejecutarse muy rápido en todas las plataformas de Apple.
Esta lista proporciona una descripción general exhaustiva de todas las funciones de clase convenientes que le brinda vDSP
que hacen que sea muy fácil agregar una o más matrices (grandes) de números y obtener un resultado (¡rápido!).
Una advertencia aquí es que las funciones estáticas a las que se hace referencia solo están disponibles a partir de las últimas versiones de iOS / macOS (iOS 13+, macOS 10.15+)
.
Si necesita admitir versiones anteriores, aún es posible usar vDSP
, pero debe administrar la memoria usted mismo. Por ejemplo, nuestro ejemplo de multiplicar se vería así:
func multiply(_ array: [Double]) -> [Double] { // Allocate memory for the output array let output = UnsafeMutablePointer<Double>.allocate(capacity: array.count) // Make sure we free up the memory once we're done defer { output.deinitialize(count: array.count) output.deallocate() } // Perform the multiplication vDSP_vmulD(array, 1, array, 1, output, 1, vDSP_Length(array.count)) // Return the result return Array(UnsafeBufferPointer(start: output, count: array.count)) } let array = Array(stride(from: 0.0, to: 50000.0, by: 1)) let result = multiply(array)
Esto definitivamente no es tan bueno como las nuevas funciones estáticas, pero es igualmente rápido.
Además, estas funciones más antiguas le brindan más parámetros para modificar, por ejemplo, el paso o la cantidad de elementos que realmente se procesarán (si desea realizar los cálculos solo en una parte del conjunto de datos o, digamos, solo en los elementos pares / impares
).
Para mayor comodidad, también agregué el código de área de juegos para la evaluación comparativa. Simplemente puede copiar / pegar
esto en un nuevo patio de juegos vacío y ejecutarlo para ver los resultados (los tiempos de ejecución promedio se registrarán en la salida de la consola).
import Accelerate import XCTest let array = Array(stride(from: 0.0, to: 50000.0, by: 1)) let array2 = Array(stride(from: 0.0, to: 50000.0, by: 1)) let array3 = Array(stride(from: 0.0, to: 50000.0, by: 1)) class MathTests: XCTestCase { func testMeanSwifty() { measure { let _ = array.reduce(0.0, +) / Double(array.count) } } func testMeanvDSP() { measure { let _ = vDSP.mean(array) } } func testPowSwifty() { measure { let _ = array.map { pow($0, 2) } } } func testPowvDSP() { measure { let _ = vDSP.multiply(array, array) } } func testSumMultSwifty() { measure { let _ = array.enumerated().map { ($0.element + array2[$0.offset]) * array3[$0.offset] } } } func testSumMultvDSP() { measure { let _ = vDSP.multiply(addition: (array, array2), array3) } } } MathTests.defaultTestSuite.run()
Espero que esto lo anime a comenzar a pensar un poco diferente sobre los cálculos en Swift para que pueda dividirlos en operaciones orientadas a vectores y ayudarlo a acelerar las cosas inmensamente.
Gracias por leer este post.
Añadir comentario