Here is a simple implementation of a two-pointer technique for sorted arrays:
function f(xs, ys)
i_begin, i_end = firstindex(xs), firstindex(xs)
map(ys) do y
i_begin = something( findnext(x -> x >= y, xs, i_begin), lastindex(xs) + 1 )
i_end = something( findnext(x -> x > y, xs, max(i_begin, i_end)), lastindex(xs) + 1 ) - 1
length(@view(xs[i_begin:i_end])) # in real code - arbitrary function instead of length
end
end
While it seems that the function should be type-stable, this is not the case. As far as I can tell, instability is related to nothing
or something
:
julia> @code_warntype f([1, 2, 3], [1, 2, 3])
Variables
#self#::Core.Compiler.Const(f, false)
xs::Array{Int64,1}
ys::Array{Int64,1}
#46::var"#46#49"{Array{Int64,1}}
i_begin::Core.Box
i_end::Core.Box
ys_new::Array{_A,1} where _A
Body::Array{_A,1} where _A
1 ─ (i_begin = Core.Box())
│ (i_end = Core.Box())
│ %3 = Main.firstindex(xs)::Core.Compiler.Const(1, false)
│ %4 = Main.firstindex(xs)::Core.Compiler.Const(1, false)
│ Core.setfield!(i_begin, :contents, %3)
│ Core.setfield!(i_end, :contents, %4)
│ %7 = Main.:(var"#46#49")::Core.Compiler.Const(var"#46#49", false)
│ %8 = Core.typeof(xs)::Core.Compiler.Const(Array{Int64,1}, false)
│ %9 = Core.apply_type(%7, %8)::Core.Compiler.Const(var"#46#49"{Array{Int64,1}}, false)
│ %10 = i_begin::Core.Box
│ (#46 = %new(%9, xs, %10, i_end))
│ %12 = #46::var"#46#49"{Array{Int64,1}}
│ %13 = Main.map(%12, ys)::Array{_A,1} where _A
│ (ys_new = %13)
└── return %13
This instability actually affects performance a lot:
julia> @btime f($(rand(1000)), $(rand(1000)))
139.358 μs (6603 allocations: 158.03 KiB)
After adding types explicitly by replacing the 2nd line with
i_begin::Int, i_end::Int = firstindex(xs), firstindex(xs)
:
julia> @btime f($(rand(1000)), $(rand(1000)));
17.733 μs (1085 allocations: 24.88 KiB)
Still, instability doesn’t go away completely: @code_warntype
still has Core.Box
es in it.
Any suggestions on how to optimize such code? The examples above were ran in Julia 1.5.