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: spec: shorthand for named return #59491

Closed
Sunshine40 opened this issue Apr 8, 2023 · 8 comments
Closed

proposal: spec: shorthand for named return #59491

Sunshine40 opened this issue Apr 8, 2023 · 8 comments
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal Proposal-FinalCommentPeriod v2 An incompatible library change
Milestone

Comments

@Sunshine40
Copy link

Suppose we could write:

func f() (i, j int) {
    i++
    return _, 1
}

which is equivalent to:

func f() (i, j int) {
    i++
    j = 1
    return
}

Currently, if one wants to replace j = 1; return with a one line return statement, he could write

func f() (i, j int) {
    i++
    return i, 1
}

But these are not equivalent except for the outcomes (see this StackOverflow question), And I'm not sure if this kind of "hack" should be discouraged.

@gopherbot gopherbot added this to the Proposal milestone Apr 8, 2023
@ianlancetaylor ianlancetaylor changed the title proposal: cmd/compile: shorthand for named return proposal: spec: shorthand for named return Apr 8, 2023
@ianlancetaylor ianlancetaylor added LanguageChange Suggested changes to the Go language v2 An incompatible library change labels Apr 8, 2023
@ianlancetaylor
Copy link
Contributor

It seems to me that return i, 1, which works today, is equivalent to return _, 1. Why should we introduce a new form? It doesn't seem any simpler, and seems slightly harder to understand.

@Sunshine40
Copy link
Author

return i, 1, which works today, is equivalent to return _, 1.

@ianlancetaylor I expected this to be true at the beginning, but wrote benchmark tests to verify that assumption out of curiosity.

The benchmark results showed that when unsafe.Sizeof(i) is super large (like 100000000), return i, 1 is not equivalent to return _, 1.

If the compiler is designed to see return i, 1 as equivalent to j = 1; return, then why is there the performance difference? (though only when the type is super large)

@DeedleFake
Copy link

Since most people generally seem to agree that naked returns are usually a bad practice, I'm not sure if allowing a partial naked return is a good idea. If the problem is a performance regression between return i, 1 and j = 1; return, that seems like a separate possible bug that should be addressed.

@ianlancetaylor
Copy link
Contributor

When two different pieces of code that should perform exactly the same turn out to perform differently, that should be addressed in the compiler. It should not be addressed by changing the language.

@Sunshine40
Copy link
Author

When two different pieces of code that should perform exactly the same turn out to perform differently

Well I wasn't sure that they should perform exactly the same (as it wasn't an easily google-ed documented behavior, or I missed something). And if they should, then it is a compiler issue (the original title had cmd/compile in it before it got changed). I wasn't familiar with these details to skip the "language change" part at first.

@ianlancetaylor
Copy link
Contributor

Based on the discussion above, we are not going to change the language in this way. Therefore, this is a likely decline. Leaving open for three weeks for final comments.

@apparentlymart
Copy link

apparentlymart commented Apr 12, 2023

In the original example:

func f() (i, j int) {
    i++
    return i, 1
}

...there is no explicit definition of i and so it starts as zero, and so this is functionally equivalent to:

func f() (i, j int) {
  return 1, 1
}

And indeed, that seems to be what the compiler seems to generate:

        TEXT    "".f(SB), NOSPLIT|ABIInternal, $0-0
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        MOVL    $1, AX   ; write immediate 1 into the first return value
        MOVQ    AX, BX   ; copy first return value into second
        RET              ; return

I also tried this variant:

func f(q int) (i, j int) {
    i = q
    i++
    return i, 1
}
        TEXT    "".f(SB), NOSPLIT|ABIInternal, $0-8
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $5, "".f.arginfo1(SB)
        FUNCDATA        $6, "".f.argliveinfo(SB)
        PCDATA  $3, $1
        INCQ    AX
        MOVL    $1, BX
        RET

This time the compiler noticed that the first argument and the first return value both belong in AX and so optimized it to just incrementing that register directly. It then assigned immediate 1 to the second return value.

Perhaps I'm being too easy on the compiler by using a type that fits neatly into a single machine register...

func f(q [1000]int) (i [1000]int, j int) {
    i = q
    q[1]++
    return i, 1
}
        TEXT    "".f(SB), NOSPLIT|ABIInternal, $0-16000
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $5, "".f.arginfo1(SB)
        LEAQ    "".i+8008(SP), DI
        MOVL    $1000, CX
        XORL    AX, AX
        REP
        STOSQ
        LEAQ    "".i+8008(SP), DI
        LEAQ    "".q+8(SP), SI
        MOVL    $1000, CX
        REP
        MOVSQ
        INCQ    "".q+16(SP)
        MOVL    $1, AX
        RET

I don't actually know the expected Go ABI for passing an array as a argument or as a return value, so this one's a bit harder for me to follow. From a brief read of the internal ABI docs I'm inferring that this [1000]int argument would be passed on the stack, because the array has more than one element.

If I'm reading the above correctly (which I might not be, since I am admittedly rusty on x86 assembly) I think it is directly modifying the array on the stack and then returning, without copying the array. It seems that the compiler has again found the optimization opportunity.

To complete the matrix of whether the value originates as an argument on one side and whether the value is a large aggregate on the other side, one more example:

func f() (i [1000]int, j int) {
    i[1]++
    return i, 1
}
        TEXT    "".f(SB), NOSPLIT|ABIInternal, $0-8000
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        LEAQ    "".i+8(SP), DI
        MOVL    $1000, CX
        XORL    AX, AX
        REP
        STOSQ
        MOVQ    $1, "".i+16(SP)
        MOVL    $1, AX
        RET

Again the array is allocated on the stack. Again the compiler has noticed that i must initially be all-zeroes and so it transformed the increment into a simple assignment of 1. There seems to be no copy made of the array here.

From earlier discussion I see that this is likely to be closed due to this not needing a language change, and instead being a potential optimizer improvement. But I also am having trouble finding a situation where the optimizer isn't already succeeding, so if this were to be turned into a suggestion to improve the optimizer I think it would help to share the exact benchmark code that illustrates the problem, so it's clearer what the optimizer improvement would be.

@ianlancetaylor
Copy link
Contributor

No change in consensus.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale May 10, 2023
@golang golang locked and limited conversation to collaborators May 9, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal Proposal-FinalCommentPeriod v2 An incompatible library change
Projects
None yet
Development

No branches or pull requests

5 participants