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.
I haven't seen any speedup, yet.  But it reduces the size of the IR, which needs to be sent over to libNVVM.  That should reduce a bit of overhead.