babel / minify

:scissors: An ES6+ aware minifier based on the Babel toolchain (beta)
https://babeljs.io/repl
MIT License
4.4k stars 225 forks source link

Discuss side-effects #815

Open fathyb opened 6 years ago

fathyb commented 6 years ago

Follow up to the conversation at #814.

@xtuc wrote :

Ok I didn't know about that. I haven't heard about issues with the array replacer. To me that's safe enough, but a user has no way to know/debug that.

I'm wondering if we shouldn't introduce flags, like GCC's -O1 to -Os or warning like in #570

@fathyb wrote :

I second both, I think we should warn the user by default and then use multiple optimization level options to silent the warnings and choose if we should assume side effects.

We could also offer the user to annotate its calls using comments, à la Closure. Something like :


/** @babel:side-effect **/
function foo() {
  sideEffectApi()
}

/** @babel:side-effect:expression:bar() **/
const dontTrim = bar()

The is also the sideEffects proposal from Webpack which may be handy, although I think we're not module aware.

I feel like this needs its own discussion, perhaps I should open an issue?

@vigneshshanmugam wrote :

Yeah totally agree, lets create a separate issue and discuss different methods there.

cc @devongovett for new scope-hoisting in Parcel, related parcel-bundler/parcel#1104

xtuc commented 6 years ago

Thanks for opening this issue!

I'm going to detail my comment here.

Warnings

First, I think that warnings will annoy most of the users and will eventually silent it, which defeats the purpose of having warning.

Especially since they are mostly doing safe stuff and we are telling them that we are doing unsafe transformations on their code.

Unlike what I said earlier, I'm now against that.

Compilation flags

Also what would be the API?

/** @babel:please leave this **/
({ foo: fn() }).foo

An annotation above the fn would be better I agree, but that would be very difficult for us across module boundaries and then we have the issue that nobody uses our convention on npm.

In my comment I mentioned -Os which means optimized for code size. It's not redundant with the previous options because they are general optimization (size and speed), but in the context of minify it's only code size that matter?

fathyb commented 6 years ago

If I understand correctly we're currently always on -O1 and other optimization levels are yet to be implemented?

A first step would be to use /*#__PURE__*/ (unless we already do 😅) when possible (isn't this exposed by NodePath.isPure?). For example we currently turn ['foo', fn()][0] to 'foo', it'd be better to only do this when we're sure fn() is pure.


+1 for the -Ofast flag, when working on 3D or audio processing it'is pretty useful to get rid of some abstraction tax without caring about size, like here :

function identity(size) {
  return {
    3: [
      1, 0, 0,
      0, 1, 0,
      0, 0, 1
    ],
    4: [
     1, 0, 0, 0,
     0, 1, 0, 0,
     0, 0, 1, 0,
     0, 0, 0, 1
    ]
  }[size]
}

function transposeMatrix(input, size = 3) {
  var output = new Array(size * size)

  for(let x = 0; x < size; x++)
    for(let y = 0; y < size; y++)
      output[x * size + y] = input[y * size + x]

  return output
}

const transposed3 = transposeMatrix(identity(3))
const transposed4 = transposeMatrix(identity(4), 4)

Current output

function identity(a) {
  return {
    3: [1, 0, 0, 0, 1, 0, 0, 0, 1],
    4: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
  }[a];
}
function transposeMatrix(a, b = 3) {
  var c = Array(b * b);
  for (let d = 0; d < b; d++)
    for (let e = 0; e < b; e++) c[d * b + e] = a[e * b + d];
  return c;
}
const transposed3 = transposeMatrix(identity(3)),
  transposed4 = transposeMatrix(identity(4), 4);

Using -03 + -Ofast (super-hypothetical)

function transposeMatrix_size_3(input) { return [ input[0], input[3], input[6], input[1], input[4], input[7], input[2], input[5], input[8] ] } function transposeMatrix_size_4(input) { return [ input[0], input[4], input[8], input[12], input[1], input[5], input[9], input[13], input[2], input[6], input[10], input[14], input[3], input[7], input[11], input[15] ] }

const transposed3 = transposeMatrix_size_3(identity_size_3()) const transposed4 = transposeMatrix_size_4(identity_size_4())


- Pass 2, evaluate pure functions (turns out faster doesn't always mean bigger) : 
```js
const transposed3 = [
  1, 0, 0,
  0, 1, 0,
  0, 0, 1
]
const transposed4 = [
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1
]
j-f1 commented 6 years ago

@fathyb Check out Prepack, which does pretty much what you’re asking for. Here’s an example of your code, lightly modified so that Prepack doesn’t remove all of it as dead code.

fathyb commented 6 years ago

@j-f1 Thought of it while writing this, thanks though that's relevant 👍

Prepack is really more advanced than what I need (and way too experimental for a real-world use), from what I understand it runs the code to serialize the heap and then deserialize it in form of code (it doesn't simplify/minify, it partially executes and outputs the partial state). That's a complete ECMAScript interpreter.

For example Prepack evaluates typeof something to 'undefined' unless you register an abstract for something. I'd like babel/minify to ignore code it doesn't fully understand. I don't want a partial-evaluator, just a performance oriented option.

A example in the native world is LLVM: out of the box it has speed based optimization (it has -Ofast and the code I wrote should be simplified to static arrays in certain cases), the Prepack equivalent would be LLPE.