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

Make local continuations direct style if possible #777

Merged
merged 4 commits into from
Jan 14, 2025

Conversation

b-studios
Copy link
Collaborator

@b-studios b-studios commented Jan 14, 2025

This PR tries to remove the overhead of CPS (and trampolining) for direct style code.

Essentially, for the following code in CPS

let cont k(x, ks) = ...
if (cond) {
  k(42, ks')
} else {
  k(43, ks')
}

we are now generating the following JS:

let x;
if (cond) {
  x = 42
} else {
  x = 43
};
...

This only works under the assumption that:

  • k is not used somewhere in a more first-class manner (under a different context, as part of a continuation passed to a function, etc.)
  • k is always called with the currently in scope meta continuation ks' (so that we can avoid assigning it -- this turns out to be essential with the feature interaction of loops)

The interaction with loops was difficult to get right:

  • turning recursive functions into direct style loops makes additional continuations more local
  • making a continuation more local turns more functions into loops

As a result, the function parse_worker (from the parsing_dollars benchmark) is now a tight loop.

Before the PR

function parse_worker_0(a_1, ks_7, k_4) {
      const x_0 = i_0.value;
      function k_3(c_0, ks_4) {
        if (c_0 === (36)) {
          return () => parse_worker_0((a_1 + (1)), ks_4, k_4);
        } else if (c_0 === (10)) {
          const x_1 = s_0.value;
          s_0.value = (x_1 + a_1);
          return () => parse_worker_0(0, ks_4, k_4);
        } else {
          return SHIFT(p_0, (k_5, ks_5, k_6) => k_6($effekt.unit, ks_5), ks_4, k_4);
        }
      }
      if ((x_0 > n_0)) {
        return SHIFT(p_0, (k_7, ks_6, k_8) => k_8($effekt.unit, ks_6), ks_7, (v_r_1, ks_8) =>
          $effekt.emptyMatch());
      } else {
        const x_2 = j_0.value;
        if (x_2 === (0)) {
          const x_3 = i_0.value;
          i_0.value = (x_3 + (1));
          const x_4 = i_0.value;
          j_0.value = x_4;
          return () => k_3(10, ks_7);
        } else {
          const x_5 = j_0.value;
          j_0.value = (x_5 - (1));
          return () => k_3(36, ks_7);
        }
      }
    }

After the PR

function parse_worker_0(a_9, ks_293, k_236) {
      parse_worker_0: while (true) {
        const x_11 = i_13.value;
        let c_0 = undefined;
        if ((x_11 > n_17)) {
          return SHIFT(p_32, (k_232, ks_292, k_233) =>
            k_233($effekt.unit, ks_292), ks_293, (v_r_95, ks_294) =>
            $effekt.emptyMatch());
        } else {
          const x_12 = j_0.value;
          if (x_12 === (0)) {
            const x_13 = i_13.value;
            i_13.value = (x_13 + (1));
            const x_14 = i_13.value;
            j_0.value = x_14;
            c_0 = 10;
          } else {
            const x_15 = j_0.value;
            j_0.value = (x_15 - (1));
            c_0 = 36;
          }
        }
        if (c_0 === (36)) {
          /* prepare tail call */
          const a_8 = (a_9 + (1));
          a_9 = a_8;
          continue parse_worker_0;
        } else if (c_0 === (10)) {
          const x_16 = s_4.value;
          s_4.value = (x_16 + a_9);
          /* prepare tail call */
          const a_10 = 0;
          a_9 = a_10;
          continue parse_worker_0;
        } else {
          return SHIFT(p_32, (k_234, ks_295, k_235) =>
            k_235($effekt.unit, ks_295), ks_293, k_236);
        }
      }
    }

Additionally, here are a few preliminary benchmark results:

image

@b-studios b-studios marked this pull request as ready for review January 14, 2025 21:06
@b-studios
Copy link
Collaborator Author

At some point, we could clean this up, add a separate IR, and / or document the transformation formally somewhere (the earlier the better, since I will forget the details).

@b-studios b-studios merged commit df1adc5 into master Jan 14, 2025
2 checks passed
@b-studios b-studios deleted the optimization/back-to-direct-style-if branch January 14, 2025 21:09
EveEme pushed a commit to EveEme/effekt that referenced this pull request Jan 20, 2025
This PR tries to remove the overhead of CPS (and trampolining) for
direct style code.

Essentially, for the following code in CPS

```
let cont k(x, ks) = ...
if (cond) {
  k(42, ks')
} else {
  k(43, ks')
}
```

we are now generating the following JS:

```javascript
let x;
if (cond) {
  x = 42
} else {
  x = 43
};
...
```

This only works under the assumption that:

- `k` is not used somewhere in a more first-class manner (under a
different context, as part of a continuation passed to a function, etc.)
- `k` is always called with the currently in scope meta continuation
`ks'` (so that we can avoid assigning it -- this turns out to be
essential with the feature interaction of loops)

The interaction with loops was difficult to get right:

- turning recursive functions into direct style loops makes additional
continuations more local
- making a continuation more local turns more functions into loops


As a result, the function `parse_worker` (from the `parsing_dollars`
benchmark) is now a tight loop.

**Before the PR**

```javascript
function parse_worker_0(a_1, ks_7, k_4) {
      const x_0 = i_0.value;
      function k_3(c_0, ks_4) {
        if (c_0 === (36)) {
          return () => parse_worker_0((a_1 + (1)), ks_4, k_4);
        } else if (c_0 === (10)) {
          const x_1 = s_0.value;
          s_0.value = (x_1 + a_1);
          return () => parse_worker_0(0, ks_4, k_4);
        } else {
          return SHIFT(p_0, (k_5, ks_5, k_6) => k_6($effekt.unit, ks_5), ks_4, k_4);
        }
      }
      if ((x_0 > n_0)) {
        return SHIFT(p_0, (k_7, ks_6, k_8) => k_8($effekt.unit, ks_6), ks_7, (v_r_1, ks_8) =>
          $effekt.emptyMatch());
      } else {
        const x_2 = j_0.value;
        if (x_2 === (0)) {
          const x_3 = i_0.value;
          i_0.value = (x_3 + (1));
          const x_4 = i_0.value;
          j_0.value = x_4;
          return () => k_3(10, ks_7);
        } else {
          const x_5 = j_0.value;
          j_0.value = (x_5 - (1));
          return () => k_3(36, ks_7);
        }
      }
    }
```

**After the PR**

```javascript
function parse_worker_0(a_9, ks_293, k_236) {
      parse_worker_0: while (true) {
        const x_11 = i_13.value;
        let c_0 = undefined;
        if ((x_11 > n_17)) {
          return SHIFT(p_32, (k_232, ks_292, k_233) =>
            k_233($effekt.unit, ks_292), ks_293, (v_r_95, ks_294) =>
            $effekt.emptyMatch());
        } else {
          const x_12 = j_0.value;
          if (x_12 === (0)) {
            const x_13 = i_13.value;
            i_13.value = (x_13 + (1));
            const x_14 = i_13.value;
            j_0.value = x_14;
            c_0 = 10;
          } else {
            const x_15 = j_0.value;
            j_0.value = (x_15 - (1));
            c_0 = 36;
          }
        }
        if (c_0 === (36)) {
          /* prepare tail call */
          const a_8 = (a_9 + (1));
          a_9 = a_8;
          continue parse_worker_0;
        } else if (c_0 === (10)) {
          const x_16 = s_4.value;
          s_4.value = (x_16 + a_9);
          /* prepare tail call */
          const a_10 = 0;
          a_9 = a_10;
          continue parse_worker_0;
        } else {
          return SHIFT(p_32, (k_234, ks_295, k_235) =>
            k_235($effekt.unit, ks_295), ks_293, k_236);
        }
      }
    }
```

Additionally, here are a few preliminary benchmark results:


![image](https://github.com/user-attachments/assets/6d3bd21a-fdcf-4149-8e0a-78ba48c4c6d2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants