-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
runtime: preserve extra M across calls from C to Go #51676
Comments
I finished a draft PR for this proposal: #51679 With hello.go package main
import "C"
//export AddFromGo
func AddFromGo(a int64, b int64) int64 {
return a + b
}
func main() {} hello.c #include <stdio.h>
#include "libgo-hello.h"
#include <stdlib.h>
int main(int argc, char **argv) {
long a = 2;
long b = 3;
long max = 1;
if (argc > 1) {
max = atoi(argv[1]);
}
printf("max loop: %ld\n", max);
PreBindExtraM();
long r;
for (int i = 0; i < max; i++) {
r = AddFromGo(a, b);
}
printf("%ld + %ld = %ld\n", a, b, r);
} benchmark with $ time ./hello 1000000
max loop: 1000000
2 + 3 = 5
real 0m0.150s
user 0m0.156s
sys 0m0.010s benchmark without $ time ./hello 1000000
max loop: 1000000
2 + 3 = 5
real 0m5.088s
user 0m1.536s
sys 0m4.116s |
Even after looking at the pull request I'm not sure precisely what you are proposing. Is user code expected to call PreBindExtraM? What is the exact semantics of that function? How would you write user documentation for it? Thanks. |
@ianlancetaylor Thanks.
Yes, user code have to call Let me try to write a bit document for it: When calling a go exported function in a c process, in short, it works as this flow:
In step 1 ( To avoid these five signal syscall, cgo also generated a built-in C function |
I haven't thought through this deeply, but is the TODO(rsc) comment on |
OK, I think that in effect what the suggested change does is, for a thread created by C, set the So, I agree: the TODO by @rsc is a better approach. With that approach, the first time a C thread calls into Go we allocate a G and M and set the Note that we will get into trouble if the C thread calls Go code, then disables the signal stack, then calls Go code again. Perhaps that case is not worth worrying about. I'm going to take this out of the proposal process because I think we can get the same effect without an API change. |
Sorry for basic question, but today does it already track when a thread created by C exits? |
To partly answer my own question, it looks like registering a destructor which would be called on thread exit would be part of the work here... |
Yes, we would use |
Oh, agreed, the TODO by @rsc is a better approach. Using
Do it need to create a new Does the following change is in the right way? I would love to have a try. Thanks.
In short, we always try to pre-bind M in every Go exported function. And drop M in destructor to avoid M leaking. |
Yes, that is the right thing to do. Your set of steps sounds basically right. |
Okay, jumping out of the |
I'm not sure I completely understand what you mean, but I think that's the right direction.
Creating the m in |
Yeah, I mean keep
Yeah, this sounds better than |
I have implemented the new way in CL 387415. In CL 387415, we introduced to variables:
|
Change https://go.dev/cl/392854 mentions this issue: |
Some time ago, I had briefly looked into whether an equivalent solution might be possible on Windows. FWIW, some people seem to suggest that Fiber Local Storage functions on Windows could provide a destructor call back on thread exit, even if not using Fibers. It looks like FlsAlloc takes an FlsCallback that is called at thread exit (and fiber deletion):
Some more from another piece of the Fiber documentation, including how FLS is treated if no fiber switching has happened:
There is also a related discussion in the MSDN forums here about trying to emulate a pthread_key_create destructor on Windows: In that discussion, user For Fiber Local Storage, one wrinkle would be if the user code is itself using fibers and for example deletes the fiber of interest. At that point, as I understand it the destructor would be called before the thread exited, but maybe things could be set up in a way so that scenario is just a performance hit (that is, ~similar performance as happens today) where the M and whatever other resources are released "early" compared to if the fiber hadn't been deleted? And if the fiber is not deleted, then thread exit still properly releases things, which avoids a leak. Maybe? In any event, this might not work, and please take with a large grain of salt, but I wanted to at least leave a note here in case it is helpful for any future work after the (exciting!) non-Windows version lands. |
A comparison instruction was missing in CL 392854. Should fix ARM builders. For #51676. Change-Id: Ica27a99be10e595bab4fad35e2e6c00a1c68a662 Reviewed-on: https://go-review.googlesource.com/c/go/+/479255 TryBot-Bypass: Cherry Mui <[email protected]> Reviewed-by: Michael Pratt <[email protected]> Run-TryBot: Cherry Mui <[email protected]>
Change https://go.dev/cl/479255 mentions this issue: |
Change https://go.dev/cl/481061 mentions this issue: |
This reapplies CL 392854, with the followup fixes in CL 479255, CL 479915, and CL 481057 incorporated. CL 392854, by doujiang24 <[email protected]>, speed up C to Go calls by binding the M to the C thread. See below for its description. CL 479255 is a followup fix for a small bug in ARM assembly code. CL 479915 is another followup fix to address C to Go calls after the C code uses some stack, but that CL is also buggy. CL 481057, by Michael Knyszek, is a followup fix for a memory leak bug of CL 479915. [Original CL 392854 description] In a C thread, it's necessary to acquire an extra M by using needm while invoking a Go function from C. But, needm and dropm are heavy costs due to the signal-related syscalls. So, we change to not dropm while returning back to C, which means binding the extra M to the C thread until it exits, to avoid needm and dropm on each C to Go call. Instead, we only dropm while the C thread exits, so the extra M won't leak. When invoking a Go function from C: Allocate a pthread variable using pthread_key_create, only once per shared object, and register a thread-exit-time destructor. And store the g0 of the current m into the thread-specified value of the pthread key, only once per C thread, so that the destructor will put the extra M back onto the extra M list while the C thread exits. When returning back to C: Skip dropm in cgocallback, when the pthread variable has been created, so that the extra M will be reused the next time invoke a Go function from C. This is purely a performance optimization. The old version, in which needm & dropm happen on each cgo call, is still correct too, and we have to keep the old version on systems with cgo but without pthreads, like Windows. This optimization is significant, and the specific value depends on the OS system and CPU, but in general, it can be considered as 10x faster, for a simple Go function call from a C thread. For the newly added BenchmarkCGoInCThread, some benchmark results: 1. it's 28x faster, from 3395 ns/op to 121 ns/op, in darwin OS & Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 2. it's 6.5x faster, from 1495 ns/op to 230 ns/op, in Linux OS & Intel(R) Xeon(R) CPU E5-2630 0 @ 2.30GHz [CL 479915 description] Currently, when C calls into Go the first time, we grab an M using needm, which sets m.g0's stack bounds using the SP. We don't know how big the stack is, so we simply assume 32K. Previously, when the Go function returns to C, we drop the M, and the next time C calls into Go, we put a new stack bound on the g0 based on the current SP. After CL 392854, we don't drop the M, and the next time C calls into Go, we reuse the same g0, without recomputing the stack bounds. If the C code uses quite a bit of stack space before calling into Go, the SP may be well below the 32K stack bound we assumed, so the runtime thinks the g0 stack overflows. This CL makes needm get a more accurate stack bound from pthread. (In some platforms this may still be a guess as we don't know exactly where we are in the C stack), but it is probably better than simply assuming 32K. Fixes #51676. Fixes #59294. Change-Id: I9bf1400106d5c08ce621d2ed1df3a2d9e3f55494 Reviewed-on: https://go-review.googlesource.com/c/go/+/481061 Reviewed-by: Michael Knyszek <[email protected]> Run-TryBot: Cherry Mui <[email protected]> Reviewed-by: DeJiang Zhu (doujiang) <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
Change https://go.dev/cl/485275 mentions this issue: |
This reverts CL 481061. Reason for revert: When built with C TSAN, x_cgo_getstackbound triggers race detection on `g->stacklo` because the synchronization is in Go, which isn't instrumented. For #51676. For #59294. For #59678. Change-Id: I38afcda9fcffd6537582a39a5214bc23dc147d47 Reviewed-on: https://go-review.googlesource.com/c/go/+/485275 TryBot-Result: Gopher Robot <[email protected]> Auto-Submit: Michael Pratt <[email protected]> Run-TryBot: Michael Pratt <[email protected]> Reviewed-by: Than McIntosh <[email protected]>
Changes were reverted again, so reopening this issue. |
Change https://go.dev/cl/485500 mentions this issue: |
Reverted again in CL 492995. 😭 |
Change https://go.dev/cl/495855 mentions this issue: |
Change https://go.dev/cl/499716 mentions this issue: |
For #51676. For #58645. Change-Id: I9045051b5a25c6dfc833eef13e6c105a0d8ae763 Reviewed-on: https://go-review.googlesource.com/c/go/+/499716 Reviewed-by: Ian Lance Taylor <[email protected]> Run-TryBot: Michael Pratt <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
There are 5
sigprocmask
calls and 3sigaltstack
calls when calling every go exported function from C.syscall during
needm
:syscall during
dropm
:We can call
PreBindExtraM
to bind extra M after loaded go so file and before call any go exported functions, for better performance.And nothing changes without this
PreBindExtraM
call.background:
We are building GoLang extension for Envoy which heavily relies on cgo.
The text was updated successfully, but these errors were encountered: