Closed xiandaonancheng closed 1 week ago
Related Issues and Documentation
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
The FAQ gives a pretty solid rationale for why TLS was intentionally omitted from the design of Go: without it, the behavior of a function call is independent of the goroutine in which it executes. Giving up that invariant for a mere performance gain seems like a very high price to pay. Could you explain in more detail how goroutine-local storage would allow you to reduce the number of objects allocated?
The FAQ gives a pretty solid rationale for why TLS was intentionally omitted from the design of Go: without it, the behavior of a function call is independent of the goroutine in which it executes. Giving up that invariant for a mere performance gain seems like a very high price to pay. Could you explain in more detail how goroutine-local storage would allow you to reduce the number of objects allocated?
I think that the goroutine pool just works like this small example below (more detail can refer the ants repo which i often use), the variable "a"/"b" is cached in the worker goroutine, so it can only alloc mem once in each worker goroutine.
func main() {
goPoolSize := 100
reqCh := make(chan func(), goPoolSize)
for i := 0; i < goPoolSize; i++ {
go func() {
for f := range reqCh {
f()
}
}()
}
server := NewServer()
for {
req := server.AcceptReq()
switch req := req.(type) {
case Req1:
reqCh <- func() {
doReq1(req)
}
case Req2:
reqCh <- func() {
doReq2(req)
}
}
}
}
func doReq1(req any) {
go_local a SomeObject
... // do something with req and a
}
func doReq2(req any) {
go_local b SomeObject
... // do something with req and b
}
Another way to reuse objects is the objects pool (goroutine-local just like objects pool binding goroutine, I think). I did some benchmark tests for the go_local (code and result are below), but unfortunately, the result says that go_local is not better than objects pool. Maybe get object from map is slower than from pool. What a terrible attempt, LOL 😓. The go_local maybe just only can reuse cached objects simply, do not consider manually type conversion and where to call pool.Put.
func BenchmarkVal(b *testing.B) {
ch, wg := goPool(goPoolSize)
for i := 0; i < b.N; i++ {
ch <- func() {
var a TVal
a.i++
ptr = &a.i // to make "a" escape
}
}
close(ch)
wg.Wait()
}
func BenchmarkGoLocal(b *testing.B) {
ch, wg := goPool(goPoolSize)
for i := 0; i < b.N; i++ {
ch <- func() {
go_local a TVal
a.i++
ptr = &a.i
}
}
close(ch)
wg.Wait()
}
func BenchmarkGoLocal2(b *testing.B) {
ch, wg := goPool(goPoolSize)
for i := 0; i < b.N; i++ {
ch <- func() {
a, _ := runtime.NewGoLocal[TVal](1, func() TVal {
return TVal{}
})
a.Val.i++
ptr = &a.Val.i
}
}
close(ch)
wg.Wait()
}
func BenchmarkPoolVal(b *testing.B) {
ch, wg := goPool(goPoolSize)
for i := 0; i < b.N; i++ {
ch <- func() {
a := pool.Get().(*TVal)
a.i++
ptr = &a.i
pool.Put(a)
}
}
close(ch)
wg.Wait()
}
func goPool(workers int) (chan func(), *sync.WaitGroup) {
wg := &sync.WaitGroup{}
ch := make(chan func(), workers)
for j := 0; j < workers; j++ {
wg.Add(1)
go func() {
defer wg.Done()
for f := range ch {
f()
}
}()
}
return ch, wg
}
var ptr *int
type TVal struct {
bs [1024]byte
i int
}
var pool = sync.Pool{New: func() interface{} {
return &TVal{}
},
}
var goPoolSize = 100
goos: darwin
goarch: amd64
pkg: github.com/golang/go/test/golocal
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkVal
BenchmarkVal-8 1983124 586.0 ns/op
BenchmarkGoLocal
BenchmarkGoLocal-8 2744718 429.9 ns/op
BenchmarkGoLocal2
BenchmarkGoLocal2-8 2720476 439.8 ns/op
BenchmarkPoolVal
BenchmarkPoolVal-8 2967628 395.1 ns/op
Why can't you create the worker state (SomeObject) explicitly in each worker goroutine and pass it to each function f?
goPoolSize := 100
reqCh := make(chan func(), goPoolSize)
for i := 0; i < goPoolSize; i++ {
go func() {
var workerState SomeObject
for f := range reqCh {
f(&workerState)
}
}()
}
I want to reiterate what @adonovan said above: the FAQ explains why we don't have goroutine-local variable. This proposal describes a way to implement goroutine-local variables. The proposal does not explain why we should do that. We made a decision long ago to not permit goroutine-local variables. We are not going to change that decision unless we have new information that suggests that we should. Thanks.
Ok, I see. Thank you for reminding me.
The goroutine local idea is from a small talk with my firend (Joey). He is a go and c++ programmer. He introduced the thread local in c++ to me. Maybe the thread local variables is mainly to keep the context or status of thread in other languages, but we are more interested in reusing the objects defined in goroutine pool. I mainly consider goroutine local objects reusing for this proposal. I will give the reasons of this proposal based on this reusing example as below.
Why can't you create the worker state (SomeObject) explicitly in each worker goroutine and pass it to each function f?
The way @adonovan mentioned above is a feasible plan to reuse objects. But i think there are some points that need attention:
I think goroutine local variables have good performance in these areas.
Based on these reasons, I think goroutine local variables can firendly help programmer easy to reuse big object resources when they use goroutine pool.
If you find any disadvantages or errors, please feel free to point them out.
The solution to your problem in Go is to use contexts, which are an explicit mechanism by which values may be passed down many levels of the call tree. The most important such variable is an event representing "has this task been cancelled?", but you can put additional arbitrary data in a context too; see context.WithValue.
As a rule, a function that accepts a Context should document which values it require to be present in the Context. Nothing will stop you from (ab)using contexts as a dumping ground of hidden state, just as often happens with thread-local store.
Yeah, You are right. I know the context.Value doesn't have any limit, but we usually only store some meta datas (trace id, session id, etc.) and settings (timeout, etc.) in context. Emmm, maybe we limit ourselves...
Based on the discussion above, and the emoji voting, this is a likely decline. Leaving open for four weeks for final comments.
No further comments.
Proposal Details
Background
As this link mentioned, the goroutines expose no unique identifier, name, or data structure to the programmer, because these maybe restrict to share more goroutines for processing. But based on my personal experience, I believe that many projects use gotoutine pool to handle several tasks. I think that we can reuse some objects in heap to reduce the number or time of GC. The goroutine local variable maybe a nice way to maintain these objects.
Design
Exposed to programmer
This proposal contains two ways to create go local variables that only have one instance for each goroutine:
1、Export a function "runtime.NewGoLocal" to create a go local variable holder by given key. Developers can create the same object holder in different scopes.
Example as below, the variables "a" and "b" are the same object holder in one goroutine:
2、Add a syntax token "go_local", just like "thread_local" in c++. Developers can define variables like "var", each variable is unique. These variables will be initialized only once in the same goroutine. Example:
Implement
To record goroutine local variables, add a map field to g struct, this map will be initialized when the call "go func" or create first go local variable.
To create goroutine local variables and alloc heap mem for them, these functions as below are written in package runtime. The function NewGoLocal and the type GoLocalHolder are exported to programmer for way 1.
For way 2, we need to modify the compiler to support the syntax token "go_local".
We need to parse the go_local decl stmt like var decl stmt in the parsing phase, and rewrite the go_local stmt to its init stmts in ir construction phase. And finally implement these rewritten stmts in the follow phases (middle end phase and ssa phase).
Before rewrite:
After rewrite:
As this rewrite example as above, we call function newGoLocalObjectForStringKey with the unique key ("a_name@a_pos") of "a" and the type of "a". Set the results to the address of "a" and a virtual bool variable indicating if "a" need to initialize.
The go_local variable "a" must be force setted to escape variable in middle end phase to ensure it can accept heap address.
The function newGoLocalObjectForStringKey will be called in ssa phase, and set the result ssa valus to the target ir name nodes (variables "a" and "_compile_only_a_need_init").
Unspport
However, the way 2, "go_local" token only can define variables in function inner scopes, can't define variables in package global scopes. Can use way 1 with same key and type instead of "go_local" in package global scopes in this proposal.
I think that package global scopes "go_local" defination maybe takes a lot work, and i have no idea to implement it. If anyone can help implement it, i will appreciate a lot.
More Details
More details can refer this PR (will update a new PR if this proposal can be approved)