Holy smoky, this loop flies now! Thanks so much to everyone who contributed to help, awesome feedback.
Following are a few comments from my further tests with enclosed my current
fastest version:
- In my script on the global scope, I can define a type, but I cannot
defined it const. So:
- GlobalIdType = Int works but
- const GlobalIdType = Int errors out.
- I can define a type const within a function definition, but it does
not seem to have any performance benefit.
- As Stefan explained, the performance hit with my initial integer
increment i += 1 was not because it was not typed but because it was not
considered a constant type by the compiler. I decoupled my array index
types with their column id types, declaring that array index type inside my
function definition, and those increments pretty much disappeared from the
profile report. I did not have to declare explicitly those index types, the
default Int would have worked just fine, I was just experimenting to make
sure I did not misunderstand anything.
- Then, again using Stefan's hint, the next bottleneck was the array
accesses. I added the types of interest as parameters of my function
(i.e., template function in the C++ world), annotated the array types
accordingly, and here we go, another order of magnitude of performance
gain! Using @code_typed shows all variables are typed, no Any shows up.
- Now, the slowest part is no longer the loop being iterated over about
100,000 times in my test, but the initial array allocation of the results
being returned by the function -- quite amazing the efficiency of the core
iteration!
This was a very educational exercise for me, this will help me write code
appropriately typed and fast from the beginning.
I have a *remaining question* on how to best pass those types in a function
signature where my main data container is a DataFrame.
Essentially, I functionally have:
function foo(dt::DataFrame)
> a = dt[:col].data # An array as column of my DataFrame, data with no NA.
> const MyDataType = eltype(a)
> ...
> end
As specified above, access is slow because the array "a" if of type
Array{Any} at compile time, so not optimal by an order of magnitude.
What I essentially did to type and hence speed up the array access was
passing the array data type parameter as follows:
>
> function foo(dt::DataFrame, dummyParameter::MyDataType)
a = dt[:col].data::Array{MyDataType} # An array as column of my
> DataFrame, data with no NA.
> ...
> end
But it is not clean, the data type "MyDataType" is redundant as embedded
in the data frame parameter "dt".
Is there a way to annotate directly the data frame parameter "dt" with its
column types so I don't have to create a dummy function parameter for this
purpose?
If so, how?
If my data frame has N columns with my data type of interest in its first
column, I am looking for something conceptually like below (syntax below
does not work):
function foo(dt::*DataFrame{**MyDataType, ...}*)
a = dt[:col].data::Array{MyDataType} # An array as column of my
> DataFrame, data with no NA.
> ...
> end
Note that the following pattern does not solve the array type inference
problem (i.e., it is slow), see Generic performance problems, #523 by John
Myles White <https://github.com/JuliaStats/DataFrames.jl/issues/523> (where
I initially learned to go access the data array directly for optimal
performance):
>
> function foo(dt::DataFrame)
> da = dt[:col]
> const MyDataType = eltype(a)
> a = dt[:col].data::Array{MyDataType} # An array as column of my
> DataFrame, data with no NA.
...
> end
(Perhaps I should ask this question in a different post as it is beyond the
scope of my initial increment performance question)
Thanks much.
On Thursday, September 18, 2014 4:25:25 PM UTC-4, Stefan Karpinski wrote:
>
> On Thu, Sep 18, 2014 at 2:33 PM, Ivar Nesje <[email protected]
> <javascript:>> wrote:
>
>> Seems like Julia is not smart enough to guess that i::IdType will always
>> ensure that i is a Int64 when IdType might change.
>
>
> Since IdType is not constant, it can change at any time – generating code
> on the premise that it cannot change would be incorrect. Instead, the code
> generated for this function needs to handle the possibility that IdType can
> change at any point, which basically makes all optimizations impossible.
> It's a bit low level (we should really expose this better), but you can use
> @code_typed to see what the inferred types of all the local variables are:
>
> julia> (@code_typed manual_iter1(1,2))[1].args[2][2]
> 38-element Array{Any,1}:
> {:dt1,Int64,0}
> {:dt2,Int64,0}
> {:zeroIdType,Any,18}
> {:oneIdType,Any,18}
> {:zeroDType,Any,18}
> {symbol("#s5149"),Any,18}
> {symbol("#s5150"),Any,18}
> {:dt1id1,Any,18}
> {:dt1D,Any,18}
> {symbol("#s5151"),Any,18}
> {symbol("#s5152"),Any,18}
> {symbol("#s5153"),Any,18}
> {:dt2id2,Any,18}
> {:dt2D1,Any,18}
> {:dt2D2,Any,18}
> {:MAX_INT,Any,18}
> {:MAX_D,Any,18}
> {:nrow1,Any,18}
> {:nrow2,Any,18}
> {:i1,Any,2}
> {:i1id1,Any,2}
> {:i1D,Any,2}
> {:i2Lower,Any,2}
> {:i2LowerD2,Any,2}
> {:i2Upper,Any,2}
> {:i2UpperD1,Any,2}
> {:i2UpperD2,Any,2}
> {:i2Match,Any,2}
> {:i,Any,2}
> {:nrowMax,Any,18}
> {:resid1,Any,18}
> {:resid2,Any,18}
> {:resD1,Any,18}
> {:resD,Any,18}
> {:resD2,Any,18}
> {:i2MatchD2,Any,18}
> {:i2MatchD1,Any,18}
> {symbol("#s5154"),Any,18}
>
>
> As you can see, it's not pretty: given Int arguments, literally only the
> types of the arguments themselves are more specific than Any. If you
> declare IdType and DType to be const, it gets better, but still not ideal:
>
> julia> (@code_typed manual_iter1(1,2))[1].args[2][2]
> 38-element Array{Any,1}:
> {:dt1,Int64,0}
> {:dt2,Int64,0}
> {:zeroIdType,Int64,18}
> {:oneIdType,Int64,18}
> {:zeroDType,Int64,18}
> {symbol("#s122"),Any,18}
> {symbol("#s121"),Any,18}
> {:dt1id1,Any,18}
> {:dt1D,Any,18}
> {symbol("#s120"),Any,18}
> {symbol("#s119"),Any,18}
> {symbol("#s118"),Any,18}
> {:dt2id2,Any,18}
> {:dt2D1,Any,18}
> {:dt2D2,Any,18}
> {:MAX_INT,Int64,18}
> {:MAX_D,Int64,18}
> {:nrow1,Int64,18}
> {:nrow2,Int64,18}
> {:i1,Int64,2}
> {:i1id1,Int64,2}
> {:i1D,Int64,2}
> {:i2Lower,Int64,2}
> {:i2LowerD2,Int64,2}
> {:i2Upper,Int64,2}
> {:i2UpperD1,Int64,2}
> {:i2UpperD2,Int64,2}
> {:i2Match,Int64,2}
> {:i,Int64,2}
> {:nrowMax,Int64,18}
> {:resid1,Any,18}
> {:resid2,Any,18}
> {:resD1,Any,18}
> {:resD,Any,18}
> {:resD2,Any,18}
> {:i2MatchD2,Any,18}
> {:i2MatchD1,Any,18}
> {symbol("#s117"),Any,18}
>
>
> When you pull something out of an untyped structure like dt1id1, dt1D,
> etc. it is a good idea to provide some type annotations if you can. Most of
> the other type annotations inside the function body are unnecessary,
> however.
>
>
crossJoinFilter.jl
Description: Binary data
