-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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
cmd/cgo: should make pointers passed to C functions escape #10303
Comments
well, this is not a escape analysis bug per-se.
For example, the example function call C.Foo(&buf) in the issue report
gets translated to a call to this func:
func _Cfunc_Foo(p0 *_Ctype_struct_Buf) (r1 _Ctype_void) {
_cgo_runtime_cgocall_errno(_cgo_0b9ef0be51ea_Cfunc_Foo,
uintptr(unsafe.Pointer(&p0)))
return
}
And you can see the p0 really is not escaping.
We should add something like:
use(unsafe.Pointer(p0))
where use is defined in assembly as a NOP, and have the prototype:
func use(unsafe.Pointer)
Just like what we did for the syscall package.
The reason I think it is not done is that we still are not clear about what
kind of
guarantee we want to provide for passing Go pointers to C code. In the pass
we talked about always making a copy, but apparently that proposal is
dismissed
after hearing severe opposing voice from the community.
|
Note that what Minux describes is a change from previous behavior, and escape analysis is "tricked" because now there is a "Go function body" that used to be written in C (hence, opaque to escape analysis). Speaking as someone new to Go and with a pathological imagination about how optimizers can jerk you around, I'd be a little worried that (future) better dead code elimination might disappear all the Go-side pointers and make the underlying object eligible for GC. In addition, if anyone on the C side gets the idea that they can mutate pointers in objects on the Go heap, sadness and woe might ensue (in a crashy and Heisenbuggy way). |
I was actually wondering about that too when a NOP function was mentioned but I know very little about Go internals. FWIW, I frequently use cgo libraries that mutate Go-allocated memory. |
The NOP function ("use") I mentioned will be defined in assembly, so
Go compiler won't know anything about it except its signature.
This suffices to "trick" the compiler into believing the argument for it
always escapes.
See http://golang.org/src/syscall/zsyscall_linux_amd64.go#L17,
http://golang.org/src/syscall/syscall.go#L92 and
http://golang.org/src/syscall/asm.s for an example.
|
Go-allocated objects should not be used by C outside of the cgo call (i.e. remembered for future use). So allocating them on stack is OK and beneficial, we do this for syscalls as well. |
Whether or not I agree with that argument (I'm definitely an "opposing voice in the community" when it comes to the current GC plans wrt heap data, see https://groups.google.com/forum/#!msg/golang-dev/JO99bwdOICs/oV3p-vaBbeEJ or https://groups.google.com/forum/#!msg/golang-dev/qvOqcmAkKnA/CcdW-ogVgrYJ), we are definitely getting corruption due to this issue for cgo code that is not remembering the passed-in pointer somewhere after it returns. If I recall correctly, certain small syscalls don't have to do as much bookkeeping and setup as cgo? Either way, for a C function that was purely operating on the passed-in pointer and not remembering it, the pointer became invalid due to stack-moving, in our experience. |
that's kinda an important detail worth noting in the issue description |
You're right, I apologize for leaving it out. The reason we discovered the unexpected difference in escape analysis is due to infrequent corruption in one of our cgo libraries. We were able to prove that when the corruption happened, the correct pointer address had changed in Go-land.
|
On Wed, Apr 1, 2015 at 2:04 AM, Dmitry Vyukov notifications@github.com
|
Syscalls also accept uintptr's, so they should not promote pointers to heap. |
Passing pointers to C prevents (or if one prefers breaks) optimizations The runtime team has chosen to port all of its code, including the entire On Wed, Apr 1, 2015 at 3:44 AM, Dmitry Vyukov notifications@github.com
|
Hi @RLH! As discussed on https://groups.google.com/forum/#!msg/golang-dev/JO99bwdOICs/oV3p-vaBbeEJ and https://groups.google.com/forum/#!msg/golang-dev/qvOqcmAkKnA/CcdW-ogVgrYJ at length (and perhaps in other places), this effectively kills the performance of a number of cgo libraries. The two main examples are people hoping to use fast math libraries written in C, and in my case, people who want to implement io.Reader and io.Writer in C. There is no way to implement an io.Reader or an io.Writer using a cgo library, given what you say, without adding forced (and imo unnecessary) memory copies, which will dramatically slow performance. There's no way to require that all byte slices passed to my cgo io.Reader/io.Writer were originally allocated with C.malloc or equivalent. The discussion in both threads seemed to indicate that this might be a good point, and the Go team was considering another way. |
In the mean time, as long as these GC optimizations aren't yet implemented, fixing this particular issue seems to be a great way to keep current Go/cgo code working as expected by people using this type of scheme. I assume this issue isn't the only reason the Go team chose to port the Go compiler to Go. |
Syscalls still stay in C. Most real Go programs crucially rely on performance of zero-copy passing of byte slices between Go and C. Allocating these byte slices in C memory in not feasible. So zero copy passing of byte slices between Go and C should stay. |
I agree it does seem like keeping zero-copy byte slices between C and Go kills a lot of possible Go GC strategies (unless byte slices are special cased and come out of a different memory segment I suppose). @dvyukov, in the case that the heap isn't being moved around, is there much harm in forcing things that go into C-land to the heap? I think you end up getting the ability to remember Go pointers in C-land for free that way. |
I can see the problem with passing stack pointers into C. While in C the So I don't think we should allow pointers to stack objects to escape to C. The other option would be to disable shrinking while in cgo. Or shrink On Wed, Apr 1, 2015 at 7:36 AM, JT Olds notifications@github.com wrote:
|
I was going to say more last night but did not, but the way this has been In particular, that brand-X Java implementation had an all-objects-moving The lack of traced/untraced in the type system means that you are not An altnerate way to address the leaking-pointers problem w/o separating Slices are also a problem because the usual idiom uses finalizers in a On Wed, Apr 1, 2015 at 10:36 AM, JT Olds notifications@github.com wrote:
|
We don't shrink stack while in C or in syscall. |
This discussion is getting way out of scope for the issue. We are not going to redefine the way cgo works in this issue. The problem is that when I converted cgo from generating C stubs to generating Go stubs, I did not preserve the semantics of the generated C stubs, namely that (1) from the compiler's perspective all pointers passed to the stubs escape, and (2) from the garbage collector's perspective all pointers passed to the stubs are kept live for the duration of the stub call. As Minux wrote, the fix is similar to what we did in syscall: insert use(x) calls in the stubs after C is done with x, both to make x appear to escape and to make sure it is considered live until that point. Something along these lines should work and not require any assembly. The diff below illustrates what I mean but is not complete (isPointer needs to be defined) and not tested.
|
Dmitry, I think you're right that copying can never happen in a syscall. On Wed, Apr 1, 2015 at 12:23 PM, Russ Cox notifications@github.com wrote:
|
@randall77 You are right! How it ever worked?.. Need more tests. |
Thank you @dvyukov I will try to create similar test for windows callbacks. Alex |
I think windows syscall might have same problem:
c does not escape to the heap. Our asm code writes into c.r1, c.r2, c.err just before syscall returns. That could be fatal if stack moved. Isn't it? Alex |
@alexbrainman It looks fatal. Also, are user args (a1, a2, a3) escape? |
I suspect some Syscall parameters do not escape. (I could not check #10391) I fixed special case with string parameters. But I don't see why other parameters have to escape. I suspect we don't see the problem, because we don't use many windows syscalls that do callbacks. And the syscalls that do, perhaps, do not receive Go pointers as arguments. Maybe we just need to fix "return values" as described above. I will try to create a test that breaks as per your description. Thank you. Alex |
If the asm code saves c pointer on Go stack, then it should be updated as well, provided that the stack slow is marked pointer. Looking at the code:
It seems that c is saved on stack, however, I don't see anything that marks this stack slot as pointer... |
asmstdcall is running on OS stack. We switching stacks in asmcgocall. I guess we should be OK then. Except for when syscall is doing callback, then we need to be careful to have any syscall "addressed" parameters stored on the heap, not on Go stack. Sounds right? Alex |
Right. Including "var c libcall", it also must be on heap. PRobably it can be part of m struct. |
What is the advantage of having libcall field in m? Alex |
Hold on. We save original c address, but then Go stack can move, so asm will be using old c address. I will try and write those tests to see what happens. Alex |
It would be a good idea to have a GODEBUG option which forces runtime to offset all stacks by a random constant. And then run all.bash in a loop in this mode. Or maybe it can be testing package flag. I.e. run tests with all possible stack offsets. |
Created new issue #10406 for windows. Alex |
Interesting, @minux, how come http://golang.org/src/syscall/syscall.go#L92 has //go:noescape? Isn't that the opposite of what we want? |
For anyone who cares, here's how I fixed this for Go 1.4.2 (compiles, passes all tests, etc). If someone wants to turn it into a patchset for tip I was too afraid to merge my new byValue function with cgoType and change cgoType, but it probably should just use cgoType instead. I'd be easily talked into giving it a shot though if this is a reasonable approach. It'd also be worth writing a test for this behavior instead of my by-hand testing, but I wasn't sure the best way. Maybe put a bunch of stuff on the stack, call a C function with something from the middle of all the stack-allocated variable definitions, and make sure it's outside of that memory range? |
We definitely want syscall.use to be noescape. The point of that function is not to make the pointers escape--from the point of view of Go, they do not. The point is to make sure that the pointers are live after the function call, as the function call itself will not make them live since they are being passed as uintptr. See https://golang.org/cl/139360044 . |
Well a use call like this is good for both keeping a variable live, but also keeping it on the heap. My understanding of this specific issue is that due to stack moving we want to force variables passed to other systems (C/kernel/whatever) to the heap? I guess syscalls are probably different. |
Syscalls are different because we know for sure that they don't squirrel away a pointer. Pointers passed to system calls do not escape. On the other hand, if we can shrink the stack while blocked in syscall.Read, we definitely have a bug. |
One option for the cgo callback case is to start a new stack for the callback. It might be a bit heavyweight, but it means we can forbid stack copying while in cgo. That might buy us a lot more than the (relatively uncommon?) callback case. |
|
whoops, so sorry, i read way wrong. forbid stack copying. that's probably great. i thought you were saying new special stack for every cgo call. it definitely seems like there's two concerns that may have different solutions basically I think cgo has the same two dimensions that syscalls have: (1) whether or not memory passed in escapes (syscalls definitely don't squirrel away memory, cgo could (saved callbacks references, etc)), and (2) whether or not the stack can shrink under cgo calls invalidating passed in memory. seems like dimension 2 could be solved a number of ways (forcing to the heap, or yeah, forbid stack copying while in cgo) separately from how dimension 1 is solved, but yeah it would be nice if it was pretty snappy at least for my case. forcing stuff to the heap seems necessary for dimension 1, but perhaps that's not the common case and could be opt in? |
CL https://golang.org/cl/10814 mentions this issue. |
Assume you have some package foo that uses a C function:
If you compile this package with Go 1.2 (what we use, until we can do some big audit of our cgo usage), we get this:
This is what we'd expect. Since the heap is (currently) a safe place to put Go-allocated memory referenced by C, we'd expect passing &buf to C.Foo to indicate buf should be heap-allocated.
On Go 1.4, we get this:
Foo &buf does not escape? It totally should! Since it didn't get escaped, we're now passing stack-allocated memory into C. Holy cow that's a bad idea with 1.4, cause stack allocated memory gets moved around.
And here's tip:
The text was updated successfully, but these errors were encountered: