-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Description
It is widely known that passing anything to an interface method call with arguments that contain pointers causes heap allocations of that value. This happens because the compiler’s escape analysis cannot prove that the call would not cause the values to be escaped. Even though we don't know exactly which function is going to be called, there is one thing that we know at the caller site: the signature of the function being called. Using that fact, the compiler could collect all functions with the same function signature, run escape analysis over them and then use the per-signature result to decide whether dynamic dispatch calls need to have the arguments escaped. You can think of this as a fallback. Currently the fallback is to heap allocate, with this approach, if none of the functions with the same signature cause their arguments to escape, then dynamic dispatch calls would not require heap allocation either.
If this was implemented in the compiler I would expect a reduced heap allocations count in most programs, some examples:
- The byte slice passed to
io.Reader/io.Writerwould likely not need to be allocated on the heap. - Arguments passed to
http.Handlerto be stack allocated (they are often wrapped, especially thehttp.ResponseWriter). - Log functions in
log/slogwould not cause allocations, and the optimization inslog.Recordwould not be really needed anymore:
Lines 271 to 272 in 215de81
r := NewRecord(time.Now(), level, msg, pc) r.AddAttrs(attrs...)
Lines 38 to 50 in 215de81
// Allocation optimization: an inline array sized to hold // the majority of log calls (based on examination of open-source // code). It holds the start of the list of Attrs. front [nAttrsInline]Attr // The number of Attrs in front. nFront int // The list of Attrs except for those in front. // Invariants: // - len(back) > 0 iff nFront == len(front) // - Unused array elements are zero. Used to detect mistakes. back []Attr
It is also worth noting thatlog/slogAPI would likely not need theEnabled()method if this kind of optimization was present:
Lines 31 to 41 in 215de81
type Handler interface { // Enabled reports whether the handler handles records at the given level. // The handler ignores records whose level is lower. // It is called early, before any arguments are processed, // to save effort if the log event should be discarded. // If called from a Logger method, the first argument is the context // passed to that method, or context.Background() if nil was passed // or the method does not take a context. // The context is passed so Enabled can use its values // to make a decision. Enabled(context.Context, Level) bool - This would reduce the amount of people reaching for
sync.Pool(proposal: net/http: add Request.CopyTo #68501)
Obviously, this would not work in all cases and has some drawbacks, it only takes one function to force heap allocation of other dynamic dispatch calls (of functions with same signature). This might easily prevent signatures based solely on basic types from benefiting from this optimization, say: func(b []byte) { go func ( /* do sth with b */ }, so folks might pollute functions with some unused and unneeded parameters just to make this optimization work: func(b []byte, _ ...struct{}). This optimization would be beneficial for signatures that take/return custom (package-local) types, like log/slogs Handle(context.Context, slog.Record) error.
This is just an idea that came to my mind while reading #62653. I'm not even sure whether this change is rational, because changes in the escape analysis results would most likely require the dependencies to be recompiled, changes to the cache and probably some other stuff that I am not even aware of.
The no-copy string -> []byte optimization could also take benefit from this kind of optimization.
CC @golang/compiler
Metadata
Metadata
Assignees
Labels
Type
Projects
Status