Skip to content

Remove recursion through @generated #9

@tveldhui

Description

@tveldhui

I tracked down a per-row allocation in the FSM join TPC-H Q1 to a call to unsafe_load!(::Date) that was not type stable. The issue was that unsafe_load! is a @generated function that calls itself recursively, and Julia does not appear to make any guarantees about type inference in this scenario.

Here is an example that demonstrates the issue:

@generated function foo(::Val{N}) where N                                                   
   if N < 1                                                                                 
      :(1)                                                                              
   else                                                                                     
      # Return 1+foo(Val(N-1)), but using Int(N) for even
      # and Float64(N) for odd            
      if N % 2 == 0                                                                         
         :(1+foo(Val(Int(N)-1)))                                                                                                                                       
      else                                                                                  
         :(1+foo(Val(Float64(N)-1)))                                                                                                                                    
      end                                                                                   
   end                                                                                      
end

If you start a new julia 1.0.1 session, paste in the above definition, and then try these calls, you’ll get type stable code:

julia> @code_typed foo(Val(2))
CodeInfo(
2 1 ─     (Base.sub_float)(1.0, 1.0)::Float64                                                     
  └──     return 3                                                                            
) => Int64

julia> @code_typed foo(Val(3))
CodeInfo(
2 1 ─     (Base.sub_float)(3.0, 1.0)::Float64                                                     
  │       (Base.sub_float)(1.0, 1.0)::Float64                                                     
  └──     return 4                                                                            
) => Int64

julia> @code_typed foo(Val(4))
CodeInfo(
2 1 ─     (Base.sub_float)(3.0, 1.0)::Float64                                                     
  │       (Base.sub_float)(1.0, 1.0)::Float64                                                     
  └──     return 5                                                                            
) => Int64

However, if you restart julia, paste in the definition of foo(), and immediately try foo(Val(4)), it will produce unstable code:

julia> @code_typed foo(Val(4))
CodeInfo(
2 1 ─ %1 = invoke Main.foo($(QuoteNode(Val{3}()))::Val{3})::Any                                   
  │   %2 = (1 + %1)::Any                                                                          
  └──      return %2                                                                          
) => Any

It’s the same call as before, but without the previous calls to foo(Val(3)) and foo(Val(2)) in the cache, the result of type inference is different.

In the case of unsafe_load!(::Blob{Date}), there was a recursion from unsafe_load!(::Blob{Date}) to unsafe_load!(::Blob{UTInstant{Day}}) to unsafe_load!(::Blob{Day}) to unsafe_load!(::Int64), and type inference was failing, resulting in an ::Any type:

screen shot 2019-01-24 at 1 15 30 pm

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions