2 de julio de 2021

Utilice MDots por la velocidad, no solo por la exactitud

Artículo original: Use MDots for speed, not just for correctness
(Use MDots for speed.pdf)
Autor: Tamar E. Granor
Traducido por: Luis María Guayán


El prefijo de referencias a variables "m." (mdot) no solo hace que su código sea inequívoco, sino que lo hace más rápido.

Puede que no haya ningún tema en el que los desarrolladores de VFP como grupo se sientan mas convencidos que si prefijar o no, todas las referencias a las variables con "m." para evitar ambigüedades. Sin embargo, puede que sea hora de que se termine ese argumento, porque resulta que el uso de mdots también hace que el código se ejecute más rápido.

Desde sus primeros días, FoxPro ha dado preferencia a los nombres de campo en las expresiones. Cuando una expresión incluye un nombre que es tanto un nombre de campo de la tabla abierta en el área de trabajo actual, como una variable, a menos que se indique lo contrario, FoxPro usa el campo. Es decir, cuando tiene un código como el Listado 1, VFP primero busca campos llamados nHeight y nWidth. Solo si no lo encuentra, decide que debe haber querido decir variable.

Listado 1. Cuando se usan nombres en una expresión, VFP da preferencia a los nombres de campos.

 nArea = nHeight * nWidth 

Si desea utilizar la variable en lugar de un campo del mismo nombre, puede precederla con la letra "m" y un punto. Los desarrolladores de VFP suelen llamar a ésta combinación "mdot".

El Listado 2 muestra el ejemplo anterior con las variables claramente indicadas.

Listado 2. Mdots deja en claro que se refiere a una variable.

 nArea = m.nHeight * m.nWidth 

En este ejemplo, mdot no es necesario para nArea porque solo se puede asignar un nuevo valor a las variables mediante el signo igual.

Convenciones de nomenclatura como solución

Debido a este comportamiento, muchos desarrolladores de VFP han adoptado convenciones de nomenclatura destinadas a garantizar que nunca tengan variables y campos con el mismo nombre. La notación más común (recomendada en el archivo de ayuda de VFP y generalmente denominada "húngara") utiliza una letra de alcance ("l" para local, "p" para privado, "g" para global/pública) seguida de una letra tipo ("c" para carácter, "n" para numérico, etc.) al principio de cada nombre de variable. En esa notación, los campos obtienen un tipo de letra, pero no un indicador de alcance. Usando esta notación, un campo que representa height sería nHeight, pero una variable de altura sería lnHeight.

El problema de confiar en una convención de nomenclatura es que VFP no la conoce y no evita todos los conflictos. Por ejemplo, no seria imposible imaginar tener un campo llamado lOrange y una variable llamada loRange. Si bien estos se ven legiblemente diferentes, para el motor VFP, son exactamente iguales y el campo se utilizará siempre que haya ambigüedad.

A estas alturas, probablemente pueda decir que está del lado de "siempre usar mdots", y si está firmemente del lado de "no mdots", probablemente ningún argumento sobre cómo funciona VFP o posibles errores lo convencerá.

MDots es más rápido

Sin embargo, también resulta que el uso de mdots hace que su código se ejecute más rápido. Cuánto más rápido depende del número de referencias a variables y del número de campos de la tabla abiertos en el área de trabajo actual.

Recientemente probé en dos computadoras diferentes, usando dos programas diferentes, uno con solo unas pocas referencias a variables y otro con muchísimas más.

En cada caso, también probé diferentes números de campos en el área de trabajo actual, comenzando sin una tabla abierta, luego con una tabla (en realidad, un cursor) con cinco campos, luego uno con 10 campos, y así sucesivamente hasta 200 campos en la tabla en el área de trabajo actual.

Dada la preferencia de VFP por los campos, no me sorprendió ver que mdot era más rápido y que cuantos más campos en la tabla en el área de trabajo actual, mayor era la ventaja de mdot.

El Listado 3 muestra el primer programa de prueba, el que tiene menos referencias a variables. El código usa variables de altura y ancho para calcular el perímetro y área. Los cálculos se realizan en un ciclo que se ejecuta durante cinco segundos; hay un total de seis referencias a variables en el ciclo y los cálculos.

Listado 3. Este programa compara el uso de variables con mdots con el uso de variables sin mdots. El bloque que se está probando contiene seis referencias a variables.

* Compare speed with and without mdot
#DEFINE SECONDSTORUN 5
LOCAL nCase1Start, nCase1LoopEnd,
nCase2LoopStart, nCase2LoopEnd
LOCAL nCase1Passes, nCase2Passes
LOCAL nLength, nWidth, nPerimeter, nArea
* Test multiple cases from no table open
* to table with many fields open.
* Store results in a cursor in a different
* workarea.
CREATE CURSOR csrMDotSpeeds
(nFields N(3), nNoMDots I, nMDots I)
SELECT 0
LOCAL nFields, nField, cFieldList
* Initialize variables for calculations
nLength = 27.3
nWidth = 13.7
FOR nFields = 0 TO 200 STEP 5
  IF m.nFields <> 0
    cFieldList = ''
    FOR nField = 1 TO m.nFields
      cFieldList = m.cFieldList + "cField" + ;
        TRANSFORM(m.nField) + " C(5), "
    ENDFOR
    cFieldList = TRIM(m.cFieldList, ", ")
    CREATE CURSOR csrDummy (&cFieldList)
  ELSE
    SELECT 0
  ENDIF
  * Now do the test
  nCase1LoopStart = SECONDS()
  nCase1LoopEnd = m.nCase1LoopStart + ;
    SECONDSTORUN
  nCase1Passes = 0
  DO WHILE nCase1LoopEnd > SECONDS()
    nCase1Passes = nCase1Passes + 1
    nPerimeter = 2*nLength + 2*nWidth
    nArea = nLength * nWidth
  ENDDO
  nCase2LoopStart = SECONDS()
  nCase2LoopEnd = m.nCase2LoopStart + ;
    SECONDSTORUN
  nCase2Passes = 0
  DO WHILE m.nCase2LoopEnd > SECONDS()
    nCase2Passes = m.nCase2Passes + 1
    nPerimeter = 2*m.nLength + 2*m.nWidth
    nArea = m.nLength * m.nWidth
  ENDDO
  INSERT INTO csrMDotSpeeds
  VALUES (m.nFields, m.nCase1Passes, ;
    m.nCase2Passes)
  IF m.nFields <> 0
    USE IN csrDummy
  ENDIF
ENDFOR
RETURN

Los resultados de esta prueba en dos máquinas diferentes, fueron bastante similares. Sin tabla abierta en el área de trabajo actual (el caso 0), la versión sin mdots fue un poco más rápida. Después de eso, sin embargo, la versión mdots siempre fue más rápida. Con 30 campos en la tabla, la versión mdots completó más de un 25% más de iteraciones; con 50 campos, la versión mdots completó un 50% más de iteraciones. En el extremo máximo de la prueba, 200 campos, la versión mdots hizo 2,7 veces más pases.

El número de iteraciones completadas por el código usando mdots fue notablemente estable. Para una máquina determinada, la diferencia entre el máximo y el mínimo fue inferior al 0,02% del valor máximo.

Por otro lado, el número de iteraciones completadas por el código sin mdots descendió de manera bastante constante. Con 200 campos, solo se completaron alrededor de un tercio de las iteraciones que sin una tabla abierta.

Es importante tener en cuenta que estamos hablando de millones de iteraciones en cinco segundos, por lo que el efecto es pequeño para cualquier referencia a variable dada. Sin embargo, en una aplicación, es probable que tenga miles o decenas de miles de referencias a variables; en una aplicación típica, es probable que la mayoría de ellos ocurran con una tabla abierta en el área actual.

Una prueba más extensa

Quería ver la diferencia que hace mdot en un programa con muchas más referencias a variables que el ejemplo de perímetro y área. Para hacerlo, adapté un fragmento de código de una aplicación cliente. El núcleo del código es una función que determina si un punto específico está "cerca" de una línea específica. Acepta cuatro parámetros, una línea, un punto (en forma de coordenadas de fila y columna) y una tolerancia. La tolerancia indica qué tan lejos de la línea puede estar algo y aún ser considerado "cerca". El código real no es importante, pero la función contiene casi 60 referencias a variables potencialmente ambiguas.

La prueba, estructurada de la misma manera que la prueba anterior, se muestra en el Listado 4.

Listado 4. Este código prueba la velocidad de un programa con más de 50 referencias a variables con y sin mdots.

* Compare speed with and without mdot
#DEFINE SECONDSTORUN 5
LOCAL nCase1Start, nCase1LoopEnd, nCase2LoopStart, nCase2LoopEnd
LOCAL nCase1Passes, nCase2Passes
* Test multiple cases from no table open
* to table with many fields open.
* Store results in a cursor in a different
* workarea.
CREATE CURSOR csrMDotSpeedsLarge ;
  (nFields N(3), nNoMDots I, nMDots I)
SELECT 0
LOCAL nFields, nField, cFieldList
LOCAL oLine AS LINE
oLine = CREATEOBJECT("Line")
oLine.LEFT = 27
oLine.TOP = 13
oLine.HEIGHT = 152
oLine.WIDTH = 53
FOR nFields = 0 TO 200 STEP 5
  IF m.nFields <> 0
    cFieldList = ''
    FOR nField = 1 TO m.nFields
      cFieldList = m.cFieldList + "cField" + ;
        TRANSFORM(m.nField) + " C(5), "
    ENDFOR
    cFieldList = TRIM(m.cFieldList, ", ")
    CREATE CURSOR csrDummy (&cFieldList)
  ELSE
    SELECT 0
  ENDIF
  * Now do the test
  nCase1LoopStart = SECONDS()
  nCase1LoopEnd = m.nCase1LoopStart + ;
    SECONDSTORUN
  nCase1Passes = 0
  DO WHILE nCase1LoopEnd > SECONDS()
    nCase1Passes = nCase1Passes + 1
    IsPointNearLineNoMDot(oLine, 55, 45, 1)
    IsPointNearLineNoMDot(oLine, 100, 27, 2)
    IsPointNearLineNoMDot(oLine, 0, 0, 1)
    IsPointNearLineNoMDot(oLine, 500, 7, 3)
  ENDDO
  nCase2LoopStart = SECONDS()
  nCase2LoopEnd = m.nCase2LoopStart + ;
    SECONDSTORUN
  nCase2Passes = 0
  DO WHILE m.nCase2LoopEnd > SECONDS()
    nCase2Passes = m.nCase2Passes + 1
    IsPointNearLineMDot(m.oLine, 55, 45, 1)
    IsPointNearLineMDot(m.oLine, 100, 27, 2)
    IsPointNearLineMDot(oLine, 0, 0, 1)
    IsPointNearLineMDot(oLine, 500, 7, 3)
  ENDDO
  INSERT INTO csrMDotSpeedsLarge ;
    VALUES (m.nFields, m.nCase1Passes, ;
    m.nCase2Passes)
  IF m.nFields <> 0
    USE IN csrDummy
  ENDIF
ENDFOR

A esta altura, no debería sorprendernos que cuantos más campos de la tabla abierta en el área de trabajo actual, mayor será la ventaja de la versión con mdots.

En mis pruebas, la versión mdot se ejecutó aproximadamente un 25% más de veces en con 70 campos en el área de trabajo y aproximadamente un 50% más de veces con 150 campos.

Sospecho que la razón por la que la diferencia no es tan extrema como en el ejemplo anterior, es que hay mucho más código que no son referencias de variables en este ejemplo. Es decir, el código general es más complejo. (De hecho, mientras que el ejemplo anterior logró millones de pases en cinco segundos, el ejemplo más extenso completó solo decenas de miles).

Para tener una mejor idea de la diferencia entre las dos pruebas, calculé un "tiempo por referencia a variable" aproximado para cada una. Específicamente, hice el cálculo del Listado 5, dividiendo los cinco segundos de la prueba por el producto del número de referencias a variables y el número de pasadas completadas. Por supuesto, este es solo un tiempo aproximado por referencia a variable porque hay otro código en cada prueba. Sin embargo, me permitió hacer una comparación entre las dos pruebas.

Listado 5. Esta ecuación calcula un "tiempo por referencia a variable" aproximado.

Time = TestTime/((# of variables) * passes)

Lo que encontré fue que la segunda prueba, más compleja, tomó aproximadamente un orden de magnitud más para cada referencia que la prueba más simple. Nuevamente, es probable que sea un reflejo del código adicional en el caso más complejo.

¿Qué pasa con las matrices?

El código que determina si un punto está cerca de una línea usa un par de matrices en sus cálculos. Dado que una referencia a un elemento de matriz no se puede confundir con una referencia a un campo, me pregunté si hace una diferencia usar mdots en esas referencias.

Probé agregando un tercer caso a la prueba más extensa. Está estructurado de la misma manera que las dos pruebas del Listado 4, pero llama a una tercera versión de IsPointNearLine que tiene mdots en referencias a variables escalares, pero no en referencias a elementos de matriz.

Encontré solo una pequeña diferencia entre ésta versión y la que tiene mdots en todas las referencias a variables, incluidas las matrices. La mayoría de las veces (66 de 82 casos), el que no tenía mdots en las referencias a matriz era más rápido, pero a veces el que usaba mdots en las referencias a matriz era más rápido. Eso sugiere que VFP es lo suficientemente inteligente como para no buscar (o no mirar mucho) un campo cuando se le da una referencia a matriz.

Algunas palabras sobre las pruebas de tiempo

Las pruebas en el entorno de Windows son inherentemente defectuosas. Entre el propio Windows y varios servicios que siempre se están ejecutando, cualquier resultado de prueba puede ser inexacto.

Hay dos cosas que puede hacer para obtener mejores resultados. Primero, antes de probar, apague todo lo que pueda que pueda interferir, como un cliente de correo electrónico, escaneo de virus bajo demanda, etc. Si no necesita una red para la prueba, considere desconectarse.

En segundo lugar, realice más de una prueba para cada caso. Ese consejo también es importante porque VFP almacena datos en caché, por lo que la primera vez que ejecuta un proceso que utiliza DBF, es probable que tarde más que las ejecuciones posteriores.

Como mencioné anteriormente, hice mis pruebas en dos máquinas diferentes. En ambos casos, me aseguré de que Outlook y mi cliente de Twitter estuvieran cerrados. Cuando se estaba ejecutando una prueba, no hice nada más con esa computadora, ni siquiera tocar el teclado o mover el mouse. Además, en el transcurso de la redacción, realicé cada una de mis pruebas varias veces.

Incluso con estas medidas, los resultados de las pruebas deben verse más como un indicador que como una respuesta definitiva. En este caso, debido a que la diferencia entre los resultados mdots y no mdots es tan grande, es seguro afirmar que mdots marca la diferencia. Por otro lado, la diferencia entre mdots en todas las referencias a variables y mdots solo en referencias a variables de elementos que no son de matriz es lo suficientemente pequeña como para insinuar la respuesta. Se necesitan más pruebas en un entorno más controlado para confirmar ese resultado.

Sólo tiene que utilizar mdots

Como dije al principio, ya estoy subida al carro de los mdots. Me ha pasado demasiadas veces que el código un campo, cuando me refiero a una variable y no quiero preocuparme por eso nunca más. Además, trabajo a menudo con códigos escritos originalmente por otros, por lo que incluso si adoptara una convención de nomenclatura estricta, es probable que gran parte del código que toco no lo esté usando.

Pero incluso si realmente cree que su convención de nomenclatura lo protegerá de ese problema, el hecho de que omitir mdots hace que su aplicación sea más lenta y debería reconsiderar su elección.


Copyright (C) Tamar E. Granor, Tomorrow’s Solutions, LLC.

2 comentarios :

  1. Excelente artículo, comencé a utilizar ya hace tiempo en mi código "mdot" y realmente se hace mas legible y menos ambiguo. Ahora convencido de que también es más rápido.

    ResponderBorrar
  2. Excelente artículo Tamar, es muy claro y convincente las pruebas que realizaste. En resumen, entiendo que VFP da prioridad a los mdots antes que a cualquier nomenclatura que querramos usar. Hace 25 años utilizaba mdots, luego de esos 25 años he vuelto a la programación y me encuentro con nomenclaturas que han sido elaboradas para que los programadores puedan comprender mejor su trabajo, pero hemos dejado a un lado que, quien debe entender en primer lugar nuestro código es la computadora (a través de VFP) y luego nosotros y eso se resume en velocidad. Ahora, para que sea mas claro y siempre pensando en VFP, podríamos pensar en una nomenclatura que convine tanto las mdots con cualquier otra, por lo menos para VFP.

    ResponderBorrar

Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.