Skip to content
kripken edited this page Oct 17, 2012 · 41 revisions

Optimizing the Generated Code

By default Emscripten will compile code in a fairly safe way, and without all the possible optimizations. You should generally try this first, to see if things work properly (if not, see the Debugging page). Afterwards, you may want to compile your code so it runs faster. This page explains how.

Note: You can also optimize the source code you are compiling into JavaScript, see Optimizing the source code.

Using emcc

The recommended way to optimize code is with emcc. emcc works like gcc, and basic usage is presented in the Emscripten Tutorial. As mentioned there, you can use emcc to optimize, for example

  emcc -O2 file.cpp

To see what the different optimizations levels do, run

  emcc --help
  • Optimization only makes sense when generating JavaScript, not when compiling to intermediate bitcode, and for that reason optimization flags are ignored unless the command will generate JavaScript. For more details see Building Projects.
  • The meaning of -O1, -O2 etc. in emcc are not identical to gcc, even though they have been chosen to be as familiar as possible! They can't be, because optimizing JavaScript is very different than optimizing native code. See the --help as mentioned before for details.
  • If you compile several files into a single JavaScript output, be sure to specify optimization during the last emcc invocation, where you transform the code into JavaScript. See Building Projects for details.

Viewing code optimization passes

If you run emcc with EMCC_DEBUG=1 (so, something like EMCC_DEBUG emcc), then it will output all the intermediate steps after each optimization pass. The output will be in TEMP_DIR/emscripten_temp, where TEMP_DIR is by default /tmp (and can be modified in ~/.emscripten).

Transforming the generated code

Sometimes it is useful to add code to the generated JavaScript before optimizations are applied. One reason this is useful is that the added code will then be optimized together with the generated code, which is very important when using the Closure Compiler, for example. Another use case is to modify the generated code in some way, for example to replace a compiled function with a handwritten one.

To do these sorts of things, you can use the --js-transform flag to emcc. It lets you define a command that is run on the raw compiled JavaScript, before optimizations. The command is run with the filename as a parameter, and you can then read that file, append to it, and overwrite it as needed. For more details, see emcc --help.

  • One dangerous situation here is replacing a generated function with a handwritten one that contains arbitrary JavaScript, because the optimizations run on the generated code in later passes assume the code is of the form generated by compilation (this allows stronger optimizations, since we know a lot about what kind of code can possibly be generated). To hide handwritten code from the optimization passes, you should remove the function name from the EMSCRIPTEN_GENERATED_FUNCTIONS array that appears as metadata in a comment at the bottom of the file. Note that if you add a function, things will work fine as only functions in that list are optimized - the only situation that is potentially problematic is replacing a generated function, because its name will be in that list.
  • You can see the transformed code as one of the passes that are saved when using EMCC_DEBUG=1, see above.

How to optimize code

The following procedure is how you should normally optimize your code:

  • First, run emcc on your code without optimization. The default settings are set to be safe. Check that your code works (if not, see the Debugging page) before continuing.
  • Build with -O2. That uses only safe optimizations, and should give good speed in most cases. If this is fast enough for you, you can stop here. If you must have all possible speed, continue.
  • Optionally, try some additional optimizations (all these are options you pass to emcc with -s OPTION=VALUE syntax):
  • INLINING_LIMIT: When set to 0, full inlining and LTO are done. We have found some LLVM LTO bugs, they have been fixed, but this should be considered less stable than the rest of LLVM. Another issue with full inlining is code size can increase. But the speedup might be worth it.
  • DOUBLE_MODE, PRECISE_I64_MATH: Settings these to 0 implements doubles and i64s in simpler, faster ways. These do not always work, though, so this is dangerous. (The best thing is to remove doubles and i64s from your code, if possible.)

(Note that -O3 contains those final additional optimizations, plus some other unsafe things. Generally speaking you should not try -O3 as it will most likely not work. Instead, use -O2, perhaps with additional tweaking as just mentioned.)

Advanced Optimization Issues

These sections regard some more complex optimizations.

Closure Compiler

Closure compiler minifies and optimizes code in very useful ways. It is run by default in -O2 and above. You can set it to run or not be run by calling emcc with --closure 1 or --closure 0.

Typed Arrays

Typed arrays in JavaScript can be much faster than untyped arrays. However this does not always lead to faster code, so you should check if it does or not. There are also two different typed array modes, see Code Generation Modes.

The default is typed arrays mode 2 (C-like, shared buffers). Mode 1 can be faster in some cases and slower in others - it's worth trying both modes.

Memory Compression

QUANTUM_SIZE of 1 can speed up your code, but is dangerous, see Code Generation Modes. The default is 4, which is the 'normal', safe value (even in -O3, because this optimization is really speculative).

Disabling LLVM Optimizations

In -O1 and above, LLVM optimizations are applied. However, in some cases they slow down the code, because they are tuned for optimizing native code, not JavaScript. It is worth trying your code without LLVM optimization as well, with --llvm-opts 0.

Decrease Emscripten Runtime Corrections

CORRECT_SIGNS, CORRECT_OVERFLOWS and CORRECT_ROUNDINGS are needed in some code. They add a lot of runtime overhead though. If you can, disable them entirely (by compiling with -s OPTION=0 etc., or by editing src/settings.js). Test your code carefully with those options disabled, because it is very possible it will no longer run properly.

If you can't disable them entirely, you can enable corrections for specific lines. Setting the CORRECT_* option to 2 (see the linespecific test for more) will correct only those lines.

To automatically find which lines need correction, you can use Emscripten's Profile Guided Optimization (PGO), described in the next section.

In -O3, no corrections are done, for maximum speed.

Profile Guided Optimization (PGO)

To optimize your code with PGO, you should do the following steps:

  • Compile your code with PGO=1, CHECK_SIGNS=1, CHECK_OVERFLOWS=1. The generated code is now instrumented to correct everything, and to take note of what needed correction.
  • Run your code on a representative workload. The code will run slowly because of the PGO instrumentation. PGO info will be written out when the program stops normally (if it doesn't stop normally, you can call CorrectionsMonitor.print() manually). Save the PGO output.
  • Recompile your code with something like
      pgo_data = read_pgo_data(pgo_filename)
      Settings.CORRECT_SIGNS = 2
      Settings.CORRECT_SIGNS_LINES = pgo_data['signs_lines']
      Settings.CORRECT_OVERFLOWS = 2
      Settings.CORRECT_OVERFLOWS_LINES = pgo_data['overflows_lines']

Here read_pgo_data is a utility function from tools/shared.py, using that we take the processed PGO output and use corrections in mode 2 (correct only the specified lines) on the right lines.

Notes:

  • You should compile your source code with -g to see the original source file and line numbers in the generated JavaScript.
  • Your code must be run on a representative workload. Corrections will only be done if they were seen to be needed, so if you later run on different input that uses different code paths, things may break.
  • There is no CHECK_ROUNDINGS, so PGO can't work on roundings corrections. This is rarely needed and has much less runtime overhead though, so just check if your code only works with CHECK_ROUNDINGS, and if so, build that way.
  • For the first step, you might need CHECK_SIGNED_OVERFLOWS=1 in rare cases and not just CHECK_OVERFLOWS=1.

(Note for manual tweaking of corrections - you can probably ignore this - you don't necessarily need to recompile each time. You can edit the generated source. unSign, for example, takes as a third parameter whether to ignore problems, so changing that to true will ignore signing on that line.)

Exception Catching

In -O1 and above exception catching is disabled. This prevents the generation of try-catch blocks, which lets the code run much faster. To re-enable them, run emcc with -s DISABLE_EXCEPTION_CATCHING=1.

Optimization Problems

  • If closure compiler hits an out-of-memory, try adjusting JAVA_HEAP_SIZE in the environment (for example, to 4096m for 4GB).