Desarrollador V8 aquí. Dada la cantidad de interés en esta pregunta y la falta de otras respuestas, puedo intentarlo; Sin embargo, me temo que no será la respuesta que esperabas.
¿Existe algún conjunto de pautas en algún lugar sobre cómo programar mientras se está en el mundo de los arreglos SMI empaquetados (por ejemplo)?
Respuesta corta: es aquí: const guidelines = ["keep your integers small enough"]
.
Respuesta más larga: dar un conjunto completo de pautas es difícil por varias razones. En general, nuestra opinión es que los desarrolladores de JavaScript deberían escribir código que tenga sentido para ellos y su caso de uso, y los desarrolladores de motores de JavaScript deberían descubrir cómo ejecutar ese código rápidamente en sus motores. Por otro lado, obviamente hay algunas limitaciones a ese ideal, en el sentido de que algunos patrones de codificación siempre tendrán mayores costos de rendimiento que otros, independientemente de las opciones de implementación del motor y los esfuerzos de optimización.
Cuando hablamos de consejos de rendimiento, tratamos de tenerlo en cuenta y estimamos cuidadosamente qué recomendaciones tienen una alta probabilidad de seguir siendo válidas en muchos motores y muchos años, y también son razonablemente idiomáticas / no intrusivas.
Volviendo al ejemplo en cuestión: se supone que usar Smis internamente es un detalle de implementación que el código de usuario no necesita conocer. Hará algunos casos más eficientes, y no debería doler en otros casos. No todos los motores usan Smis (por ejemplo, AFAIK Firefox / Spidermonkey históricamente no lo ha hecho; he oído que en algunos casos sí usan Smis en estos días; pero no conozco ningún detalle y no puedo hablar con ninguna autoridad sobre el asunto) En V8, el tamaño de Smis es un detalle interno, y en realidad ha ido cambiando con el tiempo y las versiones. En las plataformas de 32 bits, que solían ser el caso de uso mayoritario, Smis siempre han sido enteros con signo de 31 bits; en plataformas de 64 bits solían ser enteros con signo de 32 bits, que recientemente parecía el caso más común, hasta que en Chrome 80 enviamos "compresión de puntero" para arquitecturas de 64 bits, que requerían reducir el tamaño de Smi a los 31 bits conocidos de las plataformas de 32 bits. Si por casualidad basó una implementación en el supuesto de que Smis son típicamente de 32 bits, obtendría situaciones desafortunadas comoesta .
Afortunadamente, como notó, las matrices dobles siguen siendo muy rápidas. Para el código numérico pesado, probablemente tenga sentido asumir / apuntar matrices dobles. Dada la prevalencia de dobles en JavaScript, es razonable suponer que todos los motores tienen un buen soporte para dobles y matrices dobles.
¿Es posible hacer programación genérica de alto rendimiento en Javascript sin usar algo como un sistema macro para alinear cosas como vec.add () en los sitios de llamadas?
"genérico" generalmente está en desacuerdo con "alto rendimiento". Esto no está relacionado con JavaScript o con implementaciones específicas del motor.
El código "genérico" significa que las decisiones deben tomarse en tiempo de ejecución. Cada vez que ejecuta una función, el código debe ejecutarse para determinar, por ejemplo, "¿es x
un número entero? Si es así, tome esa ruta de código. ¿Es x
una cadena? Luego salte aquí. ¿Es un objeto? ¿Tiene? ¿ .valueOf
No? Entonces tal vez .toString()
? tal vez en su cadena de prototipo? Llame a eso, y vuelva a empezar desde el principio con su resultado". El código optimizado de "alto rendimiento" se basa esencialmente en la idea de eliminar todas estas comprobaciones dinámicas; eso solo es posible cuando el motor / compilador tiene alguna forma de inferir los tipos con anticipación: si puede probar (o suponer con una probabilidad suficientemente alta) que x
siempre será un número entero, entonces solo necesita generar código para ese caso ( protegido por una verificación de tipo si se trata de supuestos no comprobados).
La alineación es ortogonal a todo esto. Una función "genérica" aún puede ser incorporada. En algunos casos, el compilador podría propagar información de tipo en la función en línea para reducir el polimorfismo allí.
(A modo de comparación: C ++, al ser un lenguaje compilado estáticamente, tiene plantillas para resolver un problema relacionado. En resumen, permiten que el programador indique explícitamente al compilador que cree copias especializadas de funciones (o clases enteras), parametrizadas en tipos dados. Buena solución para algunos casos, pero no sin su propio conjunto de inconvenientes, por ejemplo, largos tiempos de compilación y grandes binarios. JavaScript, por supuesto, no tiene plantillas, puede usarlo eval
para construir un sistema que sea algo similar, pero luego Me encontré con inconvenientes similares: tendrías que hacer el equivalente del trabajo del compilador de C ++ en tiempo de ejecución, y tendrías que preocuparte por la gran cantidad de código que estás generando).
¿Cómo se modulariza el código de alto rendimiento en bibliotecas a la luz de cosas como sitios de llamadas megamórficas y desoptimizaciones? Por ejemplo, si estoy usando felizmente el paquete A de Álgebra Lineal a alta velocidad, y luego importo un paquete B que depende de A, pero B lo llama con otros tipos y lo optimiza, de repente (sin que mi código cambie) mi código corre más lento .
Sí, ese es un problema general con JavaScript. V8 solía implementar ciertos componentes internos (cosas como Array.sort
) en JavaScript internamente, y este problema (que llamamos "contaminación por retroalimentación de tipo") fue una de las razones principales por las que nos hemos alejado completamente de esa técnica.
Dicho esto, para el código numérico, no hay tantos tipos (solo Smis y dobles), y como notó, deberían tener un rendimiento similar en la práctica, por lo que si bien la contaminación por retroalimentación de tipo es realmente una preocupación teórica, y en algunos casos puede tener un impacto significativo, también es bastante probable que en escenarios de álgebra lineal no veas una diferencia medible.
Además, dentro del motor hay muchas más situaciones que "un tipo == rápido" y "más de un tipo == lento". Si una operación determinada ha visto tanto Smis como dobles, está totalmente bien. Cargar elementos de dos tipos de matrices también está bien. Usamos el término "megamórfico" para la situación en la que una carga ha visto tantos tipos diferentes que se da por vencida de rastrearlos individualmente y en su lugar usa un mecanismo más genérico que se escala mejor a grandes cantidades de tipos, una función que contiene tales cargas puede Todavía se optimiza. Una "desoptimización" es el acto muy específico de tener que tirar el código optimizado para una función porque se ve un nuevo tipo que no se ha visto anteriormente, y que el código optimizado, por lo tanto, no está equipado para manejar. Pero incluso eso está bien: simplemente regrese al código no optimizado para recopilar más comentarios de tipo y optimice nuevamente más tarde. Si esto sucede un par de veces, entonces no hay nada de qué preocuparse; solo se convierte en un problema en casos patológicamente malos.
Entonces, el resumen de todo eso es: no te preocupes por eso . Simplemente escriba un código razonable, deje que el motor se encargue de ello. Y por "razonable", quiero decir: lo que tiene sentido para su caso de uso, es legible, mantenible, utiliza algoritmos eficientes, no contiene errores como leer más allá de la longitud de las matrices. Idealmente, eso es todo, y no necesitas hacer nada más. Si te hace sentir mejor hacer algo , y / o si realmente estás observando problemas de rendimiento, puedo ofrecerte dos ideas:
Usar TypeScript puede ayudar. Gran advertencia: los tipos de TypeScript están dirigidos a la productividad del desarrollador, no al rendimiento de la ejecución (y resulta que esas dos perspectivas tienen requisitos muy diferentes de un sistema de tipos). Dicho esto, hay una cierta superposición: por ejemplo, si anotas constantemente las cosas como number
, entonces el compilador de TS te advertirá si accidentalmente colocas null
una matriz o función que se supone que solo contiene / opera en números. Por supuesto, aún se requiere disciplina: una única number_func(random_object as number)
trampilla de escape puede socavar todo silenciosamente, porque la corrección de las anotaciones de tipo no se aplica en ningún lado.
Usar TypedArrays también puede ayudar. Tienen un poco más de sobrecarga (consumo de memoria y velocidad de asignación) por matriz en comparación con las matrices regulares de JavaScript (por lo que si necesita muchas matrices pequeñas, entonces las matrices regulares son probablemente más eficientes), y son menos flexibles porque no pueden crecer o reducir después de la asignación, pero proporcionan la garantía de que todos los elementos tienen exactamente un tipo.
¿Hay alguna buena herramienta de medición fácil de usar para verificar lo que el motor Javascript está haciendo internamente con los tipos?
No, y eso es intencional. Como se explicó anteriormente, no queremos que adapte específicamente su código a los patrones que V8 pueda optimizar particularmente bien hoy, y tampoco creemos que realmente quiera hacerlo. Ese conjunto de cosas puede cambiar en cualquier dirección: si hay un patrón que te encantaría usar, podríamos optimizarlo en una versión futura (previamente hemos jugado con la idea de almacenar enteros de 32 bits sin caja como elementos de matriz ... . pero el trabajo sobre eso aún no ha comenzado, así que no hay promesas); y, a veces, si hay un patrón para el que solíamos optimizar en el pasado, podríamos decidir abandonarlo si se interpone en el camino de otras optimizaciones más importantes / impactantes. Además, cosas como la heurística en línea son notoriamente difíciles de corregir, Por lo tanto, tomar la decisión correcta en el momento adecuado es un área de investigación en curso y los cambios correspondientes en el comportamiento del motor / compilador; lo que hace que este sea otro caso en el que sería desafortunado para todos (ustedy a nosotros) si pasaste mucho tiempo modificando tu código hasta que un conjunto de versiones actuales del navegador toma aproximadamente las decisiones de línea que crees (¿o sabes?) son las mejores, solo para regresar medio año después para darte cuenta de que los navegadores actuales han cambiado sus heurísticas.
Por supuesto, siempre puede medir el rendimiento de su aplicación como un todo; eso es lo que en última instancia importa, no las elecciones específicamente hechas por el motor internamente. Tenga cuidado con los microbenchmarks, ya que son engañosos: si solo extrae dos líneas de código y las compara, entonces lo más probable es que el escenario sea lo suficientemente diferente (por ejemplo, retroalimentación de tipo diferente) que el motor tomará decisiones muy diferentes.