ASM, basic , C y otros

Para mí si es una optimización, tengo dos formas de llegar y una resulta mucho más rápido que la otra, sin dudas esa última está optimizada, no importa como.
Y no... por que el concepto de optimización es otro, y no es hacer dos cosas 100% diferentes y compararlas diciendo cual es mejor.. por que eso no tiene en cuenta al contexto que usa el compilador para optimizar, y que no se extiende a una sola función.
Y acá que la comparación no es correcta (y si importa el como) por que en el ejemplo en assembler de este post vos usás los registros del micro mientras que en C usás variables en RAM al no declarar las variables como register (si bien es una sugerencia). El compilador siempre optimiza para usar RAM, ya que los registros son bastantes "escasos" a menos que optimicés para velocidad... y aún así. Esas optimizaciones se hacen con un profiler en tiempo de ejecución y no en tiempo de compilación ;).

De hecho hay funciones intrínsecas en gcc para suplir esas instrucciones y por lo que averigüe no son del todo útiles, aunque admito que no llegué a probarlas, decidí usarlas directamente en assembler.
Mal hecho!!! Ves que lo que te digo es cierto???? Por que no probaste y perfilaste el comportamiento de esas funciones..???

No quiero hacer offtopic, pero en este post dejé un ejemplo de eso en una rutina bastante sencilla en C, assembler básico y usando las instrucciones especiales: https://www.forosdeelectronica.com/posts/1000184/ Admito que la "forma" de medir el tiempo deja bastante que desear, lo ideal sería usando un contador de intrucciones en assembler, cosa que de hecho existe, pero lamentablemente no pude utilizarla con éxito.
Otra vez comparamos mal!!! Las instrucciones especiales SIMD trabajan en paralelo, mientras que el código que has usado en C es netamente secuencial. Si no hay forma de indicarle al compilador que debe generar código SIMD, la unica solución es hacerlo a pedal en assembler... o usar otro compilador (tené en cuenta que el GCC es un compilador multiplataforma, así que no le importa mucho lo que sea particular de cada micro.. al menos hasta que la comunidad desarrolle algo para explotar - opcionalmente - las características especiales).

Edito:
Y si, usé un puntero void. :LOL:
:( :( :( jajajajajaja!!!!
 
Y no... por que el concepto de optimización es otro, y no es hacer dos cosas 100% diferentes y compararlas diciendo cual es mejor.. por que eso no tiene en cuenta al contexto que usa el compilador para optimizar, y que no se extiende a una sola función.
Y acá que la comparación no es correcta (y si importa el como) por que en el ejemplo en assembler de este post

Para mi es eso. Tengo un "pedazo" de hard que con dos códigos distintos consigo dos resultados distintos, uno mejor que otro, por lo tanto ese utiliza mejor los recursos disponibles.

En otras palabras, el recurso está, en C ese recurso termina siendo ocioso, en cambio en assembler no.

...vos usás los registros del micro mientras que en C usás variables en RAM al no declarar las variables como register (si bien es una sugerencia). El compilador siempre optimiza para usar RAM, ya que los registros son bastantes "escasos" a menos que optimicés para velocidad... y aún así. Esas optimizaciones se hacen con un profiler en tiempo de ejecución y no en tiempo de compilación ;).

Eso es cierto, podría haber mejorado la rutina en C, declarando la variable contador para que sea un registro, eso hubiera mejorado bastante el tiempo.

Mal hecho!!! Ves que lo que te digo es cierto???? Por que no probaste y perfilaste el comportamiento de esas funciones..???

Por esto:

http://hilbert-space.de/?p=22

Además para poder utilizar bien esas funciones, es necesario entender el comportamiento de las instrucciones en assembler, por lo tanto mucho no me hubiera ahorrado y me aseguro de no depender de lo que interprete el gcc.

Otra vez comparamos mal!!! Las instrucciones especiales SIMD trabajan en paralelo, mientras que el código que has usado en C es netamente secuencial.

Y si, es un recurso que si el compilador no sabe utilizar, se pierde.

Si no hay forma de indicarle al compilador que debe generar código SIMD, la unica solución es hacerlo a pedal en assembler...

De hecho hay formas diciendole "usá las instrucciones Neon". Las funciones intrínsecas son una forma de ahondar que instrucciones específicas debe usar. Cuando compilé la rutina en C, le dí especifiqué ese flag.

Por otro lado, esas rutinas suelen ser cortas y representan poco código en un proyecto, por eso no veo con malos ojos poner la lupa cuando son críticas, más si no hay otra.

Por ej. el rotador, es mucho más complejo que la rutina en assembler, en realidad dejo preparado todo en C (tablas en ram con cálculos complejos que solo se obtienen una vez), para luego si levantar esos valores en la rutina y usarlos con la imagen actual.

...o usar otro compilador (tené en cuenta que el GCC es un compilador multiplataforma, así que no le importa mucho lo que sea particular de cada micro.. al menos hasta que la comunidad desarrolle algo para explotar - opcionalmente - las características especiales).

Ahí vamos, muy probablemente otros compiladores pagos estén más optimizados, yo uso el gcc porque justamente es gratuito.
 
Eso es cierto, podría haber mejorado la rutina en C, declarando la variable contador para que sea un registro, eso hubiera mejorado bastante el tiempo.
;)

Por esto:

http://hilbert-space.de/?p=22

Además para poder utilizar bien esas funciones, es necesario entender el comportamiento de las instrucciones en assembler, por lo tanto mucho no me hubiera ahorrado y me aseguro de no depender de lo que interprete el gcc.
...
Y si, es un recurso que si el compilador no sabe utilizar, se pierde.
Seeee.... pero si terminás de leer lo que dice el link que pasaste, vas a ver que el código que genera es un problema del gcc del 2010!!
En la web del gcc ya aparece como resuelto (está el link) :cool:


De hecho hay formas diciendole "usá las instrucciones Neon". Las funciones intrínsecas son una forma de ahondar que instrucciones específicas debe usar. Cuando compilé la rutina en C, le dí especifiqué ese flag.

Por otro lado, esas rutinas suelen ser cortas y representan poco código en un proyecto, por eso no veo con malos ojos poner la lupa cuando son críticas, más si no hay otra.

Por ej. el rotador, es mucho más complejo que la rutina en assembler, en realidad dejo preparado todo en C (tablas en ram con cálculos complejos que solo se obtienen una vez), para luego si levantar esos valores en la rutina y usarlos con la imagen actual.
Es que vos has hecho las cosas bastaaaante bien, excepto confiarte en la existencia de un problema muuuuy viejo [que parece ya estar resuelto] para decidirte por el assembler sin haber verificado antes la existencia real del error. Pero aún así, el meter código ASM tan específico (aunque acotado) es candidato a generar problemas en el futuro... ni hablar si cambiás de plataforma de desarrollo.
Por otra parte, la capa de abstracción que dá el compilador te permite liberarte de los detalles de implementación de cada micro en particular. Por supuesto que siempre hay que pagar algo, tal vez performance o tamaño de código... o ambas, pero hay que ver cuanto impacta eso en la aplicación final...

Pregunta:
En cuanto tiempo máximo necesitabas ejecutar el algoritmo???
 
Última edición:
;)
Seeee.... pero si terminás de leer lo que dice el link que pasaste, vas a ver que el código que genera es un problema del gcc del 2010!!
En la web del gcc ya aparece como resuelto (está el link) :cool:

Dr., el código resultante en C y en Assembler es muy similar por el uso de las rutinas, no cambia en nada, salvo que me aseguro saber exactamente que hago.

¿Qué tan enredado le puede resultar al compilador generar el código óptimo?, en la rutina que puse, todavía se le puede optimizar más usando los registro de una forma rebuscada como puse en el post siguiente.

Hasta ese nivel, si lo quisiera optimizar, el código en C y en Assembler es prácticamente similar debido a que está basado en las instrucciones especial y en como las uso (es decir, debería modificar de la misma forma ambos códigos), por lo tanto, la única diferencia radicará en que tan bien traduce el compilador eso que quiero hacer en Assembler, pero que dejé en forma explícita en C, ¿me explico?, a la larga, no gano nada, en el peor de los casos pierdo, porque el assembler ya lo pensé para poder hacer el código en C.

Es que vos has hecho las cosas bastaaaante bien, excepto confiarte en la existencia de un problema muuuuy viejo [que parece ya estar resuelto] para decidirte por el assembler sin haber verificado antes la existencia real del error.

Como puse arriba, a la larga el código es muy similar, solo está la traducción en el medio de lo que realmente busco en assembler, es decir no tiene mucho sentido que digamos.

Pero aún así, el meter código ASM tan específico (aunque acotado) es candidato a generar problemas en el futuro... ni hablar si cambiás de plataforma de desarrollo.

Eso no lo discuto, pero lo mismo sucedería con las funciones en C pensadas para el uso de esa instrucciones especiales. Lamentablemente no queda otra que usar dichas instrucciones, de hecho para algo están.

Por otra parte, la capa de abstracción que dá el compilador te permite liberarte de los detalles de implementación de cada micro en particular. Por supuesto que siempre hay que pagar algo, tal vez performance o tamaño de código... o ambas, pero hay que ver cuanto impacta eso en la aplicación final...

En mi caso, el impacto era notable.

Pregunta:
En cuanto tiempo máximo necesitabas ejecutar el algoritmo???

Idealmente 40mS sin tener en cuenta ninguna rutina en el medio (cosa que no es cierto, ya que en el medio realmente se hacen otras cosas), en C a secas, conseguí algo cercano a los 55mS y en assembler logré reducirlo a 33mS.

Como resultado, el rotador se acerca bastante a lo ideal, no llega a los 25fps, pero pega en el palo, safa bastante.
 
Dr., el código resultante en C y en Assembler es muy similar por el uso de las rutinas, no cambia en nada, salvo que me aseguro saber exactamente que hago.

¿Qué tan enredado le puede resultar al compilador generar el código óptimo?, en la rutina que puse, todavía se le puede optimizar más usando los registro de una forma rebuscada como puse en el post siguiente.

Hasta ese nivel, si lo quisiera optimizar, el código en C y en Assembler es prácticamente similar debido a que está basado en las instrucciones especial y en como las uso (es decir, debería modificar de la misma forma ambos códigos), por lo tanto, la única diferencia radicará en que tan bien traduce el compilador eso que quiero hacer en Assembler, pero que dejé en forma explícita en C, ¿me explico?, a la larga, no gano nada, en el peor de los casos pierdo, porque el assembler ya lo pensé para poder hacer el código en C.
De la forma que escribiste el procesamiento le resulta MUY complicado imaginar lo que estás haciendo, por que tu código son sumas o lo que sea secuenciales en una interación y el compilador tiene que imaginar que las puede ejecutar en paralelo por grupos para poder usar las instrucciones NEON.... hummmmm... :no:... ni con la bola de cristal
La solución es escribir el código C de otra forma donde quede explícito el procesamiento en grupos de a 8 y esperar que el compìlador sea inteligente como para darse cuenta... pero así es mas probable que lo haga.

Como puse arriba, a la larga el código es muy similar, solo está la traducción en el medio de lo que realmente busco en assembler, es decir no tiene mucho sentido que digamos.
Eso no lo discuto, pero lo mismo sucedería con las funciones en C pensadas para el uso de esa instrucciones especiales. Lamentablemente no queda otra que usar dichas instrucciones, de hecho para algo están.
Claro que es similar, pero oculto en funciones en C, que tienen la capacidad de mutar su estructura u comportamiento sin impactar en la forma del código final. Lo que vos hiciste está bien, pero si alguien tiene que mantener ese bloque de código... deseale suerte.

Idealmente 40mS sin tener en cuenta ninguna rutina en el medio (cosa que no es cierto, ya que en el medio realmente se hacen otras cosas), en C a secas, conseguí algo cercano a los 55mS y en assembler logré reducirlo a 33mS.

Como resultado, el rotador se acerca bastante a lo ideal, no llega a los 25fps, pero pega en el palo, safa bastante.
Pero no era casi 9 a 1 en la velocidad de ejecución???? Acá lograste solo el 1.6 a 1, que si bien se ajusta (maso) a tu necesidad es casi 6 veces mas lento que la medición...
O usaste assembler sin las NEON???

A ver... el problema no es usar ASM, el problema es hacerlo sin agotar todas las posibilidades del C y su compilador..
 
De la forma que escribiste el procesamiento le resulta MUY complicado imaginar lo que estás haciendo, por que tu código son sumas o lo que sea secuenciales en una interación y el compilador tiene que imaginar que las puede ejecutar en paralelo por grupos para poder usar las instrucciones NEON.... hummmmm... :no:... ni con la bola de cristal

No se si hablamos del rotador o del ejemplo que puse.

Si hablamos del ejemplo, todas las rutinas básicamente agarran 2 bloques de memoria (2 imágenes) y realizan una AND byte a byte (o en el caso de una imagen blanco y negro de 8bits, pixel a pixel), que permite generar esa máscara final que se vé en la imagen resultante.

Las instrucciones Neon, te permiten trabajar con varios pixel a la vez en una solo instrucción, por lo tanto viendo el primer ej. Neon, lo que idealmente le tomaría a la rutina en assembler básico 16 ciclos para trabajar 16 pixeles y hacer el AND, a la instrucción Neon le lleva solo 1 ciclo.

Ahora bien, los registros qn (128 bits), no son más que 2 registros dn (64 bits), pero... los registros dn a diferencia de los qn te permiten agruparlos, por lo tanto en el segundo ej. Neon, en vez de trabajar con 128bits (1 registro qn), trabajo con 4 registros dn a la vez (256 bits o 32 bytes).

Esa optimización, no hay forma de verlo, salvo que piensen exclusivamente en las instrucciones y en assembler, cosa que después la traducción en C es prácticamente igual.

La solución es escribir el código C de otra forma donde quede explícito el procesamiento en grupos de a 8 y esperar que el compìlador sea inteligente como para darse cuenta... pero así es mas probable que lo haga.

Pensá que el compilador por más que le pongas el flag del Neon no es muy bueno que digamos, por eso esta ese mix raro de las funciones que simulan esas instrucciones, al igual que el tipo de variables/registros.

Claro que es similar, pero oculto en funciones en C, que tienen la capacidad de mutar su estructura u comportamiento sin impactar en la forma del código final. Lo que vos hiciste está bien, pero si alguien tiene que mantener ese bloque de código... deseale suerte.

Por eso dejo comentado el código en C reemplazado por la rutina en assembler, si un futuro el hard/compilador lo permite, no creo que sea muy difícil pasarlo.

Pero no era casi 9 a 1 en la velocidad de ejecución???? Acá lograste solo el 1.6 a 1, que si bien se ajusta (maso) a tu necesidad es casi 6 veces mas lento que la medición...
O usaste assembler sin las NEON???

No seas malo, fijate en el ejemplo que puse que más o menos se dá esa relación.

Por otro lado, ojala el rotador fuera tan fácil y depender de una sola operación, lamentablemente esto no es así, ya que inevitablemente al trabajar con tablas obtenidas de senos/cosenos (previamente en C), las lecturas de los píxeles no son continuos, debo ir a buscar píxeles (o bytes en memoria) en lugares distantes, eso mata en un principio el proceso. En esa parte, la rutina es similar en C o en assembler y es lo que más tiempo lleva.

Luego que tengo los valores de la imagen ya "rotados", es decir los píxeles en memoria muy cercana, ahí si con las instrucciones Neon hago la interpolación lineal (o como le llaman bilineal) y le paso el trapo al C convencional, esos 20mS de diferencia se ganan ahí.

A ver... el problema no es usar ASM, el problema es hacerlo sin agotar todas las posibilidades del C y su compilador..

Dr., lamentablemente para usar este tipo de instrucciones en gcc, no hay mejor forma que hacerlo de esta manera.
 
No se si hablamos del rotador o del ejemplo que puse.

Si hablamos del ejemplo, todas las rutinas básicamente agarran 2 bloques de memoria (2 imágenes) y realizan una AND byte a byte (o en el caso de una imagen blanco y negro de 8bits, pixel a pixel), que permite generar esa máscara final que se vé en la imagen resultante.

Las instrucciones Neon, te permiten trabajar con varios pixel a la vez en una solo instrucción, por lo tanto viendo el primer ej. Neon, lo que idealmente le tomaría a la rutina en assembler básico 16 ciclos para trabajar 16 pixeles y hacer el AND, a la instrucción Neon le lleva solo 1 ciclo.

Ahora bien, los registros qn (128 bits), no son más que 2 registros dn (64 bits), pero... los registros dn a diferencia de los qn te permiten agruparlos, por lo tanto en el segundo ej. Neon, en vez de trabajar con 128bits (1 registro qn), trabajo con 4 registros dn a la vez (256 bits o 32 bytes).

Esa optimización, no hay forma de verlo, salvo que piensen exclusivamente en las instrucciones y en assembler, cosa que después la traducción en C es prácticamente igual.

Pensá que el compilador por más que le pongas el flag del Neon no es muy bueno que digamos, por eso esta ese mix raro de las funciones que simulan esas instrucciones, al igual que el tipo de variables/registros.

Yo hablaba de este ejemplo, que está en las tres formas: C, ASM y ASM+NEON. Y para que pueda optimizar usando el set de instrucciones NEON, la unica posibilidad es que el compilador haga un loop-unrolling de a 8 invocaciones internas, lo que es imposible por que no conoce el valor final del contador del for... y - probablemente - habría que transformar a enmascarar en un macro. La unica posibilidad sería que tamanio fuera una constante y multiplo de 8 o 16. Si tuviera eso Y fuera inteligente, el compilador tal vez podría decidirse por generar código NEON para procesamiento paralelo por que estaría bastante claro que es factible utilizarlo. De otra forma no se pueden esperar maravillas.
La optimización sin NEON es mas simple en la medida que ayudés al compilador diciendole que use los registros.. donde pueda.

Por eso dejo comentado el código en C reemplazado por la rutina en assembler, si un futuro el hard/compilador lo permite, no creo que sea muy difícil pasarlo.
Si usás NEON no queda otra que hacer eso. En ASM pelado es mucho mas simple por qur todos los µ tienen las mismas instrucciones basicas.

No seas malo, fijate en el ejemplo que puse que más o menos se dá esa relación.

Por otro lado, ojala el rotador fuera tan fácil y depender de una sola operación, lamentablemente esto no es así, ya que inevitablemente al trabajar con tablas obtenidas de senos/cosenos (previamente en C), las lecturas de los píxeles no son continuos, debo ir a buscar píxeles (o bytes en memoria) en lugares distantes, eso mata en un principio el proceso. En esa parte, la rutina es similar en C o en assembler y es lo que más tiempo lleva.

Luego que tengo los valores de la imagen ya "rotados", es decir los píxeles en memoria muy cercana, ahí si con las instrucciones Neon hago la interpolación lineal (o como le llaman bilineal) y le paso el trapo al C convencional, esos 20mS de diferencia se ganan ahí.


Dr., lamentablemente para usar este tipo de instrucciones en gcc, no hay mejor forma que hacerlo de esta manera.
No soy malo :LOL:, solo que lo que presentaste muestra una cosa que está lejos de ser real... por lo que te dije antes: el contexto. El compilador puede hacer optimizaciones sobre todo el código y ajustarlo para que funcionen. Vos solo te limitaste a un fragmento que tiene mucho impacto, y aún así los resultados no fueron como las pruebas los mostraban. Claro.. mientras que cumplan con solucionar el problema es una alternativa válida, pero no es la solución "final".. :LOL:
 
Atrás
Arriba