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
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 |
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 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 prueba | Total de Ejecuciones | Tiempo total | Promedio (ms) |
---|---|---|---|
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 |
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 fragmento 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 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 |
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 |
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 |
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
Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.