Mejora del rendimiento de búsqueda de Burst Inspector

Mi nombre es Jonas Reholt y soy un estudiante que trabaja con el equipo Burst . Voy a utilizar este blog para compartir mi recorrido de optimización que ayudó a hacer posibles los recientes cambios de rendimiento en Burst Inspector. La búsqueda de Burst Inspector ahora es 13 veces más rápida, lo que permite a los desarrolladores concentrarse más rápidamente en el código que les interesa al optimizar proyectos.
Continúe leyendo para aprender cómo puede usar Unity Profiler para investigar cuellos de botella de rendimiento en su programa y cómo solucionarlos.
El compilador Unity Burst transforma su código C# en código ensamblador altamente optimizado. Burst Inspector le permite inspeccionar ese código de ensamblaje directamente en el Editor de Unity , por lo que no necesita usar herramientas externas para una inspección de código simple.
Cuando abra por primera vez el Inspector de Burst y seleccione un trabajo de destino para mostrar, verá una ventana similar a la imagen a continuación.

Como puede ver, Burst Inspector proporciona resaltado de sintaxis, flechas de flujo de ramas y mucho más.
El inspector intentará desplazarse hasta el ensamblaje que implementa la función de destino elegida, pero también es útil buscar en la vista del ensamblaje instrucciones específicas, comentarios, etc. Esto nos lleva al tema de esta entrada del blog.
Para realizar la búsqueda, el inspector debe buscar la salida del ensamblaje original y transformar estos índices en posiciones en la vista del inspector. La funcionalidad de búsqueda original seguía el patrón que se muestra a continuación y dependía en gran medida de la implementación de System.String.IndexOf(*).
while (assemblyCode.IndexOf(key, accIdx) >= 0) {
// ...
// Do logic for handling search hits
// ...
}
Al ejecutar la búsqueda anterior en 135.582 líneas de código de ensamblaje para un resultado de búsqueda común (21.769 resultados en total) resultó en un tiempo de ejecución de aproximadamente 12 segundos para la primera búsqueda y aproximadamente 5 segundos para las búsquedas posteriores. Este no es realmente un tiempo de espera deseable para un evento GUI , por lo que tuvimos que hacer algo. Al ejecutar la búsqueda a través de Unity Profiler, se reveló que el 37,3 % del tiempo de ejecución se gastó en IndexOf(*), como se ve a continuación.

Una optimización sensata debe abordar la dependencia de esta función, ya sea realizando una implementación personalizada o cambiando el algoritmo por completo. Independientemente del algoritmo que se utilice, será necesario recorrer toda la cadena. Por lo tanto, se requiere alguna implementación personalizada para encontrar coincidencias. Teniendo en cuenta esto, parecía adecuado comenzar la optimización manteniendo el algoritmo original, pero creando una función IndexOf personalizada.
Los 3,34 segundos invertidos en LongTextArea.GetFragNrFromBlockIdx() se deben a la recuperación de código de ensamblaje sin color. Esto se utiliza para realizar la búsqueda. Actualmente, Burst Inspector guarda el código de ensamblaje dos veces: una vez formateado para renderizar y otra sin formato.
Escribir una función personalizada también tiene el agradable efecto secundario de reducir la cantidad de llamadas, ya que actualmente hay una llamada por cada resultado de búsqueda, más uno.
El código fuente de IndexOf(*) revela muchas comprobaciones de seguridad necesarias para una implementación general sólida. Sin embargo, en nuestro caso podemos asumir con seguridad que la mayoría de estas comprobaciones son verdaderas. Para intentar aprovechar al máximo el rendimiento, deberá crear una función similar a C para evitar problemas como la comprobación de límites.
Puede escribir la función siguiendo el pseudocódigo a continuación, donde IsKeyMatch (*) simplemente verifica si la clave coincide o no.
List<int> Search(string assemblyCode, string key, int accIdx) {
var hits = new List<int>();
for (i = accIdx; i < assemblyCode.len - key.len; i++) {
if (IsKeyMatch(assemblyCode, key, i)) {
hits.add(i);
i += key.len-1;
}
}
return hits;
}
Sin embargo, debido a que C# es un lenguaje administrado, esta función similar a C requiere que usted fije los objetos administrados utilizados para que el recolector de elementos no utilizados no reubique la dirección de memoria. Aquí está el código repetitivo:
unsafe {
fixed (char* source = assemblyCode) {
fixed (char* needle = key) {
CustomIndexOf(source, key)
}
}
}
Al juntar estas cosas, podrá separar el bucle while original en una única llamada al buscador de índices y la lógica para manejar los resultados de la búsqueda:
matches = FindAllMathces(text, key)
foreach match {
...
Do logic for handling search hits
...
}
¿Cuales fueron las ganancias? Usando el pequeño ejemplo anterior, este cambio en el código proporciona una aceleración de 6,6x en la llamada inicial y una aceleración de 13,2x en las llamadas subsiguientes (medidas como antiguas/nuevas). La menor aceleración en la búsqueda inicial se debe a la sobrecarga de carga en el ensamblaje sin formato para evitar encontrar coincidencias en las cadenas de color.

Con estas mejoras, las búsquedas de gran carga con un poco menos de 22.000 resultados ahora tomarán alrededor de 1,8 segundos para la búsqueda inicial y alrededor de 0,4 segundos para las búsquedas posteriores. Esto hace que el Inspector de Burst sea más útil para reuniones grandes, ya que ya no hay tiempo suficiente para preparar una taza de té durante cada búsqueda.
Puedes aprovechar esta mejora de rendimiento ahora con el paquete Burst 1.8.7.
¿Buscas más información sobre Burst? Conéctate con nosotros en el foro Burst . Asegúrese de estar atento a más blogs técnicos nuevos de otros desarrolladores de Unity como parte del trabajo en curso. SerieTecnología de las Trincheras.