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: Go 2: leaking argument qualifier #30317

Closed
gobwas opened this issue Feb 19, 2019 · 14 comments
Closed

proposal: Go 2: leaking argument qualifier #30317

gobwas opened this issue Feb 19, 2019 · 14 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@gobwas
Copy link

gobwas commented Feb 19, 2019

Hi,

This proposal presents the simple idea of transfering comment like "Implementations must not retain p." from interfaces like io.Reader, io.Writer and similar to the appropriate type qualifier.

The reason for doing so is that comments can not give the Go compiler any knowledge of how argument is being used inside the interface implementation, thus compiler could only "give up" and move argument to the heap, leading to probably unneccessary memory allocations.

Example. Suppose we have some interface like this:

// Implementations must not retain p.
type Reader interface {
    Read(p []byte) (int, error)
}

When some implementation of Reader is used, and some user code calls it as an interface method,

func main() {
    var r Reader = rand.New(...)
    p := make([]byte, 4)
    r.Read(p)
}

we got something like this from escape anlysis about the argument p:

proposal.go:10:11: make([]byte, 4) escapes to heap
proposal.go:10:11:     from p (assigned) at proposal.go:10:4
proposal.go:10:11:     from r.Read(p) (parameter to indirect call) at proposal.go:11:8

The idea is pretty straightforward:

type Reader interface {
    Read(p leaking []byte) (int, error)
}

Or:

type Reader interface {
    @p leaking
    Read(p []byte) (int, error)
}

Or whatever other syntax representation. That is, the idea is not in the syntax which always very controversial, but in the benefits which this qualification may give us.

With leaking (just to name it somehow) type qualifier compiler will able to do two things: first is to prepare escape analysis with little more additional knowledge (and not move leaking objects to heap); second is to restrict use of implementations that try to retain leaking arguments somewhere.

Bonus: Cases with variadic functions may also be optimized. E.g.:

type Calculator interface {
    Sum(ns ...int) int
}

func doStuff(c Calculator) {
    ...
    c.Sum(1,2,3) // [1,2,3] Will not be allocated on heap if leaky qualifier existed.
}

Sergey.

@gopherbot gopherbot added this to the Proposal milestone Feb 19, 2019
@beoran
Copy link

beoran commented Feb 20, 2019

While syntax is not that important, seeing how now most annotations in Go are done using magic comments, perhaps the syntax could be

type Reader interface {
    // go:leaking(p)
    Read(p []byte) (int, error)
}

Like this, Go compilers that don't understand how to handle this feature will ignore it.

@ianlancetaylor ianlancetaylor added LanguageChange v2 A language change or incompatible library change labels Feb 20, 2019
@ianlancetaylor
Copy link
Contributor

If I'm reading this correctly, the leaking qualifier means that the parameter does not leak. That seems like a confusing nomenclature.

@beoran
Copy link

beoran commented Feb 21, 2019

https://dave.cheney.net/2018/01/08/gos-hidden-pragmas
Apperently there is already the //go:noescape magic comment pragma. How is this idea different from that?

@gobwas
Copy link
Author

gobwas commented Feb 21, 2019

@beoran I think its the same, except the thing, that it could not be applied (I think so?) to the interface methods.

@ianlancetaylor any better naming are welcome =) Like @beoran suggested noescape could be better I think.

@ianlancetaylor
Copy link
Contributor

The go:noescape directive, documented at https://golang.org/cmd/compile, can only be applied to a func definition with no body, or in other words a function implemented in assembler. It tells the Go compiler that the assembler code does not save its pointer arguments in any way, and as such the values passed to it do not escape. The Go compiler will never see this code, so it's reasonable to have some way to tell it what is happening.

That is quite different from what is being proposed here, which is attempting to describe code written in Go, not code written in assembler. I'm not saying that we can't do this, but it's really completely different from go:noescape.

@lpar
Copy link

lpar commented Feb 22, 2019

If you flag an argument as "leaking", the compiler would need to carry that through the rest of the code to make sure it isn't (say) assigned to a variable which is placed in a slice which is pointed to by a struct which is retained. At that point, it seems like you're asking for something like the Rust borrow checker in Go?

@gobwas
Copy link
Author

gobwas commented Feb 22, 2019

@lpar It could be named like "Rust's borrow checker in Go" or just "Go's advanced escape analysis". What would need the compiler to carry except the interface implementation? I mean, if interface implementation does not pass "not escaping/leaking" argument to the interface without such qualifier, nor to the non-interface function where the argument could escape (which current escape analysis already doing) and does not save this argument in heap-based data structures, then such implementation is compatible for such interface?

@ianlancetaylor
Copy link
Contributor

If we add this type qualifier, then presumably the compiler will follow it. That means if a method with a non-leaking parameter does accidentally leak it, the program has a serious bug that will lead to memory corruption.

@gobwas
Copy link
Author

gobwas commented Feb 27, 2019

@ianlancetaylor how could we pass non compatible implementation of an interface to the method expecting it? I mean, if concrete type X is passed (or assigned) as interface Y, compiler must check the compatibility, including arguments qualifier restrictions. Thus, X could not accidentally leak its argument.

@griesemer
Copy link
Contributor

griesemer commented Feb 27, 2019

It seems to me that this proposal confuses two different (albeit intertwined) ideas:

  1. Escape information annotation for function parameters
  2. Usage "protocol" information

Comments such as "Implementations must not retain p." are the latter (2): What the annotation says is that independent of the actual details of code generation and variable allocation, a concrete method Read implementing the respective interface method Read must not retain p because the caller may re-use the p slice. If Read held on to p, the p contents may be changed.

Note that this is independent of escape information. Yes, it's true that if my Read held on to p, then it escaped. But one could imagine a hypothetical super smart compiler that could analyze the life time of p across call sites and decide that it's ok to allocate p's underlying array on the stack.

On the other hand, if this proposal is about (1), then we would have to annotate all Read methods that can possibly implement the interface's Read method with the same annotation, so that the compiler can verify that the concrete Read methods satisfy the no-escape criteria for the respectively marked parameters. We must do this because these methods may be compiled separately and independently from the interface, and don't necessarily know that they will be used with the interface. But the real problem with such an annotation is that a compiler's escape analysis may or may not be able to prove that an accordingly marked parameter doesn't escape. Given:

func (*T) Read(p noescape []byte) (int, error) {
   ...
}

where p is marked to not escape (or leak), the compiler will need to check that this is indeed the case. But depending on the specific code in the method body, it may not be able to do so. Thus, whether this piece of code compiles or not isn't just specified by the language, but the the specifics of the implementation, which may change over time. Or it so severely restricts what can be done with such parameters in the code that it becomes cumbersome to use, and complicated to explain in the spec. We probably don't want that; this seems more like the approach taken in Rust than in Go.

(The existing annotations go:noescape we have for assembly methods rely on the programmer telling the truth about the assembly code - but since we're dealing with assembly code at this point, the compiler is at the mercy of the programmer anyway. We don't want to extend this into pure Go code.)

In summary, I don't think we want to pursue this: As explained above, if this proposal is about 1), whether code is valid or not becomes implementation dependent, not spec specified. If this proposal is about 2) we're opening Pandorra's box: There's much more one might want to express in "usage protocols" for function and method calls. I don't think we want to go there.

@griesemer
Copy link
Contributor

PS: I should add that in the case of (2) it will also be tricky for a compiler to decide whether a certain property (such as "noescape") is satisfied in general, same as for (1).

@ianlancetaylor
Copy link
Contributor

how could we pass non compatible implementation of an interface to the method expecting it?

You can use a type assertion on an interface value, so you can get a value of an interface type without having any knowledge of the dynamic type.

@beoran
Copy link

beoran commented Feb 28, 2019

Ah, I see, what (2) boils down to is "Contract based programming" like exists in Ada 2012. But, are we sure Go doesn't need that? With the discussion on Generics, "contracts" were suggested to verify the qualities of the type that is used to parameterize a generic type. Now imagine that in stead of just using contracts for generics, contracts could become a first class construct in Go, usable for all sorts of compile time or run time checking... think of the possibilities. 🌠
Maybe something like Read(p []byte) (int, error) range { i, e := Read(p); i >= 0 && contract(p, "notRetained") } , abusing the range keyword for a contract specification.

If we had something like first class contracts, I would say that the range types I was proposing before could simply reuse those contracts. Something like type Weekday = int range { Weekday > 0 && Weekday < 8 }

@ianlancetaylor
Copy link
Contributor

While there may be interesting ideas to explore in this area, we aren't going to adopt this specific proposal.

@golang golang locked and limited conversation to collaborators Mar 18, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

6 participants