Does `GC.@preserve` also prevent instruction re-ordering?

#1

does GC.@preserve also prevent the compiler re-ordering instructions? e.g. I have a C wrapper right now that is more or less:

obj = MyObjWithStorage()
client = getstorage(obj)
usedata(client)

where getstorage puts some pointers in client that point to data in obj, as well as some dynamically-allocated data that obj manages. Julia doesn’t know about these pointers so it thinks it can free up obj after its last usage, which then breaks usedata because the data is no longer valid. I’ve used GC.@preserve to fix this:

obj = MyObjWithStorage()
client = getstorage(obj)
GC.@preserve obj begin
    usedata(client)
end

This seems to fix the issue. Now I want to add an explicit close method to free up the dynamic data, so it’ll look more like

MyObjWithStorage() do obj
    client = getstorage(obj)
    GC.@preserve obj begin
        usedata(client)
    end
end

(where the do blockwill call close(obj) at the end and free up the malloced memory)

So I guess my question here is 2-fold:

  1. do I even need the GC.@preserve in this case, given that usedata is within the do block?
  2. if I do need the GC.@preserve, am I also guaranteed that the compiler won’t try to move the usedata call outside the do block, given that Julia doesn’t know that client depends on obj?
#2

To clarify, the do block should end up being basically the following (assuming the block function gets inlined):

obj = MyObjWithStorage()
try
    client = getstorage(obj)
    GC.@preserve obj begin
        usedata(client)
    end
finally
    close(obj)
end
#3

Yes. Unless the do block caller provides the GC.@preserve.

That will never happen in a way you can observe. Execution order is an well defined observable of your program while memory management is not so even though the compiler can do whatever it want with the memory it has no such freedom with the execution order. OTOH, if the definition of userdata and closer down to the basic operations are all known to the compiler, it is certainly allowed to do whatever reordering it want if it can proof that such transformation won’t affect the result.

So no you don’t get any guarantee about the ordering but you’ll never see any effect caused by it.

#4

So in the case where the close(obj) call follows the usedata(client) (like in the do block) then I shouldn’t need the GC.@preserve because the GC will always wait until after the close call, right?

#5

No, by which I mean that statement is wrong and you need it.

#6

Ah, I must still be confused then. For the code:

obj = MyObjWithStorage()
try
    client = getstorage(obj)
    usedata(client)
finally
    close(obj)
end

the close(obj) call (which presumably is doing something that requires a reference to obj) will always happen after usedata(client), so doesn’t that mean that obj will not be GC’ed for the duration of usedata(client)?

#7

No. As long as the compiler can do what you ask it to do in close, it doesn’t have to keep the object alive.

And in the end, what do you want to do? The GC.@preserve has almost no overhead (and in this case probably actually no overhead), unlike try-catch-finally. Why do you really want to get rid of it? As I said, if you don’t want to use it in the user code, just preserve it before passing it to the callback.

#8

Thanks for the clarification. The primary goal is to make sure I use GC.@preserve when I need to so things don’t crash. My secondary goal is to have a good understanding of when I need it so I don’t end up just sprinkling it randomly. Obv. it’s better to be conservative and preserve when in doubt, but also seems like a good test for my mental model of what the compiler is allowed to do.