escribir mejor código parte 2 vfp 2012

6
Escribir mejor código (Parte 2) 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 prueban? 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 )).

Upload: luis-barrera

Post on 13-Aug-2015

34 views

Category:

Documents


7 download

TRANSCRIPT

Page 1: Escribir mejor código parte 2 VFP 2012

Escribir mejor código (Parte 2)

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 prueban?

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' )NEXTlnEn = 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 prueba Total de Ejecuciones Tiempo total Promedio (ms)

Macro sustitución para valor de tabla 107000 14.8230 0.1385

EVAL() indirecto para valor de tabla 142000 11.6680 0.0822

EVAL directo para valor de tabla 96000 6.4920 0.0676

       

Macro sustitución para valor de objeto 1090000 13.7840 0.0126

Page 2: Escribir mejor código parte 2 VFP 2012

EVAL() indirecto para valor de objeto 1040000 8.7900 0.0085

EVAL directo para valor de objeto 1130000 9.1930 0.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 IFTHENELSE, a través de la � �función IIF(), la sentencia DO CASEENDCASE 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 = 1ELSE IF lnTest = 2 lnRes = 2 ELSE IF lnTest = 3 lnRes = 3 ELSE lnRes = 4 ENDIF ENDIFENDIF

Rendimiento relativo de prueba condicional

Page 3: Escribir mejor código parte 2 VFP 2012

Nombre de la prueba Total de Ejecuciones Tiempo total Promedio (ms)

Page 4: Escribir mejor código parte 2 VFP 2012

DO CASE nivel 1 282000 0.6910 0.0025

DO CASE nivel 2 242000 0.7600 0.0031

DO CASE nivel 3 275000 1.1820 0.0043

DO CASE Otherwise 247000 0.9900 0.0040

       

ICASE nivel 1 313000 0.3100 0.0015

ICASE nivel 2 281000 0.6430 0.0027

ICASE nivel 3 300000 0.7610 0.0036

ICASE Otherwise 302000 0.8510 0.0037

       

IIF ELSE nivel 1 203000 0.3100 0.0015

IIF ELSE nivel 2 235000 0.6430 0.0027

IIF ELSE nivel 3 213000 0.7610 0.0036

IIF ELSE Otherwise 227000 0.8510 0.0037

       

IIF nivel 1 213000 0.2700 0.0013

IIF nivel 2 236000 0.4240 0.0018

IIF nivel 3 209000 0.4200 0.0020

IIF Otherwise 222000 0.5100 0.0023

       

Nivel 2 de anidamiento DO CASE dentro DO CASE 67000 0.4300 0.0023

Nivel 2 de anidamiento IF... ELSE dentro IF...ELSE 56000 0.1400 0.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

Page 5: Escribir mejor código parte 2 VFP 2012

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.

  Incondicional Condicional

DO WHILE

GO TOPDO WHILE NOT EOF() luRes = cState SKIPENDDO

=SEEK( �OH�, �bigtable�, �cState� )DO WHILE cState == �OH� AND NOT EOF() lures = cState SKIPENDDO

SCAN

GO TOPSCAN luRes = cStateENDSCAN

*** SCAN FORGO TOPSCAN FOR cstate == �OH� luRes = cStateENDSCAN*** SEEK() and SCAN WHILE=SEEK( �OH�, �bigtable�, �cState� )SCAN FOR cstate == �OH� luRes = cStateENDSCAN

FOR ... NEXT

*** Utilizando GOTO ***lnRex = RECCOUNT(�bigtable�)FOR lnCnt = 1 TO lnRex GOTO (lnCnt) IF EOF() EXIT ENDIF luRes = cStateNEXT*** Utilizando SKIPlnRex = RECCOUNT( 'bigtable' )FOR lnCnt = 1 TO lnRex SKIP IF EOF() EXIT ENDIF luRes = cStateNEXT

lnRex = RECCOUNT( 'bigtable' )FOR lnCnt = 1 TO lnRex GOTO (lnCnt) IF EOF() EXIT ENDIF IF NOT cState == 'OH' LOOP ENDIF luRes = cStateNEXT

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 prueba Total de Ejecuciones Max(seg) Min(seg) Promedio (seg)

DO WHILE NOT EOF() sin control de índices 26 0.6110 0.4510 0.5160769

Page 6: Escribir mejor código parte 2 VFP 2012

SCAN...ENDSCAN sin control de índices 29 0.3610 0.3310 0.3536667

FOR...NEXT con SKIP sin control de índices 19 1.1220 0.4500 0.5746250

FOR...NEXT & GOTO sin control de índices 26 0.6600 0.4710 0.5691667

         

DO WHILE NOT EOF() con control de índices 21 2.7040 2.4240 2.5595714

SCAN...ENDSCAN con  control de índices 25 2.9640 2.3530 2.5836154

FOR...NEXT con SKIP con control de índices 9 3.0840 2.4140 2.6314444

FOR...NEXT & GOTO con control de índices 26 0.9510 0.4810 0.6214500

         

Scan FOR cState='OH' sin establecer índices 26 0.0710 0.0600 0.0680000

Scan FOR cState='OH' con  índices 19 0.1510 0.1100 0.1208571

FOR...NEXT con condición para cState='OH' 16 0.6300 0.5410 0.5867500

         

SEEK() & DO WHILE cState = 'OH' 19 0.0910 0.0700 0.0777500

SEEK() & SCAN WHILE cState = 'OH' 29 0.0900 0.0700 0.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 (DOWHILE y FORNEXT) 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.