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

Optimize lambda variable captures to only those needed #29

Open
ritchiecarroll opened this issue Jan 6, 2025 · 1 comment
Open

Optimize lambda variable captures to only those needed #29

ritchiecarroll opened this issue Jan 6, 2025 · 1 comment
Labels
challenge Hard problem to solve

Comments

@ritchiecarroll
Copy link
Member

Currently the performVariableAnalysis method operates in advance of actual conversion steps to know which variables are captured in a lambda expression. This is done because Go semantics make a "copy" of the variable being executed on a lambda, and this does not match C# that keeps references to captured variables. To make semantics safely match Go behavior in C#, a temporary variable is used to copy the captured variable, then the temporary variable is captured. For example:

func sieve() {
    ch := make(chan int)
    go generate(ch)
    for {
        prime := <-ch
        fmt.Print(prime, "\n")
        ch1 := make(chan int)
        go filter(ch, ch1, prime)
        ch = ch1

        if prime > 40 {
            break
        }
    }
}

Which gets converted to C# as so:

private static void sieve() {
    var ch = new channel<nint>(1);
    var chʗ1 = ch;
    goǃ(_ => generate(chʗ1));
    while () {
        nint prime = ᐸꟷ(ch);
        fmt.Print(prime, "\n");
        var ch1 = new channel<nint>(1);
        var chʗ2 = ch;
        var ch1ʗ1 = ch1;
        goǃ(_ => filter(chʗ2, ch1ʗ1, prime));
        ch = ch1;
        if (prime > 40) {
            break;
        }
    }
}

Currently this copy is done every time for all captures for data types might need a copy. In the example above this is critical since "copies" of the variables should be used in the lambdas, not references to the same variables. However, this is not always necessary. Sometimes, based on use case, a reference to the variable is just fine.

Not capturing temporary variables would make converted code much simpler and easier to read, which would be ideal where possible.

Doing this involves determining when variables need to be copied before being captured in a lambda expression to maintain Go's semantics, and which do not.

In the provided example, here are the variables that should be copied and which ones shouldn't (per added comments):

func sieve() {
    ch := make(chan int)
    go generate(ch)      // ch should be copied 
    for {
        prime := <-ch
        fmt.Print(prime, "\n")
        ch1 := make(chan int)
        go filter(ch, ch1, prime)  // ch and ch1 should be copied, but prime does not need to be copied
        ch = ch1   // This modification is why ch needs copying
        if prime > 40 {
            break
        }
    }
}

I believe the core requirements for this task are as follows (although there could be other use cases not considered):

A variable needs to be copied before capture if:

  • It's used in a lambda expression
  • AND it's modified after being captured
  • AND it's a reference type (channel, slice, map, etc.)
  • OR it's a loop variable in a lambda context

Variables should NOT be copied if:

  • They're basic types (int, string, etc.) unless they're loop variables
  • They're never modified after capture
  • They're not used in a lambda expression

The challenge is to revise the code, wherever necessary, e.g., performVariableAnalysis, visitGoStmt, visitDeferStmt, etc., to make this optimization happen.

@ritchiecarroll ritchiecarroll added the challenge Hard problem to solve label Jan 6, 2025
@ritchiecarroll
Copy link
Member Author

Here's another example - in this case none of the variables require being copied before lambda capture:

Go code:

package main

import "fmt"

func g1(ch chan int) {
	ch <- 12
}

func g2(ch chan int) {
	ch <- 32
}

func main() {
	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	ch1 := make(chan int)
	ch2 := make(chan int)
	ch3 := make(chan int)
	go g1(ch1)
	go g2(ch2)
	go g1(ch3)
	for i := 0; i < 3; i++ {
		select {
		case v1 := <-ch1:
			fmt.Println("Got: ", v1)
		case v1 := <-ch2:
			fmt.Println("Got: ", v1)
		case v1, ok := <-ch3:
			fmt.Println("OK: ", ok, " -- got: ", v1)
		}
	}
}

Converted C# code:

namespace go;

using fmt = fmt_package;

public static partial class main_package {

private static void g1(channel<nint> ch) {
    ch.ᐸꟷ(12);
}

private static void g2(channel<nint> ch) {
    ch.ᐸꟷ(32);
}

private static void Main() {
    ch.ᐸꟷ(1);
    ch.ᐸꟷ(2);
    fmt.Println(ᐸꟷ(ch));
    fmt.Println(ᐸꟷ(ch));
    var ch1 = new channel<nint>(1);
    var ch2 = new channel<nint>(1);
    var ch3 = new channel<nint>(1);
    var ch1ʗ1 = ch1;
    goǃ(_ => g1(ch1ʗ1));
    var ch2ʗ1 = ch2;
    goǃ(_ => g2(ch2ʗ1));
    var ch3ʗ1 = ch3;
    goǃ(_ => g1(ch3ʗ1));
    for (nint i = 0; i < 3; i++) {
        switch (select(ᐸꟷ(ch1, ꓸꓸꓸ), ᐸꟷ(ch2, ꓸꓸꓸ), ᐸꟷ(ch3, ꓸꓸꓸ))) {
        case 0 when ch1.ꟷᐳ(out var v1):
            fmt.Println("Got: ", v1);
            break;
        case 1 when ch2.ꟷᐳ(out var v1):
            fmt.Println("Got: ", v1);
            break;
        case 2 when ch3.ꟷᐳ(out var v1, out var ok):
            fmt.Println("OK: ", ok, " -- got: ", v1);
            break;
        }
    }
}

} // end main_package

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
challenge Hard problem to solve
Projects
None yet
Development

No branches or pull requests

1 participant