# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .jl
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Julia 0.6.0
#     language: julia
#     name: julia-0.6
# ---

# # Lecture 2: Type Stability

# ## Defeating type inference: Type instabilities
#
# To get good performance, there are some fairly simple rules that you need to follow in Julia code to avoid defeating the compiler's type inference.   See also the [performance tips section of the Julia manual](http://docs.julialang.org/en/stable/manual/performance-tips/).
#
# Three of the most important are:
#
# * Don't use (non-constant) global variables in critical code — put your critical code into a function (this is good advice anyway, from a software-engineering standpoint).  The compiler assumes that a **global variable can change type at any time**, so it is always stored in a "box", and "taints" anything that depends on it.
#
# * Local variables should be "type-stable": **don't change the type of a variable inside a function**.  Use a new variable instead.
#
# * Functions should be "type-stable": **a function's return type should only depend on the argument types, not on the argument values**.
#
# To diagnose all of these problems, the `@code_warntype` macro that we used above is your friend.  If it labels any variables (or the function's return value) as `Any` or `Union{...}`, it means that the compiler couldn't figure out a precise type.
#
# The third point, type-stability of functions, leads to lots of important but subtle choices in library design.  For example, consider the (built-in) `sqrt(x)` function, which computes $\sqrt{x}$:

sqrt(4)

# You might think that `sqrt(-1)` should return $i$ (or `im`, in Julia syntax).  (Matlab's `sqrt` function does this.)  Instead, we get:

sqrt(-1)

sqrt(-1 + 0im)

# Why did Julia implement `sqrt` in this silly way, throwing an error for negative arguments unless you add a zero imaginary part?  Any reasonable person wants an imaginary result from `sqrt(-1)`, surely?
#
# The problem is that defining `sqrt` to return an imaginary result from `sqrt(-1)` would **not be type stable**: `sqrt(x)` would return a real result for non-negative real `x`, and a complex result for negative real `x`, so the **return type would depend on the value of `x`** and **not just its type.**
#
# That would defeat type inference, not just for the `sqrt` function, but for **anything the sqrt function touches**.  Unless the compiler can somehow figure out `x ≥ 0`, it will have to either store the result in a "box" or compile two branches of the result.  Let's see how that works by defining our own square-root function:

mysqrt(x::Complex) = sqrt(x)
mysqrt(x::Real) = x < 0 ? sqrt(complex(x)) : sqrt(x)

# This definition is an example of Julia's [multiple dispatch style](http://docs.julialang.org/en/stable/manual/methods/), which in some sense is a generalization of object-oriented programming but focuses on "verbs" (functions) rather than nouns.  We will discuss this more in a later lecture.
#
# The `::Complex` and `::Real` are argument-type declarations.  Such declarations are **not related to performance**, but instead **act as a "filter"** to allow us to have one version of `mysqrt` for complex arguments and another for real arguments.

mysqrt(2)

mysqrt(-2)

mysqrt(-2+0im)

# Looks great, right?  But let's see what happens to type inference in a function that calls `mysqrt` instead of `sqrt`:

slowfun(x) = mysqrt(x) + 1
@code_warntype slowfun(2)

# Because the compiler **doesn't know at compile-time that x is positive** (at compile-time it **uses only types, not values**, it doesn't know whether the result is real (`Float64`) or complex (`Complex{Float64}`) and has to store it in a "box".  This kills performance.

i = 3

typeof(i)

i^100000

i = typemax(Int)

i + 1

# ## Defining our own types
#
# Let's define our own type to represent a **"point" in two dimensions**.  Each point will have an $(x,y)$ location.  So that we can use the points with our `sum` functions above, we'll also define `+` and `zero` functions to do the obvious **vector addition**.
#
# One such definition in Julia is:

# +
mutable struct Point1
    x
    y
end
Base.:+(p::Point1, q::Point1) = Point1(p.x + q.x, p.y + q.y)
Base.zero(::Type{Point1}) = Point1(0,0)

Point1(3,4)
# -

Point1(3,4) + Point1(5,6)

# Our type is very generic, and can hold any type of `x` and `y` values:

Point1(3.7, 4+5im)

# Perhaps too generic:

Point1("x", [3,4,5])

# Since `x` and `y` can be *anything*, they must be **pointers to "boxes"**.  This is **bad news for performance**.
#
# A `mutable struct` is *mutable*, which means we can create a `Point1` object and then change `x` or `y`:

p = Point1(3,4)

p.x = 7
p

q = p

# This means that every reference to a `Point1` object must be a *pointer* to an object stored elsewhere in memory, because *how else would we "know" when an object changes?*  Furthermore, an **array of `Point1` objects must be an array of pointers** (which is **bad news for performance** again):

P = [p,p,p]

P

Q = [deepcopy(p), deepcopy(p)]

p.x = -3
P

Q

# Let's test this out by creating an array of `Point1` objects and summing it.  Ideally, this would be about twice as slow as summing an equal-length array of numbers, since there are twice as many numbers to sum.  But because of all of the boxes and pointer-chasing, it should be far slower.
#
# To create the array, we'll call the `Point1(x,y)` constructor with our array `a`, using Julia's ["dot-call" syntax](http://docs.julialang.org/en/stable/manual/functions/#dot-syntax-for-vectorizing-functions) that applies a function "element-wise" to arrays:

a = rand(10^7)
a1 = Point1.(a, a)

using BenchmarkTools, Compat

@btime sum($a1)

# The time is at least **50× slower** than we would like, but consistent with our other timing results on "boxed" values from last lecture.

# ### An imperfect solution: A concrete immutable type
#
# We can avoid these two problems by:
#
# * Declare the types of `x` and `y` to be *concrete* types, so that they don't need to be pointers to boxes.
# * Declare our Point to be an [immutable](https://en.wikipedia.org/wiki/Immutable_object) type (`x` and `y` cannot change), so that Julia is not forced to make every reference to a Point into a pointer: just `struct`, not `mutable struct`:

# +
struct Point2
    x::Float64
    y::Float64
end
Base.:+(p::Point2, q::Point2) = Point2(p.x + q.x, p.y + q.y)
Base.zero(::Type{Point2}) = Point2(0.0,0.0)

Point2(3,4)
# -

Point2(3,4) + Point2(5,6)

p = Point2(3,4)
P = [p,p,p]

p.x = 6 # gives an error since p is immutable

sizeof(P) / length(P)

# If this is working as we hope, then summation should be much faster:

a2 = Point2.(a,a)
@btime sum($a2)

# Now the time is **only about 10ms**, only slightly more than twice the cost of summing an array of individual numbers of the same length!
#
# Unfortunately, we paid a big price for this performance: our `Point2` type only works with *a single numeric type* (`Float64`), much like a C implementation.

# ### The best of both worlds: Parameterized immutable types
#
# How do we get a `Point` type that works for *any* type of `x` and `y`, but at the same time allows us to have an array of points that is concrete and homogeneous (every point in the array is forced to be the same type)?  At first glance, this seems like a contradiction in terms.
#
# The answer is not to define a *single* type, but rather to **define a whole family of types** that are *parameterized* by the type `T` of `x` and `y`.  In computer science, this is known as [parametric polymorphism](https://en.wikipedia.org/wiki/Parametric_polymorphism).  (An example of this can be found in [C++ templates](https://en.wikipedia.org/wiki/Template_%28C%2B%2B%29).)
#
# In Julia, we will define such a family of types as follows:

# +
struct Point3{T<:Real}
    x::T
    y::T
end
Base.:+(p::Point3, q::Point3) = Point3(p.x + q.x, p.y + q.y)
Base.zero{T}(::Type{Point3{T}}) = Point3(zero(T),zero(T))

Point3(3,4)
# -

# Here, `Point3` is actually a family of subtypes `Point{T}` for different types `T`.   The notation `<:` in Julia means "is a subtype of", and hence `T<:Real` means that we are constraining `T` to be a `Real` type (a built-in *abstract type* in Julia that includes e.g. integers or floating point).

Point3(3,4) + Point3(5.6, 7.8)

# Now, let's make an array:

a3 = Point3.(a,a)

typeof(a3)

# Note that the type of this array is `Array{Point3{Float64},1}` (we could equivalently write this as `Vector{Point3{Float64}}`, since `Vector{T}` is a synonym for `Array{T,1}`).  You should learn a few things from this:
#
# * An `Array{T,N}` in Julia is itself a parameterized type, parameterized by the element type `T` and the dimensionality `N`.
#
# * Since the element type `T` is encoded in the `Array{T,N}` type, the element type does not need to be stored in each element.  That means that the `Array` is free to store an array of "inlined" elements, rather than an array of pointers to boxes.  (This is why `Array{Float64,1}` earlier could be stored in memory like a C `double*`.
#
# * It is still important that the element type be `immutable`, since an array of mutable elements would still need to be an array of pointers (so that it could "notice" if another reference to an element mutates it).

@btime sum($a3)

# Hooray! It is again **only about 10ms**, the same time as our completely concrete and inflexible `Point2`.

p = Point3{Int8}(3,5)

p = Point3{Integer}(3,Int8(5))

typeof(p.x)

typeof(p.y)

3 + 7im

typeof(3 + 7im)

@which sqrt(3 + 4im)

supertype(Point3{Int8})

# ## Another type of "type instability"

function mysum_slow(a)
    s = 0
    for x in a
        s += x
    end
    return s
end

mysum_slow([3,4,5])

mysum_slow([3.3,4,5])

@code_warntype(mysum_slow([3.3,4,5]))

function mysum_fast(a)
    s = zero(eltype(a))
    for x in a
        s += x
    end
    return s
end

@code_warntype mysum_fast([3.3,4,5])

mysum_fast([rand(3,3) for i = 1:10])

sum([rand(3,3) for i = 1:10])

sum(Matrix{Float64}[])

StarcoderdataJupyterScriptsDedupFiltered