Home About Me

The Hidden Costs That Shape Code Execution Speed

Performance tuning starts with hot spots: the code paths that run most often. A small saving in one of those places can change the total time dramatically. Three examples show why execution speed often comes from details that are easy to overlook.

Getter and setter calls have a cost

Consider this PHP code. Reading and writing a public member directly takes far less time than going through getter and setter methods:

<?php
    //dog_naive.php

    class dog {
        public $name = "";
        public function setName($name) {
            $this->name = $name;
        }
        public function getName() {
            return $this->name;
        }
    }

    $rover = new dog();
        // via Getter/Setter
    for ($x=0; $x<10; $x++) {
        $t = microtime(true);
        for ($i=0; $i<1000000; $i++) {
            $rover->setName("rover");
            $n = $rover->getName();
        }
        echo microtime(true) - $t;
        echo "\n";
    }
        // direct variable access
        for ($x=0; $x<10; $x++) {
        $t = microtime(true);
        for($i=0; $i<1000000; $i++) {
            $rover->name = "rover";
            $n = $rover->name;
        }
        echo microtime(true) - $t;
        echo "\n";
    }
?>

The difference is more than double in this kind of loop. Nothing mysterious is happening: a function call has overhead. Arguments must be passed, a stack frame is created and removed, and the processor has to jump to another location and back. Every language pays some of that cost, which is why C++ has inline functions and why Java can optimize such calls when it runs with optimizations enabled. Dynamic languages have a harder time removing the cost.

Using magic methods does not make it better:

class dog {
    private $_name = "";
    function __set($property,$value) {
        if($property == 'name') $this->_name = $value;
    }
    function __get($property) {
        if($property == 'name') return $this->_name;
    }
}

In practice, the magic method version is even slower. Dynamic-language performance is a persistent issue. If PHP needs a much higher ceiling, one option is Facebook's HipHop, which compiles PHP into C.

Function-local variables are cheaper to look up

Here is a simple Python experiment. The same loop runs inside a function and at module level. The function version takes about 1.8s:

def main():
    for i in xrange(10**8):
        pass
main()

The global version takes about 4.5s:

for i in xrange(10**8):
    pass

The exact times are not the point; the gap is. Disassembling the bytecode shows the reason. Inside the function, the loop uses STORE_FAST:

13 FOR_ITER                 6 (to 22)
16 STORE_FAST               1 (i)
19 JUMP_ABSOLUTE           13

At module level, it uses STORE_NAME:

13 FOR_ITER                 6 (to 22)
16 STORE_NAME               1 (i)
19 JUMP_ABSOLUTE           13

STORE_FAST is much faster. In a function, i lives in a local variable table and can be reached with an integer index. At global scope, i is a global name, and looking it up means searching a dictionary. If the function declares global i, the advantage largely disappears. This is not a problem in C or C++.

Sorted input can make a loop several times faster

Take this C/C++ loop:

 for (unsigned i = 0; i < 100000; ++i) {
   // primary loop
    for (unsigned j = 0; j < arraySize; ++j) {
        if (data[j] >= 128)
            sum += data[j];
    }
}

If data is sorted, the loop takes about 1.93s. If it is unsorted, it takes about 11.54s—more than five times slower. The same effect appears in Java and other compiled languages. The cause is branch prediction.

A railway switch is a useful analogy. When a train arrives, the switch operator knows which track is which but does not know where the train is going. The driver knows the destination but not which track to take. Someone has to stop the train and settle the question, which is expensive.

A railway switch, like a branch in code

Modern processors avoid that stop by guessing. If the guess is right, execution continues at full speed. If it is wrong, the pipeline has to be corrected, and that costs time. The guess is based on history: if most recent branches went one way, the processor predicts that direction again.

A switch point deciding which path execution takes

An if is like the switch. With sorted data, the condition is false for many iterations and then true for many iterations:

T = branch taken (condition true)
N = branch not taken (condition false)

data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...

= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)

With random data, the pattern is unpredictable:

data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, 133, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T,   N  ...

= TTNTTTTNTNNTTTN ...   (completely random - hard to predict)

One way to handle this is to remove the branch from the inner loop. This condition:

if (data[j] >= 128)
sum += data[j];

can become:

int t = (data[j] - 128) >> 31;
sum += ~t & data[j];

The branchless version performs about the same as the sorted branch version, whether the code is C/C++ or Java.

A compiler note: with GCC, -O3 or -ftree-vectorize can turn the branch into branchless code. VC++ 2010 does not do this.

The pattern across all three examples is the same. The largest gains usually come from understanding what the machine actually does with ordinary code—calls, name lookups, and branches—not from rewriting everything.