That I know, and it shows in the typed code in 2D. But the conversion to complex would allocate another array that is not used. Doing what FFT should be doing by hand, infers:
fff2()=(u=rand(3,4,5);fu=complex(u);fft!(fu))
@code_warntype optimize=:true fff2()
Variables
#self#::Core.Compiler.Const(fff2, false)
u::Array{Float64,3}
fu::Array{Complex{Float64},3}
Body::Array{Complex{Float64},3}
1 ─ %1 = invoke Random.rand(Random.Float64::Type{Float64}, (3, 4, 5)::Tuple{Int64,Int64,Int64})::Array{Float64,3}
│ %2 = Base.arraysize(%1, 1)::Int64
│ %3 = Base.arraysize(%1, 2)::Int64
│ %4 = Base.arraysize(%1, 3)::Int64
│ %5 = Base.slt_int(%2, 0)::Bool
│ %6 = Base.ifelse(%5, 0, %2)::Int64
│ %7 = Base.slt_int(%3, 0)::Bool
│ %8 = Base.ifelse(%7, 0, %3)::Int64
│ %9 = Base.slt_int(%4, 0)::Bool
│ %10 = Base.ifelse(%9, 0, %4)::Int64
│ %11 = $(Expr(:foreigncall, :(:jl_alloc_array_3d), Array{Complex{Float64},3}, svec(Any, Int64, Int64, Int64), 0, :(:ccall), Array{Complex{Float64},3}, :(%6), :(%8), :(%10), :(%10), :(%8), :(%6)))::Array{Complex{Float64},3}
│ %12 = invoke Base.copyto!($(QuoteNode(IndexLinear()))::IndexLinear, %11::Array{Complex{Float64},3}, $(QuoteNode(IndexLinear()))::IndexLinear, %1::Array{Float64,3})::Array{Complex{Float64},3}
│ Base.arraysize(%12, 1)
│ Base.arraysize(%12, 2)
│ Base.arraysize(%12, 3)
│ %16 = FFTW.ESTIMATE::UInt32
│ %17 = FFTW.NO_TIMELIMIT::Float64
│ %18 = invoke FFTW.cFFTWPlan{Complex{Float64},-1,true,3}(%12::Array{Complex{Float64},3}, %12::Array{Complex{Float64},3}, $(QuoteNode(1:3))::UnitRange{Int64}, %16::UInt32, %17::Float64)::FFTW.cFFTWPlan{Complex{Float64},-1,true,3}
│ %19 = invoke AbstractFFTs.:*(%18::FFTW.cFFTWPlan{Complex{Float64},-1,true,3}, %12::Array{Complex{Float64},3})::Array{Complex{Float64},3}
└── return %19