Open osa1 opened 1 year ago
I have been contemplating something similar. A few thoughts:
wasm_builder
. But it makes sense to have it in the CodeGenerator
.addLocal
, i.e. getTempLocal
/ freeTempLocal
. We would then continue to use addLocal
for locals that don't have temporary scope or need to be distinguishable in the debugger (e.g. source-level local variables).Constants
only emits locals for lazy constants, which is a rare case and is generated into small functions. So the optimization is probably not relevant for this particular case.Another example of a very short lived local is in ConstructorInvocation
compiler: https://github.com/dart-lang/sdk/blob/a464fc354beb0a2fee278612ccd7855071ede058/pkg/dart2wasm/lib/code_generator.dart#L1403-L1412
With this number of locals grow linearly with the constructor calls in the function. I actually see two locals introduced per constructor call, but I can't find where the other local is coming from.
This one when boxing a value creates a local to swap two values on stack: https://github.com/dart-lang/sdk/blob/a464fc354beb0a2fee278612ccd7855071ede058/pkg/dart2wasm/lib/translator.dart#L643-L646
co19 has some tests with huge list literals, for example the test ListBase_class_A01_t06 uses this huge list: https://github.com/dart-lang/co19/blob/97d07c01fdf6becf1b7e780858427d2b184df18d/LibTest/core/List/sort_A01_t06.test.dart#L30-L31
Each element in this list literal generates 2 locals, which in total generates more locals in this test than V8 supports.
One of each of these locals is for boxing, in the same swap code shown above. I can't find where the other local is generated.
Another example is computeRemainder
. This function has 10 variable declarations (including loop variables and function arguments) but the generated Wasm has 241 locals.
Functions like
CodeGenerator.makeList
andConstants.instantiateConstant
allocate locals that have very short lifetime. For example, here's a code thatinstantiateConstant
generates:$88
is generated byinstantiateConstant
and it has no uses after this tiny block.(In this particular case the local can be eliminated, but in general I think there will be a few instructions between the
local.set
andlocal.get
)binaryen can probably optimize this kind of trivial cases to use a shared local, but it will change a lot of other things as well which may make debugging more difficult.
I think we can easily reuse these short-lived locals and simplify generated code by maintaining a
Map<ValueType, Set<Local>>
state inDefinedFunction
for locals that are known to be not used again, and add locals to the map with a new methodvoid freeLocal(Local)
.addLocal
would then use one of the available locals from the map when possible.Functions like
instantiateConstant
andmakeList
would then free the locals they add when they are done. Relevant code inConstants
would look like:Seems like this could simplify generated code quite a bit with only one line added in a few places.
In cases where we think reusing a local will make debugging more difficult we can have a global parameter for disabling it, or add an optional argument to
addLocal
:addLocal(ValueType type, {bool forceFresh = false})
.