Improve optimization

* Add function-level optimization at the end of lowering to reduce memory usage and enhance final code quality.

This allow a x3 speedup for the following code:

```python
n = 10000
@jit
def foo(a, b, c):
    for i in range(n):
        c[i] = a[i] + b[i]


a = np.arange(n, dtype='float32')
b = a
c = np.empty_like(a)

foo(a, b, c)
```

because of the use of SIMD.
If we are debugging a LLVM dump, do we want to dump before or after the function optimization step?