Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use Ref, not preallocated arrays, for passing parameters by reference #44

Merged
merged 8 commits into from
Aug 17, 2017

Conversation

musm
Copy link
Contributor

@musm musm commented Aug 15, 2017

No description provided.

@stevengj
Copy link
Member

stevengj commented Aug 15, 2017

See also the cy, ae, and wrk arrays later in the file.

As long as you're updating this, could you fix these to be thread-safe? Should be as simple as:

const ai = Vector{Float64}(2 * Threads.nthreads())

and then replace pointer(ai,1) and pointer(ai,2) with pointer(ai, threadoffset-1) and pointer(ai, threadoffset), respectively, where threadoffset = Threads.threadid()*2.

@musm
Copy link
Contributor Author

musm commented Aug 15, 2017

Thanks @stevengj for the suggestion, will do.

@stevengj
Copy link
Member

One thing that worries me is how my suggestion will interact with precompiling, however: we don't want it to use Threads.nthreads() from precompile time.

We may have to do const ai = Vector{Float64}(0) and then do resize!(ai, Threads.nthreads()*2) in the module __init__ function.

@musm
Copy link
Contributor Author

musm commented Aug 16, 2017

@stevengj The arrays are constant, not sure why those need to be set based on the thread size? I updated the code to get rid of the & and have more appropriately used Ref (JuliaLang/julia#22734)

@stevengj
Copy link
Member

stevengj commented Aug 16, 2017

The current code is not thread-safe. Imagine that two threads call _bessely in parallel. Because they are using the same arrays (wrk etc.) to pass arguments by reference, you will get a race condition where one thread could be stomping on the data of the other thread.

One solution is to make sure that each thread uses a separate memory location to pass variables by reference. To do this with a pre-allocated array, you have to allocate nthreads() copies of the array and use threadid() to determine which copy to use.

Another solution would be to not pre-allocate arrays, but rather to allocate new Ref variables for each call. (You already changed airy to do this.) I'm not sure what performance penalty this incurs.

(An even cleaner solution would be possible if Julia had language-level support for thread-local storage, or if becomes cheaper to allocate Ref variables as needed: JuliaLang/julia#8134.)

src/bessel.jl Outdated
Ptr{Float64}, Ptr{Float64}, Ptr{Int32}, Ptr{Int32}),
rz1, rz2, rid, rkode,
ai1, ai2, ae1, ae2)
if ae2[] == 0 || ae2[] == 3 # ignore underflow and less than half machine accuracy loss
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully, the performance penalty for allocating new Ref variables for each call, rather than using pre-allocated arrays, is negligible. Have you benchmarked it?

Copy link
Contributor Author

@musm musm Aug 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes on master they are equivalent (thanks to yuyichao's work) and this avoids the thread issues you have mentioned.

julia> @btime __airy(z,id,kode)    # version in this PR
  680.412 ns (1 allocation: 32 bytes)
-2.197881420594205e-53 - 1.6155839776764676e-53im

julia> @btime _airy(z,id,kode)
  683.544 ns (1 allocation: 32 bytes)
-2.197881420594205e-53 - 1.6155839776764676e-53im

# Commit 952dc93489* (2017-08-02 23:54 UTC) (right before BenchmarkTools broke on master)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will update the rest of the ccall's soon.

src/bessel.jl Outdated

ccall((:zbiry_,openspecfun), Void,
(Ptr{Float64}, Ptr{Float64}, Ptr{Int32}, Ptr{Int32},
Ptr{Float64}, Ptr{Float64}, Ptr{Int32}),
Copy link
Member

@stevengj stevengj Aug 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be Ref, not Ptr. (Using Ptr here is probably a relic of the days before Ref.)

Once you change it to Ref, then you will no longer need to explicitly create Ref objects for parameters like kode that are purely inputs. You can just pass kode directly, and ccall will create the Ref for you. You only need to explicitly create Ref objects for output parameters like ai1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I have fixed them all up.

src/bessel.jl Outdated
return z
rx = Ref(x)
rz = Ref(BigFloat())
ccall((:mpfr_ai, :libmpfr), Int32, (Ptr{BigFloat}, Ptr{BigFloat}, Int32), rz, rx, ROUNDING_MODE[])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, use Ref here and pass x, not rx.

@stevengj stevengj changed the title Use Vector for constant refs for airy and biry use Ref, not preallocated arrays, for passing parameters by reference Aug 16, 2017
src/bessel.jl Outdated

ccall((:zairy_,openspecfun), Void,
(Ref{Float64}, Ref{Float64}, Ref{Int32}, Ref{Int32},
Ptr{Float64}, Ptr{Float64}, Ptr{Int32}, Ptr{Int32}),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last four arguments should be Ref too.

Copy link
Contributor Author

@musm musm Aug 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be any difference in doing so, but will change since it seems more consistent

julia> f(x::Float64) = Base.unsafe_convert(Ref{Float64}, Base.cconvert(Ref{Float64},Ref{Float64}(x)))  

julia> g(x::Float64) = Base.unsafe_convert(Ptr{Float64}, Base.cconvert(Ptr{Float64},Ref{Float64}(x)))  

julia> @code_llvm f(2.)

; Function f
; Location: REPL[18]
; Function Attrs: uwtable
define double* @julia_f_63230(double) #0 {
top:
  %1 = alloca i64, align 8
  %2 = bitcast i64* %1 to i8*
  %3 = bitcast i64* %1 to %jl_value_t*
  call void @llvm.lifetime.start(i64 8, i8* %2)
; Location: REPL[18]:1
  %4 = bitcast %jl_value_t* %3 to double*
  ret double* %4
}

julia> @code_llvm g(2.)

; Function g
; Location: REPL[19]
; Function Attrs: uwtable
define double* @julia_g_63231(double) #0 {
top:
  %1 = alloca i64, align 8
  %2 = bitcast i64* %1 to i8*
  %3 = bitcast i64* %1 to %jl_value_t*
  call void @llvm.lifetime.start(i64 8, i8* %2)
; Location: REPL[19]:1
  %4 = bitcast %jl_value_t* %3 to double*
  ret double* %4
}

Copy link
Contributor Author

@musm musm Aug 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure If I should change this or not, in the example here @yuyichao uses a Ptr for arg type for the Ref in the example JuliaLang/julia#22684

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both Ptr and Ref work, but Ref is the recommended style for pass-by-reference arguments. I normally would only use Ptr{T} for passing pointers to arrays (or raw pointers, of course).

@@ -499,7 +509,7 @@ Bessel function of the first kind of order 0, ``J_0(x)``.
"""
function besselj0(x::BigFloat)
z = BigFloat()
ccall((:mpfr_j0, :libmpfr), Int32, (Ptr{BigFloat}, Ptr{BigFloat}, Int32), &z, &x, ROUNDING_MODE[])
ccall((:mpfr_j0, :libmpfr), Int32, (Ref{BigFloat}, Ref{BigFloat}, Int32), z, x, ROUNDING_MODE[])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why doesn't z need to be a Ref here, since, since it is modified by the ccall?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused z is a Ref here?

Copy link
Member

@stevengj stevengj Aug 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

z is being passed by reference, and is modified by mpfr_j0 . z is copied to a Ref(z) container by ccall when it is passed to the Ref{BigFloat} argument, and the mpfr_j0 actually modifies this copy. I think the reason is the original z is still modified is because a BigFloat object contains a pointer to the actual "limb" data, and this raw pointer was copied over to Ref(z), so z still sees the modified limbs.

Still, it seems like it would be clearer if we initialized z = Ref(BigFloat()) and then returned z[].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed (yeah the logic is a bit confusing with all the cconverts and unsafe_converts that occur....)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So If I understand correctly the previous usage also had the same confusing behavior i.e. when passing &z .

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

z is being passed by reference, and is modified by mpfr_j0 .

Yes.

z is copied to a Ref(z) container by ccall when it is passed to the Ref{BigFloat} argument, and the mpfr_j0 actually modifies this copy.

No.

I think the reason is the original z is still modified is because a BigFloat object contains a pointer to the actual "limb" data, and this raw pointer was copied over to Ref(z), so z still sees the modified limbs.

So No.

Still, it seems like it would be clearer if we initialized z = Ref(BigFloat()) and then returned z[].

The two do the same and both mutate the origional BigFloat. Wrapping in Ref makes this less clear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see...so when you pass an instance z of a mutable type for a Ref argument then it actually passes a pointer to the original data in z?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. It always pass an pointer to a memory with the same layout as the object being Refd (don't know how to describe this but hopefuly that's unambiguous). For !isbits objects this is the object itself and for a copy for isbits ones.

@musm musm merged commit aba8c8a into master Aug 17, 2017
@musm musm deleted the musm-patch-1 branch August 17, 2017 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants