17 de mayo de 2006

Escribir mejor código (Parte 2)

Artículo original: Writing better code (Part 2)
http://weblogs.foxite.com/andykramek/archive/2006/03/13/1283.aspx
Autor: Andy Kramek
Traducido por: Ana María Bisbé York

Como dije en mi primer artículo de esta pequeña serie, una de las grandes cosas sobre Visual FoxPro es que normalmente hay varias formas diferentes de hacer lo mismo. Desafortunadamente, es al mismo tiempo, una de sus peores cosas, al menos, si hubiera sólo una vía de hacer algo, no habría que pensar mucho sobre cómo hacerlo. Algo que he notado cuando trabajo sobre un código (mío o de otro) es lo fácil que caemos los desarrolladores en las plantillas de hacer algo. Una vez que averiguamos la forma de solucionar un tipo de operación especial, tendemos a seguirlo inquestionablemente.

He tenido ocasión, recientemente, de revisar algunos de los comandos más básicos de Visual FoxPro y examinar críticamente su rendimiento. He encontrado algunos resultados sorprendentes y como espero que usted también lo encuentre, he documentado los resultados de mi investigación.

¿Cómo se ejecutaron las pruebas?

Existen varios problemas al intentar de estimar el rendimiento relativo de comandos y funciones Visual FoxPro. La solución que he adoptado fue fijar mis pruebas como funciones individuales, y entonces utilizar un generador de número aleatorio para determinar qué método es llamado. Al ejecutar pruebas individuales muchas miles de veces, en secuencias aleatorias, los efectos de atrapar y pre-compilar son canceladas efectivamente. Utilicé la misma metodología para todas las pruebas y no me molestaré en volver a mencionarlo.

Por ejemplo, el código empleado por Macro Expansion Test #5 (InDirect EVAL() para Object Value) fue:
loObj = CREATEOBJECT( 'textbox' )
loObj.Value = "Esta es una prueba para  valor de cadena"
lcObj = "loObj"
lnSt = SECONDS()
FOR lnReps = 1 TO 10000
  luVal = EVALUATE( lcObj + '.Value' )
NEXT
lnEn = SECONDS()
(Nota: 10,000 iteraciones fue suficiente para una medición razonable - el resultado fue entonces, promediado para cada prueba). Los resultados de cada prueba se escribieron en una tabla y luego los agregué y los promedié para obtener los resultados ilustrados en este artículo.

Expresiones de nombre vs Macro sustitución

Una de las grandes fortalezas de Visual FoxPro es la capacidad de utilizar vía indirecta (indirection) en tiempo de ejecución. Ahora, todo desarrollador VFP  experiementado sabe que utilizando una expresión de nombre (por ejemplo, la función EVALUATE()) es "más rápida" que la macro sustitución (operador &) para hacer esto. Pero la cuestión es, eso importa realmente? Me parece que hay varios escenarios en los que podemos querer utilizar la vía indirecta al trabajar con datos:
  • Utilizar una referencia para obtener un valor: por ejemplo, Devolver el contenido de un campo utilizando solo su nombre
  • Utilizar una referencia para dirigirse a un objeto, por ejemplo, obtener el valor de una propiedad
  • Utilizar una referencia para encontrar algún elemento específico de un dato: por ejemplo, en un comando LOCATE
La siguiente tabla muestra los resultados de ejecutar la misma operación en tres formas diferentes. Primero, utilizando la macro sustitución tradicional de VFP, luego, utilizando la función EVALUATE indirectamente (por ejemplo EVAL(lcFieldName)) y finalmente utilizando una evaluación indirecta (por ejemplo, EVAL( FIELD( nn )).

Rendimiento relativo de macro expansión y expresiones de nombres

Nombre de la pruebaTotal de EjecucionesTiempo totalPromedio (ms)
Macro sustitución para valor de tabla10700014.82300.1385
EVAL() indirecto para valor de tabla14200011.66800.0822
EVAL directo para valor de tabla960006.49200.0676
Macro sustitución para valor de objeto109000013.78400.0126
EVAL() indirecto para valor de objeto10400008.79000.0085
EVAL directo para valor de objeto11300009.19300.0081

Macro sustitución en LOCATE
1010000
8.1700
0.0081
EVAL() indirecto en LOCATE
1140000
9.1250
0.0080
EVAL() directo en LOCATE
1140000
9.1980
0.0081

Lo primero que observo es que no hay un método intrínsicamente lento al considerar cada procedimiento por separado (lo peor en este escenario muestra una diferencia de no más de 0.05 milisegundos entre la mejor y la peor tecnología - ¡difícilmente detectable!). Sin embargo, al ejecutar repetidamente (dentro de un lazo apretado, por ejemplo), está claro que EVAL() tiene un rendimiento significativamente superior al  tratar con cualquier tabla de datos u objetos y por tanto, el decir que EVAL() es más rápido que &expansión está obviamente bien fundamentado.

Es muy interesante, que existe una diferencia detectable entre utilizar una evaluación directa e indirecta y mi preferencia personal, aunque solo sea  para mejorar la legibilidad del código, es por tanto, utilizar el método indirecto. En otras palabras, aceptaré (la pequeña) mejora de rendimiento para escribir código de esta forma:
lcField = FIELD( lnCnt )
luVal = EVALUATE( lcField )
en preferencia de la menos obvia:
luVal = EVALUATE( FIELD( lnCnt ))
La conclusión principal es, por tanto, que no existe beneficio en utilizar macro sustitución cuando está disponible EVAL() como opción. Un punto secundario para notar que es como el número de repeticiones aumenta la diferencia rápidamente se torna significativa y que EVAL() debe siempre ser utilizado al procesar repetidamente.

Evaluación condicional

Al evaluar alguna condición y establecer el control dependiendo de los resultados, VFP 9.0 tiene casi una confusión de riquezas. Tenemos ahora cuatro vías diferentes de evaluar alguna condición para el tradicional IF…THEN…ELSE, a través de la función IIF(), la sentencia DO CASE…ENDCASE y finalmente la nueva función ICASE(). La cuestión es, por tanto, ¿importa lo que utilizamos en una situación dada? La velocidad de ejecución de estos comandos fueron comparadas utilizando una prueba de múltiple nivel en forma general:
IF lnTest = 1
  lnRes = 1
ELSE
  IF lnTest = 2
    lnRes = 2
  ELSE
    IF lnTest = 3 
      lnRes = 3
    ELSE
      lnRes = 4
    ENDIF
  ENDIF
ENDIF

Rendimiento relativo de prueba condicional

Nombre de la pruebaTotal de EjecucionesTiempo totalPromedio (ms)
DO CASE nivel 12820000.69100.0025
DO CASE nivel 22420000.76000.0031
DO CASE nivel 32750001.18200.0043
DO CASE Otherwise2470000.99000.0040
ICASE nivel 13130000.31000.0015
ICASE nivel 22810000.64300.0027
ICASE nivel 33000000.76100.0036
ICASE Otherwise3020000.85100.0037
IIF ELSE nivel 12030000.31000.0015
IIF ELSE nivel 22350000.64300.0027
IIF ELSE nivel 32130000.76100.0036
IIF ELSE Otherwise2270000.85100.0037
IIF nivel 12130000.27000.0013
IIF nivel 22360000.42400.0018
IIF nivel 32090000.42000.0020
IIF Otherwise2220000.51000.0023
Nivel 2 de anidamiento DO CASE dentro DO CASE670000.43000.0023
Nivel 2 de anidamiento IF... ELSE dentro IF...ELSE560000.14000.0025
Una vez más, lo primero que observo es que no hay nada aquí exactamente lento. Sin embargo, hay algunas diferencias y un par de anomalías que merece la pena notar.

Algo que vemos es que DO ... CASE es invariablemente la vía más lenta de controlar este tipo de pruebas aunque es la más legible. Puede que sea raro, la vía más rápida de ejecutar este tipo de pruebas es la función ICASE() - aunque, por supuesto, esta función, al igual que IIF() es irrelevante cuando hay un bloque de código que sigue a la prueba condicionada. El problema fundamental tanto para  IIF() como para ICASE() es, por supuesto, la legibilidad del código. Por ejemplo, la función ICASE que duplica el IF .... ELSE de antes es:
lnRes = ICASE( lnTest = 1, 1, lnTest = 2, 2, lnTest = 3, 4 )
el que es difícilmente comprensible sin un esfuerzo considerable. Por supuesto, el caso más frecuente para este tipo de lógica es dentro de una consulta SQL y en este caso, es vital aumentar la velocidad de ejecución.

La anomalía a la que me refería antes es que con todos los métodos verificados, el tiempo de ejecución aumenta en la medida que se bajan los niveles - que es precisamente lo que deseamos. Después de todo esto tiene sentido que tome más tiempo tres pruebas que dos, y más para dos que para una. Sin embargo, en ambos casos CASE e ICASE la condición "Otherwise" se ejecuta más rápido que el nivel que le precede inmediatamente, no mucho más rápido admisiblemente; pero indiscutiblemente y consistentemente más rápido. La primera vez que observé este fenómeno fue en VFP 7.0 y es interesante y persiste no sólo en la sentencia CASE en VFP 9.0; pero además en el nuevo ICASE. ¿Qué significa esto? No lo se; pero es un fenómeno observado.

Las conclusiones aquí son, primero, que si puede continuar con esto, utilice las funciones en preferencia a comandos planos. Segundo, si está haciendo esto repetidamente, entonces un IF ...ELSE escalado aporta mejor rendimiento que un DO CASE - especialmente si el anidamiento excede un nivel. La advertencia aquí es que código más complejo se vuelve más difícil de leer y comprender al utilizar un anidamiento IF ...ELSE o las funciones. Mi preferencia personal aquí es que utilice la estructura CASE como un comentario en mi código; pero realmente ejecuta utilizando uno de los otros métodos.

Enlazando datos

Una de las operaciones fundamentales en casi todas las aplicaciones centradas en datos es el requisito de recibir un conjunto de registros y luego procesarlos secuencialmente. En Visual FoxPro existen básicamente tres formas de hacer esto, el comando DO WHILE original de xBase, el comando más reciente SCAN y el lazo FOR ...NEXT, aun más reciente (aunque ya era viejo en J). Existen, por supuesto muchas variantes y variaciones; pero estas tres construcciones proporcionan la base para todo lo demás. En el pequeño trozo de pruebas siguiente he evaluado la velocidad con cada registro en una tabla grande de nombre y dirección que podría ser visitada y recibir una columna específica (y, para esta prueba, inmediatamente descartada).

Cada una de las tres construcciones fueron comprobadas incondicionalmente (es decir, desde el primer registro hasta el último) y por un conjunto de registros que cumplen una condición específica (en este caso para direcciones en el estado de Ohio).

La razón para esta prueba en particular, es que representa los dos escenarios más comunes. Sin embargo, a diferencia de otras comparaciones que hemos hecho con fechas, el código requerido para esta operación es diferente para cada opción.
IncondicionalCondicional
DO WHILE
GO TOP
DO WHILE NOT EOF()
  luRes = cState
  SKIP
ENDDO
=SEEK( ‘OH’, ‘bigtable’, ‘cState’ )
DO WHILE cState == ‘OH’ AND NOT EOF()
  lures = cState
  SKIP
ENDDO
SCAN
GO TOP
SCAN
  luRes = cState
ENDSCAN
*** SCAN FOR
GO TOP
SCAN FOR cstate == ‘OH’
  luRes = cState
ENDSCAN
*** SEEK() and SCAN WHILE
=SEEK( ‘OH’, ‘bigtable’, ‘cState’ )
SCAN FOR cstate == ‘OH’
  luRes = cState
ENDSCAN
FOR ... NEXT
*** Utilizando GOTO ***
lnRex = RECCOUNT(‘bigtable’)
FOR lnCnt = 1 TO lnRex
  GOTO (lnCnt)
  IF EOF()
    EXIT
  ENDIF
  luRes = cState
NEXT
*** Utilizando SKIP
lnRex = RECCOUNT( 'bigtable' )
FOR lnCnt = 1 TO lnRex
  SKIP
  IF EOF()
    EXIT
  ENDIF
  luRes = cState
NEXT
lnRex = RECCOUNT( 'bigtable' )
FOR lnCnt = 1 TO lnRex
  GOTO (lnCnt)
  IF EOF()
    EXIT
  ENDIF
  IF NOT cState == 'OH'
    LOOP
  ENDIF
  luRes = cState
NEXT
La tabla utilizada tiene 125 000 registros y por tanto es moderadamente grande para este tipo de operación. Los resultados pueden sorprenderlo, ¡a mi me sorprendieron!

Rendimiento en pruebas de procesamientos con Ciclos

Nombre de la pruebaTotal de EjecucionesMax(seg)Min(seg)Promedio (seg)
DO WHILE NOT EOF() sin control de índices260.61100.45100.5160769
SCAN...ENDSCAN sin control de índices290.36100.33100.3536667
FOR...NEXT con SKIP sin control de índices191.12200.45000.5746250
FOR...NEXT & GOTO sin control de índices260.66000.47100.5691667
DO WHILE NOT EOF() con control de índices212.70402.42402.5595714
SCAN...ENDSCAN con  control de índices252.96402.35302.5836154
FOR...NEXT con SKIP con control de índices93.08402.41402.6314444
FOR...NEXT & GOTO con control de índices260.95100.48100.6214500
Scan FOR cState='OH' sin establecer índices260.07100.06000.0680000
Scan FOR cState='OH' con  índices190.15100.11000.1208571
FOR...NEXT con condición para cState='OH'160.63000.54100.5867500
SEEK() & DO WHILE cState = 'OH'190.09100.07000.0777500
SEEK() & SCAN WHILE cState = 'OH'290.09000.07000.0735000
Hay varios aspectos a resaltar de esta tabla.

Lo primero, al procesar todos los registros en una tabla, el impacto de controlar índices es significativo si está empleando algún lazo de control que confíe en el comando SKIP - que es explícito en (DO…WHILE y FOR…NEXT) o implícito en (SCAN…ENDSCAN).

Segundo, SCAN FOR es además más lento cuando hay control de índices.  En esas pruebas hay un índice optimizable en la columna utilizada e incluso si SCAN es optimizable, al agregarle control de índice ralentiza el proceso  notablemente.

Tercero, el control de índices no tiene importancia cuando estamos usando SEEK() y la cláusula WHILE con SCAN o DO WHILE. Esto tiene una pequeña diferencia cuando empleamos deliberadamente el mismo rendimiento y usualmente necesitamos un índice de control al emplear en cualquier caso, un WHILE como alcance.

Las siguientes conclusiones se pueden derivar de estos resultados:
  • Al procesar todos los registros. Asegúrese de que no existe control de índices al utilizar un comando que incluya un SKIP. Probablemente la mejor generalización es utilizar el lazo FOR, con un GOTO, ya que está esencialmente NO afectado por la presencia o ausencia de índices controlados.
  • La forma más eficiente de controlar procesamiento selectivo depende de si hay un índice disponible para que haya un SEEK(). Si lo hace, entonces utilice la combinación de SEEK() con alcance WHILE de lo contrario utilice SCAN FOR sin controlar índices en la tabla.

No hay comentarios. :

Publicar un comentario