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

proposal: text/template,html/template: Template.ExecuteLite skip resolution to methods to allow DCE #72895

Open
seankhliao opened this issue Mar 16, 2025 · 15 comments
Labels
LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool Proposal
Milestone

Comments

@seankhliao
Copy link
Member

Proposal Details

Use of reflect.Value.Method or reflect.Value.MethodByName disables dead code elimination in the linker as it cannot know needs to kept around vs what can be dropped.

text/template and by extension html/template are perhaps the main paths through which these methods become reachable in programs written in Go.
This leads to issues like:

I assert that many using of text/template only need to operate on static data (precomputed struct fields, maps, slices).
A new method for template execution that skips attempting to resolve arguments to methods could work for a majority of users while still enabling DCE.

package template

// ExecuteLite executes the named template like [Template.ExecuteTemplate]
// except that arguments are not resolved to methods. For example:
//
//   {{ .Foo }}
//
// Would resolve to a struct field named "Foo", or a map with the key "Foo", 
// but not a method named "Foo".
//
// Exclusive use of [Template.ExecuteLite] in rendering templates allows the compiler
// to perform dead code elimination.
func (t *Template) ExecuteLite(w io.Writer, name string, data any) error
@seankhliao seankhliao added this to the Proposal milestone Mar 16, 2025
@seankhliao
Copy link
Member Author

cc @robpike

@gabyhelp gabyhelp added the LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool label Mar 16, 2025
@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Mar 16, 2025
@robpike
Copy link
Contributor

robpike commented Mar 16, 2025

I never liked APIs designed to work around toolchain problems. They are a category error.

@seankhliao
Copy link
Member Author

seankhliao commented Mar 16, 2025

It can also be considered as a safe API which won't have any unexpected side effects from executing code.

maybe naming it ExecuteStatic

@ikonst
Copy link
Contributor

ikonst commented Mar 17, 2025

  1. Do we have other examples of such 'category errors'? Especially APIs which have been there for a while, so we can see if we've grown to regret them or whether the rationale seems moot now.

  2. To draw a parallel, consider APIs which make an RPC. They have a cost that we routinely acknowledge, and sometimes work around by providing a local alternative. How is this different from acknowledging toolchain limitations? (RPC cost is localized while DCE suppression is "use anywhere, pay everywhere", ostensibly worse, but my parallel isn't about the cost but about acknowledging cost.)

  3. To @seankhliao 's point there's silver lining to a template that's safe from code execution. I'd be surprised this didn't come up before.

@ianlancetaylor
Copy link
Member

Do we have other examples of such 'category errors'?

We try to avoid such APIs so I don't know that there are many examples in the standard library.

I don't see the comparison to RPCs as particularly relevant. RPCs have an inherent cost, as you say. That cost doesn't have anything to do with tooling, it's part of the definition of RPCs.

What we're talking about here is that the linker has a particular optimization in which it removes unreferenced methods. When some standard library calls are used, that optimization is invalid, so it isn't performed. None of this is inherent to the way that methods are used in Go. It's just a specific implementation of a specific optimization in the linker.

The argument that you are making is in some ways an argument against making this kind of optimization at all. Optimizations that have a surprising performance cliff are confusing for users. In the discussion of the CL that added this linker optimization, https://go.dev/cl/20483, text/template came up and it seems that people decided that the optimization was worth doing anyhow for small utility programs. It seems unfortunate to use that to say that now the optimization applies to large programs so we have to figure out a way to complicate templates. It seems like a valid alternative is to say that the optimization only applies to small utility programs.

@ianlancetaylor
Copy link
Member

By the way, I don't think the security argument is all that strong, because in my experience most code passes a customized type to text/template. That type has whatever methods are needed, and generally nothing else. Also, of course, most code does not use user input as a template.

That said, that does point the way to a different approach. The compiler/linker can record calls to text/template.(*Template).Execute and record the type being passed in. Then the linker could ensure that all methods of that type are retained, but not require that all methods of all types be retained.

This is, of course, a significant special case for the standard library. But all of this code is already a special case.

From a quick look, text/template is the only standard library package that calls MethodByName. The net/rpc package does call Method, but that package is not nearly as widely used.

@seankhliao
Copy link
Member Author

It's hard to show with just a github search but quite often I see Template.Execute used with map[string]any or similar, though I agree it isn't great practice working with unclear data, https://github.com/search?q=language%3AGo+%2Ft.*m%3Fp%3Fl%3F.*Execute%28Template%29%3F%5C%28.*%2C%2F&type=code

I think that nested any would defeat analysis by the compiler as different types can be passed inside?

but maybe that's better if method use in templates are really common. Also hard to search for, and we don't encourage getters but: https://github.com/search?q=%2F%5C%7B%5C%7B.*+%5C.Get.*%5C%7D%5C%7D%2F&type=code&p=1

I did propose this as I also saw this was the only stdlib api that used MethodByName

@ikonst
Copy link
Contributor

ikonst commented Mar 17, 2025

Then the linker could ensure that all methods of that type are retained, but not require that all methods of all types be retained.

Since you can chain field invocations, then also (recursively)

  • methods of field types
  • methods of method return types (if you want to double-down on special casing, you can cull methods which accept arguments as those will fail with "wrong number of args" ;)

@ianlancetaylor
Copy link
Member

We don't have to solve every existing case, we just have to provide a clear path forward for people who actually care about this. After all, ExecuteLite doesn't solve every case either.

@ikonst
Copy link
Contributor

ikonst commented Mar 17, 2025

We don't have to solve every existing case, we just have to provide a clear path forward for people who actually care about this.

Are you suggesting that the "retained" guarantee does not extend on chained methods? I assumed correctness is not negotiable.

@ikonst
Copy link
Contributor

ikonst commented Mar 17, 2025

@seankhliao wrote:

I did propose this as I also saw this was the only stdlib api that used MethodByName

To be fair, you might find yourself promulgating it into html/template's Execute and ExecuteTemplate as otherwise e.g. x/net/trace couldn't avoid the issue. A variant on "colored functions", but unlikely to go infinitely deep and virulent.

@ikonst
Copy link
Contributor

ikonst commented Mar 17, 2025

That cost doesn't have anything to do with tooling, it's part of the definition of RPCs.

I was trying to find an example where we let the imperfect characteristics of an underlying platform shape the APIs. The difference here is that networks started imperfect and got more reliable with time, while reflection started off reliable and since then been threatened by mavericks attempting to skirt safety regulations ;)

The argument that you are making is in some ways an argument against making this kind of optimization at all. Optimizations that have a surprising performance cliff are confusing for users.

Yes, and even if we made it easier to diagnose (a few proposals were made, but so far only https://github.com/aarzilli/whydeadcode offers a practical tool), the cliff-y nature is inherently confusing.

While we might've never known this optimization, now we've tasted the forbidden fruit so to speak: I've seen a 200MB binary reduced to 100MB, seen build times slashed in half (for huge monolith packages, the kind you get with some big corp SDKs). Users in charge of efficiency and developer experience especially in large companies would have a unique interest in impactful optimizations of this kind, in a way that sets them apart from regular users. It might be possible to keep both crowds happy, though of course the regular users are the important long tail for the ecosystem's success.

It can also be argued that large packages (either by themselves or through a widely cast network of imports) are what those efficiency-seekers should be after. I'm not sure how practical this approach is, but it might be.

@seankhliao
Copy link
Member Author

I don't feel that a compiler special case would be particularly clear to program authors. You can't really write a test or linter that ensures the property (presumably if you care enough for this property, you want it to continue to hold in the future). It does have the benefit that maybe more programs will immediately benefit if their data args happen to fit the criteria.

ExecuteLite is perhaps more restrictive, but a much clearer signal of intent, and can easily be checked for in tests and linters.

@ianlancetaylor
Copy link
Member

Are you suggesting that the "retained" guarantee does not extend on chained methods?

No, you're quite right about that. I was responding to an earlier comment.

I don't feel that a compiler special case would be particularly clear to program authors.

But we can see that the existing compiler special case is also not particularly clear. Adding ExecuteLite doesn't make it more clear--it just addresses one specific use of MethodByName, just as my suggestion does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool Proposal
Projects
Status: Incoming
Development

No branches or pull requests

5 participants