rescript-compiler icon indicating copy to clipboard operation
rescript-compiler copied to clipboard

Integer/Char pattern matching output is not optimal

Open cometkim opened this issue 6 months ago • 1 comments

The integer/Char pattern matching optimization output is worse than that of the plain switch table.

For this example

benchmark.mjs
import { bench, run, barplot, summary, do_not_optimize } from "mitata";
import fc from "fast-check";

const samples = fc.sample(
  fc.integer({ min: 0, max: 256 }),
  { numRuns: 10000, seed: 42 }
);

function isFinalIf(ch) {
  return (
    ch === 48
    || ch === 49
    || ch === 50
    || ch === 51
    || ch === 52
    || ch === 53
    || ch === 54
    || ch === 55
    || ch === 56
    || ch === 57
    || ch === 65
    || ch === 66
    || ch === 67
    || ch === 68
    || ch === 69
    || ch === 70
    || ch === 71
    || ch === 72
    || ch === 73
    || ch === 74
    || ch === 75
    || ch === 76
    || ch === 77
    || ch === 78
    || ch === 79
    || ch === 80
    || ch === 82
    || ch === 83
    || ch === 84
    || ch === 85
    || ch === 86
    || ch === 87
    || ch === 88
    || ch === 89
    || ch === 90
    || ch === 97
    || ch === 99
    || ch === 102
    || ch === 110
    || ch === 113
    || ch === 117
    || ch === 121
    || ch === 61
    || ch === 60
    || ch === 62
    || ch === 126
  );
}

function isFinalSwitch(ch) {
  switch (ch) {
    case 48:
    case 49:
    case 50:
    case 51:
    case 52:
    case 53:
    case 54:
    case 55:
    case 56:
    case 57:
    case 65:
    case 66:
    case 67:
    case 68:
    case 69:
    case 70:
    case 71:
    case 72:
    case 73:
    case 74:
    case 75:
    case 76:
    case 77:
    case 78:
    case 79:
    case 80:
    case 82:
    case 83:
    case 84:
    case 85:
    case 86:
    case 87:
    case 88:
    case 89:
    case 90:
    case 97:
    case 99:
    case 102:
    case 110:
    case 113:
    case 117:
    case 121:
    case 61:
    case 60:
    case 62:
    case 126:
      return true;
  }
  return false;
}

function isFinalIncludes(ch) {
  return [
    48,
    49,
    50,
    51,
    52,
    53,
    54,
    55,
    56,
    57,
    65,
    66,
    67,
    68,
    69,
    70,
    71,
    72,
    73,
    74,
    75,
    76,
    77,
    78,
    79,
    80,
    82,
    83,
    84,
    85,
    86,
    87,
    88,
    89,
    90,
    97,
    99,
    102,
    110,
    113,
    117,
    121,
    61,
    60,
    62,
    126,
  ].includes(ch);
}

/** Generated by ReScript v12.0.0-alpha.13
 *
 * [Open in Playground](https://rescript-lang.org/try?version=v12.0.0-alpha.13&module=esmodule&code=DYUwLgBAlgzgYlAdgQ2ASUZAvBAxgCwiwD4IBvAKAghgHcowC9DLqAfCAFgA4qIPOATj4cArAAYREUQEYpogEzyAzPM7zR8gGzyA7FK2b2ELTuNb953ueHHdku3LtK7qu+rtH+EXWe+7Lf2t-W29uBzCXMLcwjzCvDm4-RMDE4MTQjkEIrNSIQUyIGXEojhliqXKYspk8mQUnby1Gji0ck1KihS0iUjAAJwBXECkAfV6IADNUGBHqAF8KRaA)
 */
function isFinalRes(ch) {
  if (ch < 63) {
    if (ch >= 58) {
      return ch >= 60;
    } else {
      return ch >= 48;
    }
  }
  if (ch < 81) {
    return ch >= 65;
  }
  if (ch >= 127) {
    return false;
  }
  switch (ch) {
    case 81:
    case 91:
    case 92:
    case 93:
    case 94:
    case 95:
    case 96:
    case 98:
    case 100:
    case 101:
    case 103:
    case 104:
    case 105:
    case 106:
    case 107:
    case 108:
    case 109:
    case 111:
    case 112:
    case 114:
    case 115:
    case 116:
    case 118:
    case 119:
    case 120:
    case 122:
    case 123:
    case 124:
    case 125:
      return false;
    case 82:
    case 83:
    case 84:
    case 85:
    case 86:
    case 87:
    case 88:
    case 89:
    case 90:
    case 97:
    case 99:
    case 102:
    case 110:
    case 113:
    case 117:
    case 121:
    case 126:
      return true;
  }
  return false;
}

summary(() => {
  barplot(() => {
    bench("if-else chain", () => {
      do_not_optimize(samples.filter(isFinalIf));
    });

    bench("switch statement", () => {
      do_not_optimize(samples.filter(isFinalSwitch));
    });

    bench("array includes", () => {
      do_not_optimize(samples.filter(isFinalIncludes));
    });

    bench("ReScript pattern", () => {
      do_not_optimize(samples.filter(isFinalRes));
    }).baseline();
  });
});

run();

And gives the following results on my machine:

$ node --expose-gc benchmark.mjs
clk: ~4.48 GHz
cpu: Intel(R) Core(TM) Ultra 7 258V
runtime: node 24.0.2 (x64-linux)

...

  ReScript pattern
   1.75x slower than switch statement
   1.24x faster than if-else chain
   2.49x faster than array includes
$ bun --expose-gc benchmark.mjs
clk: ~4.49 GHz
cpu: Intel(R) Core(TM) Ultra 7 258V
runtime: bun 1.2.13 (x64-linux)

...

summary
  ReScript pattern
   2.22x slower than switch statement
   2.03x faster than array includes
   3.33x faster than if-else chain

According to this micro benchmark, the branches generated by ReScript are never faster than the simplest naive switch case.

cometkim avatar May 23 '25 16:05 cometkim

Minified code (without the function name) size:

  • isFinalIf: 427
  • isFinalSwitch: 402
  • isFinalIncludes: 166
  • isFinalRes (ReScript): 244

Size compression looks good enough.

However, the if-else chain tends to gzip more effectively in the real world. Sequential case pass-through either.

cometkim avatar May 23 '25 16:05 cometkim