Consejos de rendimiento de Euphoria


Consejos generales

  • Si su programa es suficientemente rápido, olvídese de acelerarlo. Solo hágalo simple y legible.

  • Si su programa es demasiado lento, los consejos de más abajo probablemente no solucionen su problema. Debería encontrar un mejor algoritmo.

  • La forma más fácil de ganar un poco de velocidad es desactivar la verificación de tipos en tiempo de ejecución. Inserte esta línea:
               without type_check
    
    al comienzo de su archivo principal .ex, delante de cualquier sentencia de inclusión. Típicamente ganará entre el 0 y 20%, dependiendo de los tipos definidos y los archivos que incluya. La mayoría de los archivos de inclusión estándares hace alguna verificación de tipos definidos por el usuario. Un programa que está completamente libre de verificaciones, debería acelerar ligeramente.

    También, para asegurarse quite o comente cualquier sentencia

               with trace
               with profile
               with profile_time
    
    with trace (aún sin ninguna llamada a trace()), y with profile pueden provocar fácilmente una caida del 10% o más. with profile_time podría disminuir la velocidad en el 1%. Cada una de estas opciones, también, consumirá memoria adicional.

  • Los cálculos usando enteros son más rápidos que los basados en números de punto flotante .

  • Declare las variables como enteros, en lugar de átomos, toda vez que sea posible y secuencias, en lugar de objetos. Esto le hará ganar un poco de velocidad.

  • En una expresión que realiza cáculos en punto flotante, normalmente es más rápida si escribe los números constantes en forma de punto flotante, por ejemplo, si 'x' tiene un valor en punto flotante, digamos , x = 9.9

    cambie:
               x = x * 5
    
    a:
               x = x * 5.0
    
    Esto le evita al intérprete tener que convertir el entero 5 al punto flotante 5.0

  • Euphoria hace la evaluación de corto circuito de las condiciones de if, elsif, y while que involucran a and y or. Euphoria detendrá la evaluación de cualquier condición una vez que determine si la condición es verdadera o no. Por ejemplo, en la sentencia if:
            if x > 20 and y = 0 then
                ...
            end if
    
    La prueba "y = 0" solamente se hará si "x > 20" es verdadera.

    Para obtener máxima velocidad, puede ordenar sus pruebas. Haga "X > 20" primero, si es más probable que sea falso que "y = 0".

    En general, con una condición "A and B", Euphoria no evaluará la expresión B cuando A es falsa (cero). Análogamente, con una condición como "A or B", B no se evaluará si A es verdadera (no cero).

    Las sentencias if simples están altamente optimizadas. Con la versión actual del intérprete, los 'if' simples anidados que comparan enteros, son comúnmente un poco más rápidos que una sentencia if única de corto circuito, por ejemplo:

           if x > 20 then
               if y = 0 then
                   ...
               end if
           end if
    

  • La velocidad de acceso a las variables privadas, locales y globales es la misma.

  • No hay penalización de rendimiento por definir constantes, en lugar de escribir números literales en el código. La velocidad de:
               y = x * MAX
    
    es exactamente la misma que:
               y = x * 1000
    
    donde se definió previamente:
               constant MAX = 1000
    
  • No hay penalización de rendimiento por tener muchos comentarios en su código. Los comentarios se ignoran por completo. No se ejecutan de ningún modo. Podría demorar unos pocos milisegundos más la carga inicial del programa, pero esto es un costo muy pequeño a pagar por la futura mantenibilidad del código, y cuando enlaza su programa, todos los comentarios se eliminan, por lo que el costo se hace absolutamente nulo.


Midiendo el rendimiento

En cualquier lenguaje de programación, y especialmente en Euphoria, tiene que hacer realmente mediciones antes de sacar conclusiones acerca del rendimiento.

Euphoria provee tanto análisis por conteo de ejecución, como por tiempo (solo DOS32). Lea refman.doc. Frecuentemente se sorprenderá con los resultados de estos perfiles. Concentre sus esfuerzos en los lugares del programa que usan un alto porcentaje del tiempo total (o al menos, los que se ejecutan una gran cantidad de veces). No tiene sentido reescribir una sección del código que usa el 0.01% del tiempo total. Comúnmente habrá un lugar, o unos pocos donde este código haga una diferencia significativa.

También puede medir la velocidad del código usando la función time(), por ejemplo:

        atom t
        t = time()
        for i = 1 to 10000 do
            -- pequeña porción de código
        end for
        ? time() - t

Tendría que reescribir la pequeña porción de código de formas diferentes, para determinar cual es la forma más rápida.


¿Cómo acelerar los ciclos?

El análisis de perfiles de ejecución le mostrará los puntos calientes de su programa. Normalmente están dentro de los ciclos. Observe cada cálculo dentro de un ciclo y pregúntese si realmente es necesario que ocurran cada vez que el ciclo se ejecuta, o si podría hacerse una vez y fuera del ciclo.


Convirtiendo multiplicaciones en sumas con un ciclo

La suma es más rápida que la multiplicación. A veces puede reemplazar una multiplicación por la variable de ciclo con una suma. Algo como:

        for i = 0 to 199 do
            poke(screen_memory+i*320, 0)
        end for
se convierte en:
        x = screen_memory
        for i = 0 to 199 do
            poke(x, 0)
            x = x + 320 
        end for

Guardando resultados en variables
  • Es más rápido guardar el resultado de un cálculo en una variable, que recalcular todo más tarde. Aún a veces en cosas tan simples como una operación con índices, o sumar 1 a la variable son dignas del ahorro.

  • Cuando tiene una secuencia con múltiples niveles de índices, es más rápido cambiar el código:
               for i = 1 to 1000 do
                   y[a][i] = y[a][i]+1
               end for
    
    a:
               ya = y[a]
               for i = 1 to 1000 do
                   ya[i] = ya[i] + 1
               end for
               y[a] = ya
    
    Por lo que hace 2 operaciones con índices por repetición, en lugar de 4. Las operaciones, 'ya = y[a]' y 'y[a] = ya' son muy livianas. Solo copian un puntero. No copian la secuencia completa.

  • Hay un leve costo cuando crea una nueva secuencia usando {a,b,c}. Si es posible, mueva esta operación fuera de un ciclo crítico, almacenándola en una variable antes del ciclo y referenciando la variable dentro del ciclo.


Llamadas de rutinas en-línea

Si tiene una rutina pequeña y rápida, pero se la llama una enorme cantidad de veces, ahorrá tiempo haciendo la operación en-línea, en lugar de llamar la rutuina. Su código puede hacerse menos legible, por lo que sería mejor usar la operación en-línea, solamente en los lugares que generan un montón de llamadas a la rutina.


Operaciones sobre secuencias

Euphoria le permite operar en una secuencia grande de datos usando una sentencia única. Esto le ahorra de escribir un ciclo donde se procesa un elemento por vez. Por ejemplo:

        x = {1,3,5,7,9}
        y = {2,4,6,8,10}

        z = x + y
contra:
        z = repeat(0, 5)  -- si es necesario
        for i = 1 to 5 do
            z[i] = x[i] + y[i]
        end for

En la mayoría de los lenguajes interpretados, es mucho más rápido procesar una secuencia (array) completa en una sentencia, que hacer operaciones escalares dentro de un ciclo. Esto se debe a que el intérprete tiene una gran cantidad de consumo de recursos para cada sentencia que ejecuta. Euphoria es diferente. Euphoria es muy liviano, con poco consumo de recursos interpretativos, asique las operaciones sobre secuencias no siempre ganan. La única solución es medir el tiempo en ambas formas. El costo por elemento es comúnmente más bajo cuando se procesa una secuencia dentro de una sentencia, pero hay consumos de recursos asociados con la asignación de desasignación de secuencias que puede alterar la escala de otro modo.


Algunos casos especiales de optimizaciones

Euphoria automáticamente optimiza ciertos casos especiales. Las 'x' e 'y' de más abajo, podrían ser variables o expresiones arbitrarias.

        x + 1      -- más rápido que x + y general 
        1 + x      -- más rápido que y + x general
        x * 2      -- más rápido que x * y general
        2 * x      -- más rápido que y * x general
        x / 2      -- más rápido que x / y general
        floor(x/y) -- donde x e y son enteros, es más rápido que x/y
        floor(x/2) -- más rápido que floor(x/y)

La 'x' de abajo es una variable simple, y la 'y' cualquier variable o expresión:

        x = append(x, y)   -- más rápido que z = append(x, y) general 
        x = prepend(x, y)  -- más rápido que z = prepend(x, y) general 

        x = x & y          -- donde x es mucho más grande que y,
                           -- es más rápido que z = x & y general

Cuando usted escribe un ciclo que "hace crecer" una secuencia, agregando o concatenando los datos en ella, el tiempo, en general, crecerá en proporción con el cuadrado del número (N) de elementos que está agregando. Sin embargo, si usted puede utilizar una de las formas optimizadas especiales de append(), prepend() o de concatenación enumeradas arriba, el tiempo crecerá solo en proporción con N (aproximadamente). Esto podía ahorrarle una cantidad de tiempo enorme al crear una secuencia extremadamente larga (podría también utilizar repeat() para establecer el tamaño máximo de la secuencia, y después completar los elementos en un ciclo, según se discute más abajo).


Asignación con operadores

Para obtener mayor velocidad, convierta:

        lado-izquierdo = lado-derecho op expresión
a:
        lado-izquierdo op= expresión
toda vez que el lado-izquierdo contenga al menos 2 índices, o por lo menos, un índice y un subrango. En los casos más simples, las dos formas ejecutan a la misma velocidad (o a una velocidad muy próxima).


Consejos para los modos gráficos de píxel

  • El modo 19 es el modo más rápido para gráficos animados y juegos.

  • La CPU no provee cache para la memoria de video (en modo 19). Generalmente lleva más tiempo leer o escribir datos a la pantalla que a un área general de memoria que usted asigne. Esto se suma a la eficacia de las pantallas virtuales, donde usted hace que toda su imagen se actualice en un bloque de memoria que usted consigue con allocate(), y entonces periódicamente copia con mem_copy() la imagen que resulta a la verdadera memoria de pantalla. De esta manera usted nunca tiene que leer la memoria (lenta) de pantalla.

  • Al dibujar píxeles, puede encontrar que los modos 257 y superiores son rápidos cerca del borde superior de la pantalla y lentos cerca del inferior.


Consejos para el modo de texto

Escribir texto en la pantalla usando puts() o printf() es bastante lento. Si es necesario, en DOS32, puede hacerlo mucho más rápido escribiendo en la memoria de video, o usando display_text_image(). Hay un consumo muy grande de recursos en cada puts() a la pantalla, y un costo relativamente pequeño por caracter. El consumo de recursos con exw (WIN32) es especialmente alto (en Windows 95/98/ME, al menos). Linux y FreeBSD están en medio de DOS32 y WIN32 en relación a la velocidad de salida. Por lo tanto, tiene sentido armar una cadena grande antes de llamar a puts(), en lugar de llamarla para cada caracter. Sin embargo, no hay ventaja en armar una cadena grande que una línea.

La lentitud de la salida de texto se debe principalmente al consumo de recursos del sistema operativo.


Rutinas de librería

Algunas rutinas comunes son estremadamente rápidas. Probablemente no podría hacer el trabajo más rápido de ninguna otra forma, aún si usa lenguaje C o ensamblador. Algunas de ellas son:

  • mem_copy()
  • mem_set()
  • repeat()

Otras rutinas son razonablemente rápidas, pero en algunos casos en que la velocidad es crucial, tendría que ser capaz de hacer ese trabajo más rápido.

        x = repeat(0,100)
        for i = 1 to 100 do
            x[i] = i
        end for
es algo más rápido que:
        x = {}
        for i = 1 to 100 do
            x = append(x, i)
        end for
porque append() tiene que asignar y reasignar el espacio mientras que 'x' crece de tamaño. Con repeat(), el espacio para 'x' se asigna una vez al comienzo. (append() es suficientemente inteligente para no asignar espacio con cada append() a 'x'. Asignará algo más de lo que necesita, para reducir la cantidad de reasignaciones).

Puede reemplazar:

        remainder(x, p)
con:
        and_bits(x, p-1)
para mayor velocidad cuando 'p' es una potencia positiva de 2. 'x' tiene que se un entero no negativo que quepa en 32 bits.

arctan() es más rápida que arccos() o arcsin().


Búsqueda

La función find() de Euphoria es la forma más rápida de buscar un valor en una secuencia de hasta 50 elementos. Más allá de esto, debería considerar el uso de una tabla hash (demo\hash.ex) o de un árbol binario (demo\tree.ex).


Ordenamiento

En la mayoría de los casos puede usar la rutina shell sort de include\sort.e.

Si tiene una enorme cantidad de datos para ordenar, debería probar uno de los ordenamientos de demo\allsorts.e (por ejemplo, great sort). Si los datos son demasiado grandes para entrar en memoria, no se fie de la capacidad de intercambio de memoria de Euphoria. En su lugar, ordene unos pocos de miles de registros por vez y escríbalos en una serie de archivos temporales. Luego, una todos los archivos temporales en otro ordenado completamente.

Si sus datos son enteros solamente, y están dentro de un rango razonablemente acotado, pruebe el bucket sort de demo\allsorts.e.


Sacando ventaja de la memoria cache

A medida que las CPU incrementan su velocidad, la brecha entre la velocidad de la memoria cache del chip y la velocidad de la memoria principal o DRAM (memoria dinámica de acceso aleatorio) se hace cada vez mayor. Podría tener 256 Mb de DRAM en su computadora, pero el cache del chip es probable que sea solo de 8K (datos) más 8K (instrucciones) on a Pentium, o 16K (datos) más 16K (instrucciones) en un Pentium con MMX o un Pentium II/III. La mayoría de las máquinas tienen también un cache de "nivel 2" de 256K o 512K.

Un algoritmo que recorre de una secuencia larga de varios miles de elementos o más, muchas veces, realizar desde el comienzo hasta el final una operación pequeña en cada elemento, no hará buen uso del cache de datos del chip. Puede ser que sea mejor recorrerla una vez, aplicando varias operaciones a cada elemento, antes de moverse al elemento siguiente. El mismo argumento se mantiene cuando su programa comienza a intercambiar, y los datos menos recientemente usados se mueven al disco.

Estos efectos del cache no se sienten tanto en Euphoria, ya que están en lenguajes compilados de bajo nivel, pero son mensurables.


Usando código de máquina y C

Euphoria le permite llamar rutinas escritas en código de máquina Intel de 32 bits. En WIN32, Linux y FreeBSD puede llamar rutinas de C en archivos .dll o .so, y esas rutinas de C pueden llamar a sus rutinas Euphoria. Podría necesitar llamar rutinas de C o en código de máquina porque algo no se puede hacer directamente en Euphoria, o debería hacerlo para mejorar la velocidad.

Para aumentar la velocidad, el código de máquina o las rutinas de C necesitan hacer una cantidad significativa de trabajo en cada llamada, de lo contrario el consumo de recursos de configurar los argumentos y hacer las llamadas, ocupará la mayor parte del tiempo, y puede ser que no representen ninguna ganacia.

Muchos programas tienen alguna operación interna que consume la mayor parte del tiempo de CPU. Si puede codificar esto en C o código de máquina mientras deja el grueso del programa en Euphoria, puede ser que alcance una velocidad comparable a C, sin sacrificar la flexibilidad y seguridad de Euphoria.

 
Usando el Traductor Euphoria a C

Puede descargar el Traductor Euphoria a C desde el sitio web de RDS. Este traducirá cualquier programa Euphoria en un conjunto de archivos fuente de C que luego podrá compilar usando un compilador de C.

El archivo ejecutable que obtiene al usar el Traductor debería correr igual, pero más rápido que cuando usa el intérprete. El incremento de velocidad puede ir desde un procentaje muy bajo hasta un factor de 5 o más.