{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Lecture 2: Code specialization\n", "\n", "In lecture 1, we learned how the semantics of types affect performance — it was not *possible* to implement a C-speed `sum` function, for example, unless (a) the element type is attached to the array as a whole so that (b) the element data can be stored consecutively in memory and (b) the computional code can be specialized at compile-time for `+` on this element type.\n", "\n", "For code to be fast, however, it must also be possible to take advantage of this information in your own code. This is obviously possible in statically typed languages like C, which are compiled to machine instructions ahead of time with type information declared explicitly to the compiler. It is more subtle in a language like Julia, where the programmer does *not* usually explicitly declare type information.\n", "\n", "In particular, we will talk about how Julia achieves both fast code and *type-generic code* (e.g. a `sum` function that works on any container of any type supporting `+`, even user-defined types), by aggressive automated **code specialization** by the Julia compiler." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A type-generic sum function\n", "\n", "Let's again look at our simple hand-written `sum` function." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "mysum (generic function with 1 method)" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function mysum(a)\n", " s = zero(eltype(a))\n", " for x in a\n", " s += x\n", " end\n", " return s\n", "end" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5.0008825087619815e6" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a = rand(10^7)\n", "mysum(a)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "true" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mysum(a) ≈ sum(a)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 3.908 ms (0 allocations: 0 bytes)\n", " 12.330 ms (0 allocations: 0 bytes)\n" ] }, { "data": { "text/plain": [ "5.0008825087619815e6" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "using BenchmarkTools\n", "@btime sum($a)\n", "@btime mysum($a)" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Hooray! Basically the same speed as Julia's built-in `sum` function and `numpy.sum`! And it only required **7 lines of code**, some care with types, and a very minor bit of wizardry with the `@simd` tag to get the last factor of two.\n", "\n", "Moreover, the code is still **type generic**: it can sum any container of any type that works with addition. For example, it works for complex numbers, which are about two times slower as you might expect (since each complex addition requires two real additions):" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 12.041 ms (0 allocations: 0 bytes)\n" ] }, { "data": { "text/plain": [ "4.997905404810494e6 + 4.999604643441703e6im" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "z = rand(Complex{Float64}, length(a));\n", "@btime mysum($z)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And we **didn't have to declare any types** of any arguments or variables; the compiler figured everything out. How?" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Set([2, 17, 6, 24])" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "s = Set([2, 17, 6 , 24])" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Set{Int64}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "typeof(s)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(true, false)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "2 in s, 13 in s" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "49" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mysum(s)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Type inference and specialization\n", "\n", "To go any further, you need to understand something very basic about how Julia works. Suppose we define a very simple function:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "f (generic function with 1 method)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f(x) = x + 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We didn't declare the type of `x`, and so our function `f(x)` will work with **any type of `x`** (as long as the `+ 1` operation is defined for that type):" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "4" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f(3) # x is an integer (technically, a 64-bit integer)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "4.1" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f(3.1) # x is a floating-point value (Float64)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "ename": "MethodError", "evalue": "MethodError: no method matching +(::Array{Int64,1}, ::Int64)\nClosest candidates are:\n +(::Any, ::Any, !Matched::Any, !Matched::Any...) at operators.jl:502\n +(!Matched::Complex{Bool}, ::Real) at complex.jl:292\n +(!Matched::Missing, ::Number) at missing.jl:93\n ...", "output_type": "error", "traceback": [ "MethodError: no method matching +(::Array{Int64,1}, ::Int64)\nClosest candidates are:\n +(::Any, ::Any, !Matched::Any, !Matched::Any...) at operators.jl:502\n +(!Matched::Complex{Bool}, ::Real) at complex.jl:292\n +(!Matched::Missing, ::Number) at missing.jl:93\n ...", "", "Stacktrace:", " [1] f(::Array{Int64,1}) at ./In[10]:1", " [2] top-level scope at In[13]:1" ] } ], "source": [ "f([1,2,3]) # x is an array of integers" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "ename": "MethodError", "evalue": "MethodError: no method matching +(::String, ::Int64)\nClosest candidates are:\n +(::Any, ::Any, !Matched::Any, !Matched::Any...) at operators.jl:502\n +(!Matched::Complex{Bool}, ::Real) at complex.jl:292\n +(!Matched::Missing, ::Number) at missing.jl:93\n ...", "output_type": "error", "traceback": [ "MethodError: no method matching +(::String, ::Int64)\nClosest candidates are:\n +(::Any, ::Any, !Matched::Any, !Matched::Any...) at operators.jl:502\n +(!Matched::Complex{Bool}, ::Real) at complex.jl:292\n +(!Matched::Missing, ::Number) at missing.jl:93\n ...", "", "Stacktrace:", " [1] f(::String) at ./In[10]:1", " [2] top-level scope at In[14]:1" ] } ], "source": [ "f(\"hello\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How can a function like `f(x)` work for any type? In Python, `x` would be a \"box\" that could contain anything, and it would then look up *at runtime* how to compute `x + 1`. But we just saw that untyped Julia `sum` code could be fast.\n", "\n", "The secret is **just-in-time (JIT) compilation**. The first time you call `f(x)` **with a new type of argument** `x`, it will **compile a new version of `f` specialized for that type**. The *next* time it calls `f(x)` with the same argument type\n", "\n", "So, right now, after evaluating the above code, we have *three* versions of `f` compiled and sitting in memory: one for `x` of type `Int` (we say `x::Int` in Julia), one for `x::Float64`, and one for `x::Vector{Int}`.\n", "\n", "We can even see what the compiled code for `f(x::Int)` looks like, either the [compiler (LLVM) bytecode](https://en.wikipedia.org/wiki/LLVM) or the low-level (below C!) [assembly code](https://en.wikipedia.org/wiki/Assembly_language):" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "; Function f\n", "; Location: In[10]:1\n", "define i64 @julia_f_35737(i64) {\n", "top:\n", "; Function +; {\n", "; Location: int.jl:53\n", " %1 = add i64 %0, 1\n", ";}\n", " ret i64 %1\n", "}\n" ] } ], "source": [ "@code_llvm f(1)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\t.section\t__TEXT,__text,regular,pure_instructions\n", "; Function f {\n", "; Location: In[10]:1\n", "; Function +; {\n", "; Location: In[10]:1\n", "\tdecl\t%eax\n", "\tleal\t1(%edi), %eax\n", ";}\n", "\tretl\n", "\tnopw\t%cs:(%eax,%eax)\n", ";}\n" ] } ], "source": [ "@code_native f(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's break this down. When you tell Julia's compiler that `x` is an `Int`, it:\n", "\n", "* It knows `x` fits into a 64-bit CPU register (and is passed to the function via a register).\n", "\n", "* Looks at `x + 1`. Since `x` and `1` are both `Int`, it knows it should call the `+` function for two `Int` values. This corresponds to *one machine instruction* `leaq` to add two 64-bit registers.\n", "\n", "* Since the `+` function here is so simple, it won't bother to do a function call. It will [inline](https://en.wikipedia.org/wiki/Inline_expansion) the `(+)(Int,Int)` function into the compiled `f(x)` code.\n", "\n", "* Since it now knows what `+` function it is calling, it knows that the *result* of the `+` is *also* an `Int`, and it can return it via register.\n", "\n", "This process works recursively if we define a new function `g(x)` that calls `f(x)`:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "4" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "g(x) = f(x) * 2\n", "g(1)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "; Function g\n", "; Location: In[17]:1\n", "define i64 @julia_g_35851(i64) {\n", "top:\n", "; Function *; {\n", "; Location: int.jl:54\n", " %1 = shl i64 %0, 1\n", " %2 = add i64 %1, 2\n", ";}\n", " ret i64 %2\n", "}\n" ] } ], "source": [ "@code_llvm g(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When it specialized `g` for `x::Int`, it not only figured out what `f` function to call, it not only *inlined* `f(x)` *into `g`*, but the compiler was smart enough to *combine the two additions* into a single addition `x + 4`." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "; Function h\n", "; Location: In[19]:1\n", "define i64 @julia_h_35853(i64) {\n", "top:\n", "; Function g; {\n", "; Location: In[17]:1\n", "; Function *; {\n", "; Location: int.jl:54\n", " %1 = shl i64 %0, 2\n", ";}}\n", "; Function *; {\n", "; Location: int.jl:54\n", " %2 = add i64 %1, 4\n", ";}\n", " ret i64 %2\n", "}\n" ] } ], "source": [ "h(x) = g(x) * 2\n", "@code_llvm h(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Julia's type inference is smart enough that it can figure out the return type even for recursive functions:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "fib (generic function with 1 method)" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fib(n::Integer) = n < 3 ? 1 : fib(n-1) + fib(n-2)" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "10-element Array{Int64,1}:\n", " 1\n", " 1\n", " 2\n", " 3\n", " 5\n", " 8\n", " 13\n", " 21\n", " 34\n", " 55" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[fib(n) for n = 1:10]" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "10-element Array{Int64,1}:\n", " 1\n", " 1\n", " 2\n", " 3\n", " 5\n", " 8\n", " 13\n", " 21\n", " 34\n", " 55" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fib.(1:10)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Body\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G│╻ <\u001b[1G\u001b[39m\u001b[90m1 \u001b[39m1 ─ %1 = (Base.slt_int)(n, 3)\u001b[36m::Bool\u001b[39m\n", "\u001b[90m\u001b[72G│ \u001b[1G\u001b[39m\u001b[90m \u001b[39m└── goto #3 if not %1\n", "\u001b[90m\u001b[72G│ \u001b[1G\u001b[39m\u001b[90m \u001b[39m2 ─ return 1\n", "\u001b[90m\u001b[72G│╻ -\u001b[1G\u001b[39m\u001b[90m \u001b[39m3 ─ %4 = (Base.sub_int)(n, 1)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││╻ <\u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %5 = (Base.slt_int)(%4, 3)\u001b[36m::Bool\u001b[39m\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m└── goto #5 if not %5\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m4 ─ goto #6\n", "\u001b[90m\u001b[72G││╻ -\u001b[1G\u001b[39m\u001b[90m \u001b[39m5 ─ %8 = (Base.sub_int)(%4, 1)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %9 = invoke Main.fib(%8::Int64)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││╻ -\u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %10 = (Base.sub_int)(%4, 2)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %11 = invoke Main.fib(%10::Int64)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││╻ +\u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %12 = (Base.add_int)(%9, %11)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m└── goto #6\n", "\u001b[90m\u001b[72G│ \u001b[1G\u001b[39m\u001b[90m \u001b[39m6 ┄ %14 = φ (#4 => 1, #5 => %12)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G│╻ -\u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %15 = (Base.sub_int)(n, 2)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││╻ <\u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %16 = (Base.slt_int)(%15, 3)\u001b[36m::Bool\u001b[39m\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m└── goto #8 if not %16\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m7 ─ goto #9\n", "\u001b[90m\u001b[72G││╻ -\u001b[1G\u001b[39m\u001b[90m \u001b[39m8 ─ %19 = (Base.sub_int)(%15, 1)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %20 = invoke Main.fib(%19::Int64)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││╻ -\u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %21 = (Base.sub_int)(%15, 2)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %22 = invoke Main.fib(%21::Int64)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││╻ +\u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %23 = (Base.add_int)(%20, %22)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G││ \u001b[1G\u001b[39m\u001b[90m \u001b[39m└── goto #9\n", "\u001b[90m\u001b[72G│ \u001b[1G\u001b[39m\u001b[90m \u001b[39m9 ┄ %25 = φ (#7 => 1, #8 => %23)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G│╻ +\u001b[1G\u001b[39m\u001b[90m \u001b[39m│ %26 = (Base.add_int)(%14, %25)\u001b[36m::Int64\u001b[39m\n", "\u001b[90m\u001b[72G│ \u001b[1G\u001b[39m\u001b[90m \u001b[39m└── return %26\n" ] } ], "source": [ "@code_warntype fib(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Dispatch on the argument type\n", "\n", "It is often useful to declare the argument types in Julia. For example, above, we define `fib(n::Integer)`. This says that the argument must be *some* type of integer. If we give it a different number type, it will now give an error:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "ename": "MethodError", "evalue": "MethodError: no method matching fib(::Float64)\nClosest candidates are:\n fib(!Matched::Integer) at In[20]:1", "output_type": "error", "traceback": [ "MethodError: no method matching fib(::Float64)\nClosest candidates are:\n fib(!Matched::Integer) at In[20]:1", "", "Stacktrace:", " [1] top-level scope at In[24]:1" ] } ], "source": [ "fib(3.7)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Integer` is an \"abstract\" type in Julia. There are many subtypes of `Integer` in Julia, including `Int64` (64-bit signed integers), `UInt8` (8-bit unsigned integers), and `BigInt` (arbitrary-precision integers: the number of digits grows as the numbers get larger and larger).\n", "\n", "Declaring the argument type has **no effect on performance** in Julia: the compiler automatically specializes the function when it is called, even if we declare no type at all. There are three main reasons to declare an argument type in Julia:\n", "\n", "* *Clarity*: Declaring the argument type can help readers understand the code. (However, *over-specifying* the type may make the function less general than it has to be!)\n", "\n", "* *Correctness*: Our `fib` function above would have given *some* answer if we allowed the user to pass `3.7`, but it probably wouldn't be the intended answer.\n", "\n", "* *Dispatch*: Julia allows you to define **different versions of a function** (different **methods**) for different argument types.\n", "\n", "For example, let's use this to define two versions of a [factorial](https://en.wikipedia.org/wiki/Factorial) function `myfact`, one for integers that recursively multiplies its arguments:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "myfact (generic function with 1 method)" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function myfact(n::Integer) \n", " n < 0 && throw(DomainError(\"n must be positive\"))\n", " return n < 2 ? one(n) : n * myfact(n-1)\n", "end" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3628800" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "myfact(10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You need `BigInt` for factorials of larger arguments, since factorials grow rapidly (faster than exponentially with `n`):" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "myfact(BigInt(100))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since we said `n::Integer`, this will give an error for a floating-point argument:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "ename": "MethodError", "evalue": "MethodError: no method matching myfact(::Float64)\nClosest candidates are:\n myfact(!Matched::Integer) at In[25]:2", "output_type": "error", "traceback": [ "MethodError: no method matching myfact(::Float64)\nClosest candidates are:\n myfact(!Matched::Integer) at In[25]:2", "", "Stacktrace:", " [1] top-level scope at In[28]:1" ] } ], "source": [ "myfact(3.7)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, it turns out that there is a very natural extension of the factorial to arbitrary real and complex numbers, via the [gamma function](https://en.wikipedia.org/wiki/Gamma_function): $n! = \\Gamma(n+1)$. Julia has a built-in `gamma(x)` function, so we can define `myfact` for other number types based on that:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "myfact (generic function with 2 methods)" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "using SpecialFunctions\n", "myfact(x::Number) = gamma(x+1)" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "120.0" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "myfact(5.0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The \"factorial\" of $-\\frac{1}{2}$ is then $\\sqrt{\\pi}$:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3.1415926535897936" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "myfact(-0.5)^2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now there are two different *methods* of `myfact`. You can get a list of the methods of any function by calling `methods`:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/html": [ "2 methods for generic function myfact: