Using Macro to avoid typing in Julia - julia
I have read that Julia has Macros but I am unsure if the Macros Julia provide are the ones I am thinking about.
I have the following expression:
Global.data[
Dates.value(Dates.Year(dtCursor)) - 2000,
Dates.value(Dates.Month(dtCursor)),
Dates.value(Dates.Day(dtCursor)),
Dates.value(Dates.Hour(dtCursor)) + 1,
Dates.value(Dates.Minute(dtCursor)) + 1,
1
]
And I repeat this a lot. I am wondering if I could have a macro that with dtCursor as parameter (it might be other variables in other cases) types all that for me. I am therefore looking for the Macro expansion functionality which was traditionally found in Macro assemblers.
I definitively do not want to include this as a function as this code is executed tens of thousands of times and therefore I do not want to add the overhead of a function call.
I have tried:
macro readData(_dtCursor, value)
return :(
Global.data[
Dates.value(Dates.Year(_dtCursor)) - 2000,
Dates.value(Dates.Month(_dtCursor)),
Dates.value(Dates.Day(_dtCursor)),
Dates.value(Dates.Hour(_dtCursor)) + 1,
Dates.value(Dates.Minute(_dtCursor)) + 1,
value
]
)
end
And later be invoked by:
println(#readData(dtCursor, 1))
Where dtCursor is a DateTime variable.
But I am getting:
ERROR: LoadError: UndefVarError: _dtCursor not defined
I have read https://docs.julialang.org/en/v1/manual/metaprogramming/index.html#man-macros-1 but a bit of help understanding what to do in this case is really welcomed.
Use a function
I definitively do not want to include this as a function as this code is executed tens of thousands of times and therefore I do not want to add the overhead of a function call.
You are definitively wrong.
You might be right in some languages, but not in JuliaLang.
(I do think this is a very useful questiom though because can highlight for others not to do this 😀)
That function-call in-lines away, and even if it didn't we have other tools (#inline) we would want to use before using a macro.
Macro's are for syntactic transformations.
If you are not doing a syntactic tranformation think again before using macros.
Here is a link to a good point made during by Steven G. Johnson at his keynote in juliacon:
"Functions are mostly good enough for Jeff Bezanson. Don't try to outsmart Jeff Bezason"
How to write it as a Macro and as a Function
The following answers your original question question
using Dates
using BenchmarkTools
macro readData(_dtCursor, value)
return :(
Global.data[
Dates.value(Dates.Year($(esc(_dtCursor)))) - 2000,
Dates.value(Dates.Month($(esc(_dtCursor)))),
Dates.value(Dates.Day($(esc(_dtCursor)))),
Dates.value(Dates.Hour($(esc(_dtCursor)))) + 1,
Dates.value(Dates.Minute($(esc(_dtCursor)))) + 1,
$value
]
)
end
function readData(_dtCursor, value)
Global.data[
Dates.value(Dates.Year(_dtCursor)) - 2000,
Dates.value(Dates.Month(_dtCursor)),
Dates.value(Dates.Day(_dtCursor)),
Dates.value(Dates.Hour(_dtCursor)) + 1,
Dates.value(Dates.Minute(_dtCursor)) + 1,
value
]
end
Benchmark it.
You say this is going to be run on 10,000s of times.
So I will benchmark on 100_000 uses, just to be safe.
const Global = (; data=[join((y, m, d, h, M, s)," ") for y in 2000:2010, m in 1:3, d in 1:20, h in 1:10, M in 1:30, s in 1:30]);
size(Global.data)
length(Global.data)
const sample_dts = map(1:100_000) do _
y, m, d, h, M, s = rand.(axes(Global.data))
dt = DateTime(y+2000, m, d, h-1, M-1)
end;
func_demo() = [readData(dt, 3) for dt in sample_dts];
macro_demo() = [#readData(dt, 3) for dt in sample_dts];
#btime func_demo()
#btime macro_demo()
They benchmark as identical
julia> #btime macro_demo();
5.409 ms (3 allocations: 781.34 KiB)
julia> #btime func_demo();
5.393 ms (3 allocations: 781.34 KiB)
Infact they specialize into (basically) the same code.
julia> #code_typed macro_demo()
CodeInfo(
1 ─ %1 = Main.sample_dts::Core.Compiler.Const(DateTime[2002-01-18T04:19:00, 2001-01-19T08:22:00, 2006-02-08T04:07:00, 2011-01-08T09:03:00, 2006-02-10T06:18:00, 2002-03-12T00:05:00, 2011-02-20T08:29:00, 2011-02-20T07:12:00, 2005-01-13T03:22:00, 2006-01-01T00:29:00 …
2005-03-10T04:29:00, 2002-03-12T09:11:00, 2002-03-11T00:28:00, 2007-02-12T02:26:00, 2003-02-15T07:29:00, 2009-01-01T02:02:00, 2009-
01-03T02:11:00, 2001-02-16T03:16:00, 2004-01-17T05:12:00, 2010-02-02T05:10:00], false)
│ %2 = %new(Base.Generator{Array{DateTime,1},getfield(Main, Symbol("##50#51"))}, getfield(Main, Symbol("##50#51"))(), %1)::Base.Gen
erator{Array{DateTime,1},getfield(Main, Symbol("##50#51"))}
│ %3 = invoke Base.collect(%2::Base.Generator{Array{DateTime,1},getfield(Main, Symbol("##50#51"))})::Array{String,1}
└── return %3
) => Array{String,1}
julia> #code_typed getfield(Main, Symbol("##50#51")).instance(1) # check the internals
│ %1 = %1 = Main.Global::Core.Compiler.Const((#==GIANT Inlined Const ==#)
│ %2 = Base.getfield(%1, :data)::Array{String,6}
│ %3 = Base.sub_int(dt, 2000)::Int64
│ %4 = Base.add_int(dt, 1)::Int64
│ %5 = Base.add_int(dt, 1)::Int64
│ %6 = Base.arrayref(true, %2, %3, dt, dt, %4, %5, 3)::String
└── return %6
) => String
julia> #code_typed func_demo()
CodeInfo(
1 ─ %1 = Main.sample_dts::Core.Compiler.Const(DateTime[2002-01-18T04:19:00, 2001-01-19T08:22:00, 2006-02-08T04:07:00, 2011-01-08T09:03:00, 2006-02-10T06:18:00, 2002-03-12T00:05:00, 2011-02-20T08:29:00, 2011-02-20T07:12:00, 2005-01-13T03:22:00, 2006-01-01T00:29:00 … 2005-03-10T04:29:00, 2002-03-12T09:11:00, 2002-03-11T00:28:00, 2007-02-12T02:26:00, 2003-02-15T07:29:00, 2009-01-01T02:02:00, 2009-
01-03T02:11:00, 2001-02-16T03:16:00, 2004-01-17T05:12:00, 2010-02-02T05:10:00], false)
│ %2 = %new(Base.Generator{Array{DateTime,1},getfield(Main, Symbol("##43#44"))}, getfield(Main, Symbol("##43#44"))(), %1)::Base.Gen
erator{Array{DateTime,1},getfield(Main, Symbol("##43#44"))}
│ %3 = invoke Base.collect(%2::Base.Generator{Array{DateTime,1},getfield(Main, Symbol("##43#44"))})::Array{String,1}
└── return %3
) => Array{String,1}
julia> #code_typed getfield(Main, Symbol("##43#44")).instance(1)
CodeInfo(
1 ─ %1 = Main.Global::NamedTuple{(:data,),Tuple{Array{String,6}}}
│ %2 = Base.getfield(%1, :data)::Array{String,6}
│ %3 = Base.sub_int(dt, 2000)::Int64
│ %4 = Base.add_int(dt, 1)::Int64
│ %5 = Base.add_int(dt, 1)::Int64
│ %6 = Base.arrayref(true, %2, %3, dt, dt, %4, %5, 3)::String
└── return %6
) => String
There is a very minor difference in the generors function between the two.
Wheree the value became a Compliler.Const or a NamedTuple when inlining,
but after that goes the LLVM that difference goes way too I think
(Check #code_llvm if your really interested. But we are already super deap into the weeds.)
This is probably the wrong code to be optimizing in the first place.
A long with the guidance to benchmark any optimization you do.
One should also profile the code to decide what is worth optimizing.
A function that is only called 10,000s of times and not allocating giant arrays etc, probably not worth worrying too much about.
Especially if you are just worrying about function call overhead,
which is only a handful of CPU cycles.
You have to splice in the variables you pass as macro arguments:
julia> macro readData(dtCursor, value)
return :(
Global.data[
Dates.value(Dates.Year($dtCursor)) - 2000,
Dates.value(Dates.Month($dtCursor)),
Dates.value(Dates.Day($dtCursor)),
Dates.value(Dates.Hour($dtCursor)) + 1,
Dates.value(Dates.Minute($dtCursor)) + 1,
$value
]
)
end
#readData (macro with 1 method)
julia> #macroexpand #readData(dtCursor, 1)
:((Main.Global).data[(Main.Dates).value((Main.Dates).Year(Main.dtCursor)) - 2000, (Main.Dates).value((Main.Dates).Month(Main.dtCursor)), (Main.Dates).value((Main.Dates).Day(Main.dtCursor)), (Main.Dates).value((Main.Dates).Hour(Main.dtCursor)) + 1, (Main.Dates).value((Main.Dates).Minute(Main.dtCursor)) + 1, 1])
Furthermore, Julia macros are hygenic; that means that there will be no confusion about the name _dtCursor in the macro definition, and the name dtCursor at call site. One thing you might need to do is to escape the inputs, though.
Also, this might be an overkill. You should benchmark the macro version against the function version; maybe, there's enough inlining happening that the macro doesn't actually matter.
Related
Is the following function considered type-stable in Julia?
I want to have a curried version of a function. So, I write the code as follows: f(x::Int64, y::Int64) = x + y f(x::Int64) = (y::Int64) -> f(x, y) But I am not sure if Julia considers this an example of a type-unstable definition. On the face of it, one of the methods returns an anonymous function, while another returns an Int64. Yet, when the curried version is applied, the final result is also an Int64. So, my questions are: Is this code type-stable? If not, is there a way to have a curried version of a function without writing type-unstable code? Thanks in advance.
Yes, it is. According to the official doc, you can investigate it by using the #code_warntype macro: julia> #code_warntype f(1, 5) MethodInstance for f(::Int64, ::Int64) from f(x::Int64, y::Int64) in Main at REPL[2]:1 Arguments #self#::Core.Const(f) x::Int64 y::Int64 Body::Int64 1 ─ %1 = (x + y)::Int64 └── return %1 The arguments of this function have the exact type Int64, and as we can see in the Body::Int64, the inferred return type function is Int64. Furthermore, we have f(x) which is based on the type-stable function f(x, y): julia> #code_warntype f(1) MethodInstance for f(::Int64) from f(x::Int64) in Main at REPL[15]:1 Arguments #self#::Core.Const(f) x::Int64 Locals #3::var"#3#4"{Int64} Body::var"#3#4"{Int64} 1 ─ %1 = Main.:(var"#3#4")::Core.Const(var"#3#4") │ %2 = Core.typeof(x)::Core.Const(Int64) │ %3 = Core.apply_type(%1, %2)::Core.Const(var"#3#4"{Int64}) │ (#3 = %new(%3, x)) └── return #3 Here as well, there's not any unstable defined parameter type. Look at the following as an example of an unstable-typed function: julia> unstF(X) = x*5 unstF (generic function with 1 method) julia> #code_warntype unstF(1) MethodInstance for unstF(::Int64) from unstF(X) in Main at REPL[17]:1 Arguments #self#::Core.Const(unstF) X::Int64 Body::Any 1 ─ %1 = (Main.x * 5)::Any └── return %1 If you try this in the REPL, you'll see the Any appears with a red color. Since we have the Body::Any (Any with the red color), we can conclude that the returned object by this function is a non-concrete type object. Because the compiler doesn't know what is the x (Note that the input is X). So the result can be Anything! So this function is type-unstable for (here, for integer inputs. Note that you should investigate the type-stability of your function by your desired input(s). E.g., #code_warntype f(5) and #code_warntype f(5.) should be observed if I can pass it Float64 or Int64 either).
Julia short-circuiting
Consider function foo(x) x isa Bar || throw(ArgumentError("x is not a Int64...")) dosomething(x) end as opposed to a traditional function foo(x) if !(x isa Bar) throw(ArgumentError("x is not a Bar...")) end dosomething(x) end (functionally equivalent is !(x isa Int64) && ...) Poking around a few packages, it seems this sort of conditional evaluation is not popular -- at face value, it seems convenient, and I prefer it a bit stylistically. Is there a consensus on this stylistically? I understand that using the Unicode \in is discouraged over the word "in" by at least the Blue style guide for the sake of readability/ compatibility -- is this something similar? Is this less performant? At face value, it seems like it would take about the same number of operations. I came across this post as well, but the answers aren't very satisfying, even ignoring that it's now likely outdated.
When asking yourself performance question like this it is usually good idea to peek into compiled code via #code_lowered, #code_typed, #code_llvm, #code_native. Each of those macros explains one step further in the compilation process. Consider function x5a(x) x < 5 && (x=5) x end function x5b(x) if x < 5 x=5 end x end Let's try #code_lowered julia> #code_lowered x5a(3) CodeInfo( 1 ─ x#_3 = x#_2 │ %2 = x#_3 < 5 └── goto #3 if not %2 2 ─ x#_3 = 5 └── goto #3 3 ┄ return x#_3 ) julia> #code_lowered x5b(3) CodeInfo( 1 ─ x#_3 = x#_2 │ %2 = x#_3 < 5 └── goto #3 if not %2 2 ─ x#_3 = 5 3 ┄ return x#_3 ) Almost identical - will it get simplified further in the compilation process? Let's see! julia> #code_typed x5a(3) CodeInfo( 1 ─ %1 = Base.slt_int(x#_2, 5)::Bool └── goto #3 if not %1 2 ─ nothing::Nothing 3 ┄ %4 = φ (#2 => 5, #1 => x#_2)::Int64 └── return %4 ) => Int64 julia> #code_typed x5b(3) CodeInfo( 1 ─ %1 = Base.slt_int(x#_2, 5)::Bool └── goto #3 if not %1 2 ─ nothing::Nothing 3 ┄ %4 = φ (#2 => 5, #1 => x#_2)::Int64 └── return %4 ) => Int64 Both functions have identical lowered codes which means that they will result in an identical assembly code and hence execute identical sets of CPU instructions. Regarding the style - it is not documented in official style guidelines so code readability is the criterion and like Bogumil said this style is quite popular.
From my experience: There should be no performance difference between if and short-circuting evaluation. The choice should be purely stylistic. the style cond || ... and cond && ... is quite popular. Just if the expression following the || or && is long so that it would not fit a single line then if is used.
Julia: read CSV into Vector{T} efficiently / type stability
I have a number of very large CSV files which I would like to parse into custom data structures for subsequent processing. My current approach involves CSV.File and then converting each CSV.Row into the custom data structure. It works well for small test cases but gets really inefficient for the large files (GC very high). The problem is in the second step and I suspect is due to type instability. I'm providing a mock example below. (I'm new to Julia, so apologies if I misunderstood something) Define data structure and conversion logic: using CSV struct Foo a::Int32 b::Float32 end Foo(csv_row::CSV.Row) = Foo(csv_row.a, csv_row.b) Using the default constructor causes 0 allocations: julia> #allocated foo1 = Foo(1, 2.5) 0 However, when creating the object from CSV.Row all of a sudden 80 bytes are allocated: julia> data = CSV.File(Vector{UInt8}("a,b\n1,2.5"); threaded = false) 1-element CSV.File{false}: CSV.Row: (a = 1, b = 2.5f0) julia> #allocated foo2 = Foo(data[1]) 80 In the first case all types are stable: julia> #code_warntype Foo(1, 2) Variables #self#::Core.Compiler.Const(Foo, false) a::Int64 b::Int64 Body::Foo 1 ─ %1 = Main.Foo::Core.Compiler.Const(Foo, false) │ %2 = Core.fieldtype(%1, 1)::Core.Compiler.Const(Int32, false) │ %3 = Base.convert(%2, a)::Int32 │ %4 = Core.fieldtype(%1, 2)::Core.Compiler.Const(Float32, false) │ %5 = Base.convert(%4, b)::Float32 │ %6 = %new(%1, %3, %5)::Foo └── return %6 Whereas in the second case they are not: julia> #code_warntype Foo(data[1]) Variables #self#::Core.Compiler.Const(Foo, false) csv_row::CSV.Row Body::Foo 1 ─ %1 = Base.getproperty(csv_row, :a)::Any │ %2 = Base.getproperty(csv_row, :b)::Any │ %3 = Main.Foo(%1, %2)::Foo └── return %3 So I guess my question is: How can I make the second case type-stable and avoid the allocations? Providing the types explicitly in CSV.File does not make a difference by the way.
While this does not focus on type stability, I would expect the highest performance combined with flexibility from the following code: d = DataFrame!(CSV.File(Vector{UInt8}("a,b\n1,2.5\n3,4.0"); threaded = false)) The above efficiently transforms a CSV.File into a type stable structure, additionally avoiding data copying in this process. This should work for your case of huge CSV files. And now: julia> Foo.(d.a, d.b) 2-element Array{Foo,1}: Foo(1, 2.5f0) Foo(3, 4.0f0)
How to see Zygote differentiated function implementation?
I have written a simple function in a .jl file that I can successfully differentiate using forward. However I am new to Julia and I do not understand how to see the generated source code for the differentiated function. I've tried all sorts of things like #code_lowered Zygote.forward(maxPool, [1.0, 2.0]) and #code_lowered Zygote.forward(maxPool) but they just show me the call to forward itself. How can I see the code that Zygote generates for the forward and reverse passes? using Pkg using Zygote, ForwardDiff function size1d(v) return size(v)[1] end function max(a, b) if a > b a else b end end function maxPool(v) return [max(v[2 * i - 1], v[2 * i]) for i in 1:div(size1d(v), 2)] end v = [1.0, 2.0, 3.0, 4.0] df = [20.0, 30.0] println("maxPool(v):") println(maxPool(v)) println() println("maxAdjoint:") maxAdjoint = Zygote.forward(max, 3.0, 4.0)[2] println(maxAdjoint(1.0)) println() println("maxPoolAdjoint:") maxPoolAdjoint = Zygote.forward(maxPool, v)[2] println(maxPoolAdjoint(df))
Zygote has its own macro Zygote.#code_adjoint for showing the lowered adjoint code, i.e. the code that generates the gradient of a function in reverse mode. I'm not sure about forward mode though. Here's a simple example in reverse mode: julia> using Zygote julia> f(x) = 2x + 1 f (generic function with 1 method) julia> #code_lowered f(1) CodeInfo( 1 ─ %1 = 2 * x │ %2 = %1 + 1 └── return %2 ) julia> Zygote.#code_adjoint f(1) Zygote.Adjoint(1: (%3, %4 :: Zygote.Context, %1, %2) %5 = Zygote._forward(%4, Main.:*, 2, %2) %6 = Base.getindex(%5, 1) %7 = Base.getindex(%5, 2) %8 = Zygote._forward(%4, Main.:+, %6, 1) %9 = Base.getindex(%8, 1) %10 = Base.getindex(%8, 2) return %9 , 1: (%1) %2 = (#10)(%1) %3 = Zygote.gradindex(%2, 2) %4 = (#7)(%3) %5 = Zygote.gradindex(%4, 3) %6 = Zygote.tuple(nothing, %5) return %6 ) We might worry from the length and apparent complexity of this lowered adjoint code that the gradient is slow, but we can check the LLVM code to make sure everything ends up being elided away: julia> #code_llvm f'(1) ; # /Users/mason/.julia/packages/Zygote/SAZMM/src/compiler/interface.jl:50 within `#34' define i64 #"julia_#34_18250"(i64) { top: ret i64 2 }
Julia: how to see devectorized code?
I would like to see devectorized code of some expression say here obj.mask = exp.(1.0im*lambda*obj.dt/2.); How one could print a generic expression in devectorized form in Julia?
I don't think that what you ask for exists (please proof me wrong if I'm mistaken!). The best you can do is use #code_lowered, #code_typed, #code_llvm, #code_native macros (in particular #code_lowered) to see what happens to your Julia code snippet. However, as Julia isn't translating all dots to explicit for loops internally, non of these snippets will show you a for-loop version of your code. Example: julia> a,b = rand(3), rand(3); julia> f(a,b) = a.*b f (generic function with 1 method) julia> #code_lowered f(a,b) CodeInfo( 1 1 ─ %1 = Base.Broadcast.materialize │ │ %2 = Base.Broadcast.broadcasted │ │ %3 = (%2)(Main.:*, a, b) │ │ %4 = (%1)(%3) │ └── return %4 │ ) So, Julia translates the .* into a Base.Broadcast.broadcasted call. Of course, we can go further and do julia> #which Base.Broadcast.broadcasted(Main.:*, a, b) broadcasted(f, arg1, arg2, args...) in Base.Broadcast at broadcast.jl:1139 and check broadcast.jl in line 1139 and so on to trace the actual broadcasted method that will be called (maybe Tim Holy's Rebugger is useful here :D). But as I said before, there won't be a for-loop in it. Instead you will find something like this: broadcasted(::DefaultArrayStyle{1}, ::typeof(*), r::AbstractRange, x::Number) = range(first(r)*x, step=step(r)*x, length=length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(*), r::StepRangeLen{T}, x::Number) where {T} = StepRangeLen{typeof(T(r.ref)*x)}(r.ref*x, r.step*x, length(r), r.offset) broadcasted(::DefaultArrayStyle{1}, ::typeof(*), r::LinRange, x::Number) = LinRange(r.start * x, r.stop * x, r.len) Update Ok, eventually, I found for-loops in copyto! in broadcast.jl. But this is probably to deep into the rabbit hole.