Al igual que los dispositivos iOS, cada vez con más velocidad aparece una nueva versión de Swift. Estos 11 consejos para acelerar la compilación con Swift pueden ser especialmente útiles, ya que en muchos casos nos encontramos con una versión antigua de Swift que no podemos actualizar.
¿Listo? ¡Allá Vamos!
Mostrar tiempos de compilación en Xcode
En primer lugar, necesitamos medir los tiempos de compilación con Swift. Este primer paso es crucial para poder verificar si los consejos expuestos en este artículo surten efecto.
Puedes habilitar un temporizador directamente dentro de la interfaz del usuario de Xcode. Este no se encuentra visible de forma predeterminada, pero sí se ejecuta en la línea de comandos.El temporizador aparecerá cada vez que se haga un build de la aplicación.
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
Después de habilitar el temporizador, reinicia el Xcode y verás en la barra de estado de Xcode el tiempo que tarda la compilación de la aplicación:
Se recomienda hacer un clean del proyecto y eliminar los datos derivados de la aplicación. Puedes hacerlo desde la línea de comandos:
rm -rf ~/Library/Developer/Xcode/DerivedData
Identificar el código de compilación lento
Los tiempos de compilación con Swift son lentos principalmente debido a la costosa verificación de tipos. Por defecto, Xcode no muestra ningún tiempo. Sin embargo, puedes indicarle que muestre funciones y expresiones de compilación lentas.
Abre el [project’s build settings] y agrega las siguientes configuraciones a tu [Other Swift Flags]:
-Xfrontend -warn-long-function-bodies=100
-Xfrontend -warn-long-expression-type-checking=100
El 100 es un número entero que representa el límite de tiempo de compilación que se coloca en sus funciones y expresiones. Se mide en milisegundos (ms).
Cuando compilas tu código, cualquier función o expresión que supere los 100 ms se marcará con un warning. Esto te brinda la oportunidad de refactorizar tu código y reducir esos tiempos de compilación con Swift.
Establecer el nivel de optimización en la configuración de compilación
El compilador de Xcode es lo suficientemente inteligente como para tomar algunas decisiones de optimización, y saber cuándo ignorar resultados de funciones que no tienen uso, o llamar directamente métodos que no fueron subclasificados, así que no es mala idea dejar que el Xcode lo optimice solo.
Ve a [build settings] y busca la sección de [Swift Compiler — Code Generation]. Allí puedes encontrar la configuración de [Optimization Level] que tiene tres opciones:
- No Optimization
- Optimization for Speed
- Optimization for Size
Si no estás haciendo una gran cantidad de depuración, es mejor configurarlo en el modo [Optimization for Speed]. Esto eventualmente reducirá el tiempo de compilación ya que el compilador omitirá los pasos de adjuntar valores al hilo del depurador.
Optimizar la generación dSYM
Usar un archivo dSYM tiene sentido para los informes de errores. Esto es particularmente útil cuando no tienes el depurador conectado. Sin embargo, tarda tiempo en crearse, por lo que es mejor usarlo solo cuando no está conectado al depurador Xcode.
Asegúrate de configurar tu [Debug Information Format] y crear siempre archivos dSYM para sus versiones de release y para sus versiones de depuración que no se ejecutan en el simulador. No es necesario crearlos cuando se ejecuta en el simulador de iOS.
Optimizar dependencias
Si usas CocoaPods, puedes optimizar todas tus dependencias agregando lo siguiente al final de tu Podile:
1 2 3 4 5 6 7 8 9 10 |
post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| if config.name == 'Debug' config.build_settings['OTHER_SWIFT_FLAGS'] = ['$(inherited)', '-Onone'] config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Owholemodule' end end ends end |
Dependencias de terceros
Una de las formas mas comunes de manejar dependencias de terceros es utilizando CocoaPods. Se trata de un mero uso, ya que cuando se trata de tiempos de compilación no es la mejor opción.
Una alternativa a la que puedes recurrir es Carthage. Es un poco más complicado de usar que CocoaPods, pero mejorará tus tiempos de construcción. Carthage lo logra construyendo solo dependencias externas cuando se agrega una nueva a tu proyecto. Si no la has agregado, tu proyecto no necesitará construir todas sus dependencias externas.
Xcode tiene un nuevo sistema de compilación
En Xcode 9, Apple presentó un nuevo sistema de compilación y no está habilitado por defecto. Uno de los principales beneficios del nuevo sistema de compilación es tiempos de compilación más rápidos.
Para usar el nuevo sistema de compilación, tienes que habilitarlo en el menú File y seleccionar [Workspace Settings] (o [Project Settings] si no estás utilizando un [Workspace]).
Los consejos anteriores se centran en sacar el provecho del Xcode para tener un mayor rendimiento en la velocidad de la compilación, pero muchos problemas con la rapidez de compilación se pueden resolver escribiendo el código de otra manera, y aquí te daré unos consejos para ello.
Evitar Print o debugPrint
Las funciones [print] o [debugPrint] son muy útiles para la depuración del código. El error más común entre los programadores es dejar muchos prints flotando en todo el código. Si a esto le sumamos proyectos muy grandes podrían ser cientos o miles:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
func testMethod(x: Int, withPrint: Bool) -> Int { // do something if withPrint { print(x) } return x } class OptimizationTests: XCTestCase { func testWithPrint() { // time: 7.914 sec let classTest = ClassTest() self.measure { for _ in 0...900000 { _ = classTest.testMethod(x: 100, withPrint: true) } } } func testWithOutPrint() { // time: 0.509 sec let classTest = ClassTest() self.measure { for _ in 0...900000 { _ = classTest.testMethod(x: 100, withPrint: false) } } } } |
En el test anterior se puede apreciar que la funcion testWithOutPrint() 15 veces mas rápido que testWithPrint(), dado que el comando [print] escribe en la consola. Esto hace un uso del disco que tiene un costo en el rendimiento. Este uso impide que el compilador Swift elimine gran cantidad de código innecesario como:
- Llamadas de funciones vacías
- Crear objetos que no se usan
- Bucles vacíos
Por ejemplo:
1 2 3 4 5 6 7 8 9 10 |
class TestClass { func doNothing() { } } for i in 0...1_000 { let testClass = TestClass() testClass.doNothing() testClass.doNothing() } |
En el anterior ejemplo se aprecia que el código será eliminado sin dejar alguno para compilación, pero si se agrega un print en el método doNothing el código no se eliminará en la optimización.
El mejor consejo es que no dejes estos comandos en tu código si no son necesarios. Cambiar la lógica en su uso puede servir para eliminar todos los print en el build de lanzamiento.
1 2 3 4 5 |
func debug_print<T>(object: T) { #if DEBUG print(object) #endif } |
Existen otras dependencias de terceros que ayudan con este problema como el SwiftyBeaver.
Algunas pautas de codificación
El uso de métodos final o private, variables let y declaración del tipo de variable aumentan tu rendimiento y disminuyen el tiempo de compilación con Swift.
Xcode normalmente te dice que debes usar let en lugar de var, así que usa let siempre que puedas.
Como probablemente sepas, Swift es un lenguaje orientado a objetos, lo que significa que puede hacer subclases y sobrescribir métodos para ampliar la funcionalidad. Para que esto funcione, Swift usa algo llamado [Dynamic Dispatch].
[Dynamic Dispatch] es el algoritmo que decide qué método debe invocarse cada vez que se envía un mensaje a un objeto. Utiliza una vTable (tabla virtual).
Cuando se llama un método de una clase parece una acción obvia, pero acuérdate que tambien busca subclases y si el método esta sobrescrito por otra subclase, esto debe hacerlo una y otra vez hasta llegar a fondo. Para ayudar a Swift a optimizar esta costosa tarea, puedes agregar el atributo final al comienzo del método, variable o incluye toda la clase.
Por último, deja de usar variables sin declarar su tipo. Si no lo haces, el compilador aumentará su tiempo de compilación:
1 2 3 4 5 6 7 8 9 10 |
func testFunction() { let classTest = ClassTest() self.measure { for _ in 0...9000000 { var text = "test" var integrer = 1 _ = classTest.testFunction(x: 100) } } } |
Solo agregando final al inicio, el método testFunction() mejora a un 5% el tiempo de compilación y al solo agregar el tipo a las variables y cambiar let por var mejora a un 4%.
Usar valores (estructuras) y no referencias (clases) en Array
Otro buen consejo. Cuando una Array en Swift contiene referencias, obtiene automáticamente las propiedades de NSArray, por lo tanto, no se puede optimizar. Pero si solo mantiene valores como Int o Structs en Array, el compilador puede optimizarlos fácilmente. Si tiene que elegir entre una estructura y una clase, esta es una gran ventaja de las estructuras: mantener estructuras en un Array es mucho más eficiente que mantener clases.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
struct contactStruct { var name: String var phoneNumber: Int } class contactClass { var name: String var phoneNumber: Int init(name : String, phoneNumber : Int) { self.name = name self.phoneNumber = phoneNumber } } func testStructInArray() { // 10% porciento mas rápido var a = [contactStruct]() self.measure { for _ in 1...1000000 { let entry = contactStruct(name: "name", phoneNumber: 623127050) a.append(entry) } } } func testClassInArray() { var a = [contactClass]() self.measure { for _ in 1...1000000 { let entry = contactClass(name: "name", phoneNumber: 623127050) a.append(entry) } } } |
Protocolo de clases si es posible
Si sabes que el protocolo que estás definiendo es solo para clases, márcalo como class protocol (protocolo de clase). Cuando el compilador sabe que el protocolo es solo para clases, puedes optimizar el ARC (recuento automático de referencias) haciendo que tu código se ejecute más rápido.
1 2 |
protocol classProtocol: class { } |
Conclusión
Hemos visto una serie de propuestas para mejorar el rendimiento de compilación con Swift en Xcode. El mismo Xcode proporciona múltiples opciones de configuración, pero las mejores optimizaciones dependen de la forma en la que escribamos el código. Está en manos de cada uno decidir si estas funciones son útiles o no y elegir los que se adapten mejor a cada proyecto. La mejor manera de descubrirlo y decidir es probar todas las opciones y usar la combinación óptima.
Como nada es gratis, en especial las mejoras de velocidad, algunas de estas opciones tienen un precio, como ciertas restricciones o el aumento del tamaño de la build.
Seguramente, como desarrollador ya habrías usado la mayoría de estas opciones. Pero si encontraste y aprendiste algo nuevo y útil de este artículo, ¡compártelo en tu red y permite que otros también aprendan de estos trucos!