June 3, 2008

Lowlevel Code Optimization in WinRunner


At some moment in your automated test project you enter the phase you are questioning yourself: "Weren't we automating the tests because it is fast? Why are my tests running so slow!?!". Then it is time to review your code and to optimize it a bit.

In this post, I want to discuss low level optimization. As WinRunner does not have a smart compiler (it is more an interpreted language then a compiled one), we have to do all optimizations by ourself.

1. Order of function calls in a condition
When you use and and or constructs in conditions ('if' statements for example), you have to think about the order:

Let's say, we have two checks, a time consuming (fncSlowCheck) and a fast one (fncFastCheck).
When you use an AND or an OR construct, you need to put the fastest compare first:

if (fncFastCheck() == E_OK && fncSlowCheck() == E_OK) { ... }

if (fncFastCheck() == E_OK || fncSlowCheck() == E_OK) { ... }

WinRunner will first evaluate the fast function. When this evaluates to false, the second function will not be called in case of the AND construction, because the total if statement can never become TRUE, and the if statement is stepped over.
With the OR, it is just the other way around. When the first function evaluates to TRUE, the second function will not be called.

This behavior is called lazy evaluation and is something you have to keep in mind when you use functions in conditional statements that can impact the application under test.

if (edit_set(myEdit1, "foo") != E_OK && edit_set(myEdit2, "bar") != E_OK) {
    write2report("Something went wrong setting myEdit1 and/or myEdit2", ERROR)
}

Besides this is a crappy way of reporting, the second edit will never be set as soon the first edit_set() evaluates to an error.

2. Use switch / case constructs. They are fast!
The reason why switch / case are fast is because jmp (jump) commands in assembly are faster than cmp (compare) commands used with each loop iteration. Combined with the so called "fall through" mechanism, they cannot be beaten by loops. For a detailed article, see Duff's device on wikipedia.

3. ++i is faster than i++
Make it a good habit to use ++{variable} in your for constructs:

for (i = 0 ; i < 100 ; ++i)

I absolutely noticed the difference since I had some large multi dimensional arrays I had to iterate through.
One note: Keep in mind the order of handling of the incrementation. Do not blindly search and replace all {variable}++ with ++{variable}, this will mess up your test.

4. Calls to external functions can be slow
When you use external functions in time critical processes, it is a good habit to check the performance of these functions. Once, I had external and() and or() functions (they are not provided by WinRunner) and I used them to make a bit collection of matches of a table row. With each table row check, the correct bit was set to 1 or 0 in case of a match or a non-match.
But this rowcheck function was very slow. More then 3 minutes for a 25x25 table for example.

First, I didn't bother about the bad performance, we did a lot within a check: Negative checks, regular expressions and other exotic stuff to support the testers. But the rowcheck function got used more and more and the long idle times became annoying.
We created a lookup table for the powers of 2, optimized the loops and conditional statements, but it still underperformed.

Then, we measured the time for a "x = and(a, b);" call and it was 1 tenth of a second. This means more then one minute in case of 25x25 checks, and we not only used it onced, but three times in one iteration.
I assumed that the and() function had to be fast, because it was fast in C, the language the external lib was written in.
After changing the bit collection through and()s and or()s to an array and performing calculations on the array (the product as an and(), the sum for an or()) the performance was increased to 23 seconds for a 25x25 table check. Even with regular expression and negative testing in place.

5. Function calls can be slow
Whenever a function is called, a function is created on the stack, initialized, executed and destroyed. Processors are fast nowadays, but it still takes some time.
I used to use isEmpty({variable}) and isNotEmpty({variable}) for a check on empty or not. The only thing the function did was:

public function isEmpty(inValue) {
    return (inValue == "");
}

It seemed smart on that moment, because if we created other definitions for empty, we just have to enter it in the function to implement it everywhere in the system. Unfortunately, we never came up with other definitions. We noticed, the isEmpty(myVariable) function was seven times as slow then a normal myVariable == EMPTY statement (EMPTY equals "" in our system).
With this knowledge we replaced all isEmpty() functions in time critical functionality, such as the table row search function mentioned above.

A little side note:
    myVariable == EMPTY
or
    myVariable == ""
does not make any difference in WinRunner.

Last word
As mentioned before, this is how you optimize code on a very low level. As long as your design is not right, use senseless synchronization timers or bad synchronization mechanisms, the performance of your test will not increase significantly. Optimization must be in balance on all levels of the test process; From scripting automated tests to requirements and risks.

No comments: