faster-cpython / ideas

1.67k stars 49 forks source link

Register instructions and ops in the instruction definition DSL #521

Closed gvanrossum closed 1 year ago

gvanrossum commented 1 year ago

It's pretty straightforward to add register instructions to the DSL. We can allow

register inst(NAME, (arg1, arg2 -- result)) {
    <code>
}

which generates code like this:

{
    PyObject *arg1 = REG(oparg1);
    PyObject *arg2 = REG(oparg2);
    PyObject *result;
    <code>
    REG(oparg3) = result;
}

There's an obvious restriction that there can be at most three I/O effects altogether (because there are only three opargs).

But what should we do for micro-ops? In the stack VM, the effect of two micro-ops is easily combined into the effect of a single instruction by simulating the stack effects in the generator: each input effect is a POP, each output effect is a PUSH. However, this doesn't work the same way for registers. E.g. if we had two register micro-ops,

register op(X, (arg1 -- res1)) { ... }
register op(Y, (arg2 --)) { ... }
macro(Z) = X + Y;

what should the register effect of Z be? I guess this could be (arg1, arg2 -- res1). But how can we pass info from one micro-op into the next? In the stack world the first op can PUSH something that the second op will POP, and since the PUSH and POP cancel each other out these are not reflected in the overall stack effect.

I can punt for now and only add register inst(...), so Irit can use the generator to define new register ops more easily, but this reduces the power of the story about combining micro-ops into macro-instructions, so I think eventually we will need an answer.

One possibility would be to use name correspondence, so that e.g.

register op(X, (arg1 -- temp1)) { ... }
register op(Y, (temp1 -- res1)) { ... }
macro(Z) = X + Y;

implies that Z has one register input (oparg1==arg1) and one register output (oparg2==res1) and one value that gets transferred between the micro-ops (temp1) -- in the current composition model the latter will just become a local variable that is never spilled to the register array. (In a world where we have a separate micro-op interpreter, we'd just have to spill it. This will be interesting because the bytecode->micro-code translator would have to allocate a register in this case.)

Note that in the register world we're probably not going to have super-instructions, so we won't have to think about those (but since this made me think of them, they would not have this problem because each component has its own three opargs).

Thoughts (@markshannon, @brandtbucher, @iritkatriel)?

gvanrossum commented 1 year ago

FWIW, Mark anticipated this when he wrote:

Stack-based instructions are much more easily composed than register-based ones.

iritkatriel commented 1 year ago

We probably need to see use cases for macro(). I don’t know if it makes sense to have any where the second instruction doesn’t consume the output of the first.

gvanrossum commented 1 year ago

There are some use cases for macro in the Examples section of the Instruction Definitions doc. There's this macro:

macro ( LOAD_ATTR_INSTANCE_VALUE ) =
        counter/1 + CHECK_OBJECT_TYPE + CHECK_HAS_INSTANCE_VALUES +
        LOAD_INSTANCE_VALUE + unused/4 ;

where the first two components "pass through" owner, which finally is consumed by LOAD_INSTANCE_VALUE:

    op ( CHECK_OBJECT_TYPE, (owner, type_version/2 -- owner) ) { ... }
    op ( CHECK_HAS_INSTANCE_VALUES, (owner -- owner) ) { ... }
    op ( LOAD_INSTANCE_VALUE, (owner, index/1 -- null if (oparg & 1), res) ) { ... }

I guess the generator should pay attention to name correspondence between input and output effects of an op as well as between the output effect of one op and the input of the next. In the stack world the convention is that if an input has the same name as an output (and they refer to the same stack entry) the instruction is not supposed to write to the output, we could keep that convention. But we still need to add the convention that name correspondence between different components means "use the same oparg/register".

iritkatriel commented 1 year ago

Should we close this? Should we remove register instructions from the generator for 3.12?

gvanrossum commented 1 year ago

I just ignore them for new features, so they probably don't work any more. Feel free to remove what's left of them, we will be able to use the git history to see what was there.

iritkatriel commented 1 year ago

Removed in https://github.com/python/cpython/pull/102739.