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

runtime: preserve extra M across calls from C to Go #51676

Closed
doujiang24 opened this issue Mar 15, 2022 · 17 comments
Closed

runtime: preserve extra M across calls from C to Go #51676

doujiang24 opened this issue Mar 15, 2022 · 17 comments
Labels
help wanted NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@doujiang24
Copy link
Contributor

There are 5 sigprocmask calls and 3 sigaltstack calls when calling every go exported function from C.

syscall during needm:

rt_sigprocmask(SIG_SETMASK, NULL, [], 8) = 0
rt_sigprocmask(SIG_SETMASK, ~[], NULL, 8) = 0
sigaltstack(NULL, {ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}) = 0
sigaltstack({ss_sp=0xc00003e000, ss_flags=0, ss_size=32768}, NULL) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0

syscall during dropm:

rt_sigprocmask(SIG_SETMASK, ~[], NULL, 8) = 0
sigaltstack({ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}, NULL) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0

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.

@doujiang24
Copy link
Contributor Author

I finished a draft PR for this proposal: #51679
Any feedbacks are welcome, thanks!

With PreBindExtraM, c call go is ~30x faster in the following simple test case:

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 PreBindExtraM:

$ time ./hello 1000000
max loop: 1000000
2 + 3 = 5

real    0m0.150s
user    0m0.156s
sys     0m0.010s

benchmark without PreBindExtraM(just remove it):

$ time ./hello 1000000
max loop: 1000000
2 + 3 = 5

real    0m5.088s
user    0m1.536s
sys     0m4.116s

@ianlancetaylor
Copy link
Contributor

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 ianlancetaylor changed the title proposal: cgo: add PreBindExtraM to reduce signal syscall. proposal: cmd/cgo: add PreBindExtraM to reduce signal syscall Mar 15, 2022
@doujiang24
Copy link
Contributor Author

@ianlancetaylor Thanks.

Is user code expected to call PreBindExtraM? What is the exact semantics of that function? How would you write user documentation for it? Thanks.

Yes, user code have to call PreBindExtraM to enable this optimization, as shown in the hello.c.
Without the additional call of PreBindExtraM, everything just works as previous, nothing changes.

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:

  1. bind an extra M(also a P, we don't care it here),
  2. execute the go function,
  3. drop the extra M (P).

In step 1 (needm) and step 3 (dropm), there are five signal syscalls.

To avoid these five signal syscall, cgo also generated a built-in C function PreBindExtraM.
You can call PreBindExtraM to pre-bind extra M, before you call any go exported functions, after you loaded the go so file.
After pre-binding extra M, step 1 and step 3 will be skipped when calling any go exported functions.

@aclements
Copy link
Member

I haven't thought through this deeply, but is the TODO(rsc) comment on dropm relevant to this case? It seems like if the runtime could use TLS to bind an M to a C thread, we wouldn't need to manipulate the sigaltstack so frequently. But I may be wrong about that.

@ianlancetaylor ianlancetaylor added this to Incoming in Proposals (old) Mar 16, 2022
@ianlancetaylor
Copy link
Contributor

OK, I think that in effect what the suggested change does is, for a thread created by C, set the g TLS variable to a newly created G and and associated M. However, there is no way to actually release that G and M if the thread exits.

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 g TLS variable. Then we just keep that around, but if the thread exits we release that G and M and put the M back on the extram list.

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.

@ianlancetaylor ianlancetaylor changed the title proposal: cmd/cgo: add PreBindExtraM to reduce signal syscall runtime: preserve extra M across calls from C to Go Mar 16, 2022
@ianlancetaylor ianlancetaylor removed this from Incoming in Proposals (old) Mar 16, 2022
@ianlancetaylor ianlancetaylor added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Mar 16, 2022
@ianlancetaylor ianlancetaylor modified the milestones: Proposal, Backlog Mar 16, 2022
@thepudds
Copy link

Then we just keep that around, but if the thread exits we release that G and M and put the M back on the extram list.

Sorry for basic question, but today does it already track when a thread created by C exits?

@thepudds
Copy link

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...

@ianlancetaylor
Copy link
Contributor

Yes, we would use pthread_key_create with a destructor function. We wouldn't actually track when a thread exits as such.

@doujiang24
Copy link
Contributor Author

Oh, agreed, the TODO by @rsc is a better approach. Using pthread_key_create to register a destructor is a good idea.

set the g TLS variable to a newly created G and and associated M.

Do it need to create a new g? Maybe using the g0 could be a better choice, as it does now.

Does the following change is in the right way? I would love to have a try. Thanks.

  1. pthread_key_create to register a destructor when loading go so file, maybe in the x_cgo_sys_thread_create function.
  2. needm in _cgo_wait_runtime_init_done when thread key value is NULL, also, set the thread key value to a non-NULL value.
  3. when the destructor is called, dropm.

In short, we always try to pre-bind M in every Go exported function. And drop M in destructor to avoid M leaking.

@ianlancetaylor
Copy link
Contributor

Do it need to create a new g? Maybe using the g0 could be a better choice, as it does now.

Yes, that is the right thing to do.

Your set of steps sounds basically right.

@doujiang24
Copy link
Contributor Author

Okay, jumping out of the pre-bind rabbit hole, maybe step 2 change to the following is simpler (or expected)?
2. skip dropm when a destructor is registered.

@aclements
Copy link
Member

I'm not sure I completely understand what you mean, but I think that's the right direction. cgocallback already calls needm if the g TLS isn't set, so it's probably easiest to let it keep doing that, rather than moving responsibility for that to _cgo_wait_runtime_init_done, and just leave that g/m set. That also means we don't need to access this new pthread_key's value from Go; we're only using it for its destructor.

_cgo_wait_runtime_init_done might be a good place to ensure the pthread_key is set to a non-NULL value for that thread (otherwise the destructor won't be called), and possibly a good place to ensure the pthread_key has been created in the first place.

Creating the m in _cgo_wait_runtime_init_done would probably work, but it's sort on the wrong side of the language divide.

@doujiang24
Copy link
Contributor Author

Yeah, I mean keep needm in cgocallback, and skip dropm when a destructor is registered by pthread_key_create, since I have noticed the following comment for dropm in source code.

// We may have to keep the current version on systems with cgo
// but without pthreads, like Windows.

_cgo_wait_runtime_init_done might be a good place to ensure the pthread_key is set to a non-NULL value for that thread

Yeah, this sounds better than x_cgo_sys_thread_create. I will have a try. Thanks.

@doujiang24
Copy link
Contributor Author

I have implemented the new way in CL 387415.
Please help to take a look if it's the right direction. If yes, I'll continue to improve it.
Any feedbacks are welcome, thanks.

In CL 387415, we introduced to variables:

  1. x_cgo_pthread_key_created indicates if we have registered the destructor or not,
  2. x_cgo_dropm to save the cgodropm function address, since I found it's hard to import cgodropm from go to gcc_libinit.c.

@gopherbot
Copy link

Change https://go.dev/cl/392854 mentions this issue: runtime/cgo: store M for C-created thread in pthread key

@thepudds
Copy link

thepudds commented Mar 2, 2023

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):

PFLS_CALLBACK_FUNCTION callback function
An application-defined function. If the FLS slot is in use, FlsCallback is called on fiber deletion, thread exit, and when an FLS index is freed. Specify this function when calling the FlsAlloc function.

Some more from another piece of the Fiber documentation, including how FLS is treated if no fiber switching has happened:

Fiber Local Storage
A fiber can use fiber local storage (FLS) to create a unique copy of a variable for each fiber. If no fiber switching occurs, FLS acts exactly the same as thread local storage. The FLS functions (FlsAlloc, FlsFree, FlsGetValue, and FlsSetValue) manipulate the FLS associated with the current thread. If the thread is executing a fiber and the fiber is switched, the FLS is also switched.

There is also a related discussion in the MSDN forums here about trying to emulate a pthread_key_create destructor on Windows:

https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/043b1f9f-47f2-4905-a6ab-89c8c6172c28/thread-local-storage?forum=windowssdk

In that discussion, user deltamind106 (who has with 5 MSDN karma points) suggests that Fibers are not an appropriate solution. However, user HomeCloset (who has ~6,000 MSDN karma points) contradicts them and seems to suggest Fibers are useful for similar destructor functionality as pthread_key_create... but the discussion is perhaps open to interpretation. There are some other possible approaches that are also discussed there.

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.

gopherbot pushed a commit that referenced this issue Mar 24, 2023
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 <cherryyz@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Run-TryBot: Cherry Mui <cherryyz@google.com>
@gopherbot
Copy link

Change https://go.dev/cl/479255 mentions this issue: runtime: fix ARM assembly code in cgocallback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants