Cómo funciona la optimización guiada por perfil (PGO)

La optimización guiada por perfil (también conocida como PGO o "pogo") es una forma de optimizar aún más las compilaciones optimizadas de tu juego con información sobre la forma en la que se comporta cuando se juega en el mundo real. De esta manera, se quita el código que se ejecuta con poca frecuencia, como casos de error o extremos, de las rutas de ejecución crítica, lo que lo acelera.

Diagrama que muestra una descripción general visual de cómo funciona la PGO

Figura 1: Descripción general de cómo funciona la PGO

Para usar la PGO, primero debes instrumentar tu compilación para generar datos de perfil con los que el compilador pueda trabajar. Luego, usa tu código. Para ello, ejecuta esa compilación y genera uno o más archivos de datos de perfil. Por último, copia esos archivos del dispositivo y úsalos con el compilador para optimizar el ejecutable con la información de perfil que capturaste.

Cómo funcionan las compilaciones optimizadas sin PGO

Una compilación optimizada sin usar datos de perfil usa varios métodos heurísticos para decidir cómo generar el código optimizado.

El desarrollador señala algunos explícitamente, por ejemplo, en C++ 20 o versiones posteriores, a través de sugerencias de dirección de rama, como [[likely]] y [[unlikely]]. Otro ejemplo sería usar la palabra clave inline, o incluso __forceinline (aunque, en general, es mejor y más flexible usar la primera). De forma predeterminada, algunos compiladores suponen que el primer segmento de una rama (es decir, la sentencia if y no la parte else) es la más probable. El optimizador también puede hacer suposiciones a partir del análisis estático del código sobre cómo se ejecutará, pero su alcance suele ser limitado.

El problema con estas heurísticas es que no pueden ayudar correctamente al compilador en todas las situaciones (incluso con lenguaje de marcado manual exhaustivo), por lo que si bien el código que se genera suele estar bien optimizado, no es tan bueno como debería podría ser si el compilador tuviera más información sobre su comportamiento en el tiempo de ejecución.

Cómo generar un perfil

Cuando tu ejecutable se compila con la PGO habilitada en modo instrumentado, tu ejecutable aumenta con código al comienzo de cada bloque de código; por ejemplo, al comienzo de una función o al comienzo de cada grupo de una rama. Este código se usa para realizar el seguimiento de un recuento de cada vez que se ingresa al bloque a través de la ejecución de código, que el compilador puede usar más adelante para generar código optimizado.

También se realiza otro tipo de seguimiento, por ejemplo, del tamaño de las operaciones de copia típicas en un bloque, para que más tarde se puedan generar versiones rápidas, además de integradas, de la operación.

Después de que el juego realice algún tipo de trabajo representativo, el ejecutable debe llamar a una función (__llvm_profile_write_file()) para escribir los datos del perfil en una ubicación personalizable del dispositivo. Esta función se vincula automáticamente a tu juego cuando la configuración de compilación tiene habilitada la instrumentación de la PGO.

El archivo de datos de perfil escrito se debe volver a copiar en la computadora host y, preferentemente, se debe mantener en una ubicación junto con otros perfiles de la misma compilación para que puedan usarse juntos.

Por ejemplo, puedes modificar el código de tu juego para llamar a __llvm_profile_write_file() cuando finalice la escena actual del juego. Para crear un perfil, debes compilar el juego con la instrumentación activada y, luego, implementarlo en el dispositivo Android. Mientras se ejecuta, los datos del perfil se capturan de forma automática: el ingeniero de QA ejecuta el juego con diferentes situaciones (o solo realiza pruebas normales).

Cuando hayas terminado de ejecutar diferentes partes del juego, puedes volver al menú principal, lo que finalizaría la escena actual del juego y escribiría los datos del perfil.

Luego, se puede usar una secuencia de comandos para copiar los datos del perfil del dispositivo de prueba y subirlos a un repositorio central donde se puedan capturar para su uso posterior.

Combinación de datos de perfil

Una vez que se obtenga el perfil de un dispositivo, debe convertirse del archivo de datos de perfil generado por la compilación instrumentada a la forma en que el compilador puede consumirlo. AGDE lo hace de forma automática por cualquier archivo de datos de perfil que agregues a tu proyecto.

La PGO se diseñó para combinar los resultados de múltiples ejecuciones de perfiles instrumentados. AGDE también lo hace de forma automática si tienes varios archivos en un solo proyecto.

Como ejemplo de cómo puede resultar útil la combinación de conjuntos de datos de perfil, supongamos que tienes un equipo de ingenieros de QA que juegan diferentes niveles del juego. Cada una de sus partidas se graba y se usa para generar datos de perfil a partir de una compilación del juego instrumentada por la PGO. La combinación de perfiles te permite combinar los resultados de todas estas ejecuciones de prueba distintas, que pueden ejecutar partes de tu código muy diferentes, para ofrecer mejores resultados.

Mejor aún, cuando se realizan pruebas longitudinales, en las que se conservan las copias de los datos de perfil de la versión interna a la versión interna, la recompilación no necesariamente invalida los datos de perfil anteriores. En general, el código es relativamente estable de una versión a otra, por lo que los datos de perfil de compilaciones anteriores aún pueden ser útiles y no se vuelven obsoletos de inmediato.

Genera compilaciones optimizadas según el perfil

Una vez que hayas agregado los datos del perfil a tu proyecto, puedes usarlos para compilar tu ejecutable. Para ello, habilita la PGO en el modo de optimización en la configuración de la compilación.

Esto le indica al optimizador del compilador que use los datos de perfil que capturaste antes cuando tomes decisiones de optimización.

Cuándo utilizar la optimización guiada por perfil

La PGO no es un elemento que se habilite al comienzo del desarrollo ni durante la iteración diaria del código. Durante el desarrollo, debes enfocarte en optimizaciones algorítmicas y basadas en diseños de datos, ya que te brindarán beneficios mucho mayores.

La PGO llega más adelante en el proceso de desarrollo, cuando se realizan mejoras para el lanzamiento. Piensa en la optimización guiada por perfil como la opción adecuada que te permite obtener el mejor rendimiento de tu código después de haberlo dedicado un tiempo a optimizarlo.

Mejora de rendimiento esperada con la PGO

Esto depende de una gran cantidad de factores, que incluyen qué tan completos y obsoletos están los perfiles, y qué tan cerca estará el código de una compilación optimizada tradicional.

En general, una estimación muy conservadora sería que los costos de CPU se reduzcan alrededor de un 5% en los subprocesos clave. Es posible que veas resultados diferentes.

Sobrecarga de instrumentación

La instrumentación de la PGO es integral y, si bien se genera de forma automática, no es gratuita. La sobrecarga de la instrumentación de PGO puede variar según tu base de código.

Costo de rendimiento de la instrumentación guiada por perfil

Es posible que veas una disminución en la velocidad de fotogramas con compilaciones instrumentadas. En algunos casos, según la cercanía con la utilización del 100% de la CPU durante el funcionamiento normal, esta disminución puede ser tan grande como para dificultar el juego normal.

Recomendamos que la mayoría de los desarrolladores creen un modo de repetición semideterminista para su juego. Este tipo de funcionalidad permite que el equipo de control de calidad inicie el juego en una ubicación de inicio conocida y repetible en el juego (como un juego guardado o un nivel de prueba específico) y, luego, registre la entrada. Esta entrada que se registra desde la compilación de prueba se puede incorporar en una compilación instrumentada de la PGO, reproducir y generar datos de perfil reales, independientemente del tiempo que tarde en procesar un fotograma individual, incluso si el juego estaba funcionando tan lento que no se podía jugar.

Este tipo de funcionalidad también tiene otros beneficios importantes, como multiplicar el esfuerzo del verificador: un verificador puede registrar sus entradas en un dispositivo y, luego, puede reproducirse en varios tipos diferentes de dispositivos para pruebas de humo.

Un sistema de repetición como este puede tener grandes beneficios en Android, donde hay una gran cantidad de variantes de dispositivos en el ecosistema, y los beneficios no terminan allí: puede formar parte del sistema de compilación de integración continua, lo que te permite realizar una regresión de rendimiento habitual durante la noche y pruebas de humo.

La grabación debe registrar la entrada del usuario en el punto más apropiado dentro del mecanismo de entrada de tu juego (probablemente no eventos de pantalla táctil directos, sino grabar sus consecuencias como comandos). Estas entradas también deben incluir un recuento de fotogramas que se marque de forma monotónica durante el juego, de modo que, durante la reproducción, el mecanismo de repetición pueda esperar el fotograma apropiado en el que debe activar un evento.

En el modo de reproducción, el juego debe evitar el acceso en línea, no debe mostrar anuncios y debe funcionar en un paso fijo (a la velocidad de fotogramas objetivo). Considera inhabilitar vsync.

No es importante que todo (por ejemplo, los sistemas de partículas) en tu juego se repita de manera totalmente determinista, pero las mismas acciones deben proporcionar las mismas consecuencias y resultados en el juego, es decir, el juego debe ser igual.

Costo de memoria de la instrumentación guiada por perfil

La sobrecarga de memoria de la instrumentación de la PGO varía mucho según la biblioteca específica que se compila. En nuestras pruebas, observamos un aumento general aproximado de 2.2 veces en el tamaño del ejecutable de la prueba. Este aumento de tamaño incluía tanto el código adicional necesario para instrumentar los bloques de código como el espacio necesario para almacenar los contadores. Estas pruebas no fueron exhaustivas y es posible que tu experiencia sea diferente.

Cuándo actualizar o descartar los datos de tu perfil

Debes actualizar tus perfiles cada vez que realices un gran cambio en tu código (o contenido del juego).

Lo que esto significa con exactitud depende de tu entorno de compilación y de tu ubicación de desarrollo.

Como se mencionó antes, no debes transferir los datos de perfil a los cambios principales del entorno de compilación. Si bien esto no impedirá que compiles o rompas tu compilación, se reducirán los beneficios de rendimiento de usar la PGO, ya que se aplicarán pocos datos de perfil al nuevo entorno de compilación. Sin embargo, este no es el único caso en el que los datos de tu perfil podrían quedar inactivos.

Comencemos por suponer que no usarás la PGO hasta el final del desarrollo cuando te preparas para un lanzamiento, más allá de recopilar una captura semanal para que los ingenieros enfocados en el rendimiento puedan verificar que no se producirá ningún inconveniente inesperado.

Esto cambia a medida que te acercas al período de lanzamiento, en el que el equipo de control de calidad realiza pruebas todos los días y ejecuta el juego de manera exhaustiva. Durante esta fase, puedes generar perfiles a partir de esos datos todos los días y usarlos a fin de informar compilaciones futuras para pruebas de rendimiento y ajustar tus propios presupuestos de rendimiento.

Cuando te preparas para un lanzamiento, debes bloquear la versión de compilación que planeas lanzar y, luego, hacer que el control de calidad ejecute esa generación con los datos de tu perfil nuevo. Luego, compila con estos datos para producir una versión final de tu archivo ejecutable.

Entonces, el control de calidad puede proporcionar una compilación final de envío optimizada para garantizar que sea útil lanzarla.