I've started a refactoring of the IR instructions code. I implemented a function called GenericInstrMaker, which constructs a closure to be used as an instruction constructor. This is meant to be a quick way to create new "classes" for instructions that follow a generic pattern (eg: all arithmetic, comparison, bitwise, logical operators).
Erick and I have also been discussing introducing some x86 instruction classes into the IR for code generation and register allocation purposes, which made me think it would be nice to have an "X86Instr" class in our instruction class hierarchy. However, this somewhat breaks my current model, because, some x86 instructions will be branch instructions, but these can't have both X86Instr and BranchInstr as their prototype.
Hence, I've been thinking that perhaps, instead of having a BrancInstr class, all instructions should have an "isBranch" boolean flag. I was also thinking that instructions could have other flags, such as a "write" flag, and a "read" flag, to indicate that some instructions have memory access side effects. These side effect flags would be useful for writing an IR optimizer, later on.
Any comments on those ideas?
- Maxime
Afficher les réponses par date
On 2010-07-15, at 10:47 PM, Maxime Chevalier-Boisvert wrote:
I've started a refactoring of the IR instructions code. I implemented a function called GenericInstrMaker, which constructs a closure to be used as an instruction constructor. This is meant to be a quick way to create new "classes" for instructions that follow a generic pattern (eg: all arithmetic, comparison, bitwise, logical operators).
Erick and I have also been discussing introducing some x86 instruction classes into the IR for code generation and register allocation purposes, which made me think it would be nice to have an "X86Instr" class in our instruction class hierarchy. However, this somewhat breaks my current model, because, some x86 instructions will be branch instructions, but these can't have both X86Instr and BranchInstr as their prototype.
Hence, I've been thinking that perhaps, instead of having a BrancInstr class, all instructions should have an "isBranch" boolean flag. I was also thinking that instructions could have other flags, such as a "write" flag, and a "read" flag, to indicate that some instructions have memory access side effects. These side effect flags would be useful for writing an IR optimizer, later on.
I'm not clear on what you are proposing. Are you saying that the IR will contain X86 instructions and "normal" IR instructions at the same time? I'm assuming that in what follows.
At the last meeting I expressed my doubts that having real machine instructions in the IR was a good idea. Here are some more thoughts that lead me to think it is a bad idea:
1) The IR is meant to be a generic representation of the code, that expresses the meaning of the program and that makes program analysis and transformation easy. Obviously the X86 instructions are as low level as possible and specific to the X86 architecture (set of instructions, 2 operand instructions, etc). Mixing these levels is not "clean". It mixes two semantic levels where concepts from one level don't exist or are awkward at the other level. I fear the IR code will become tainted with X86-specific concerns, and that it will be extremely tedious to port the system to a different processor. If there is one important concept in computer-science and programming languages in particular, it is that the complexity of large software systems can be tackled by layering systems (in the case of programming languages, building a virtual machine abstraction). Our IR should be this virtual machine.
2) It is naïve to think that the best X86 instruction can be selected when other parts of the IR aren't yet converted to X86. For example, an integer multiplication can be done with at least 4 different instructions (add, asl, mul, lea). The best one to use will depend on which operands are to be multiplied (memory location, which register class, constant, etc). But the nature of an operand will depend on the instruction which generates its value. For example, consider the 2 IR instructions
(1) t1 := x + y (2) t2 := t1 + 1
The best x86 code to generate for the second IR instruction will depend on which CPU resources t1 and t2 map to. If t1 and t2 are mapped to the same register (because t1 becomes dead after IR instruction 2), say %eax, then the best x86 code will be
inc %eax
If on the other hand t1 and t2 map to 2 different registers, say %eax and %ebx, then the best x86 code will be
lea 1(%ebx),%eax
If on the other hand t1 is in memory, say stack slot 8(%ebp), and t2 is in a registers, say %eax, then the best x86 code will be
mov 8(%ebp),%eax inc %eax
I think it is better to maintain a high-level of abstraction for the IR instructions, and to have an independent pass which translates the IR instructions to x86 code. The x86 specific knowledge can be in this "x86 back-end". As we discussed in the last meeting, to keep things simple, we should use an on-the-fly register allocator to attribute registers/memory locations to the IR temporaries. On-the-fly register allocation requires that the IR instructions are translated to x86 in sequence.
What I recall from early discussions is that Maxime expects the compiler to iteratively modify/refine the IR representation of the program based on newly discovered properties of the program. The hope is that the parts of the IR that have been translated to x86 code do not have to be translated again. But imagine the program above where the second IR instruction has been translated to an "lea" x86 instruction, and now the monitoring system discovers that x and y are always equal to 0 so that t1 is also 0, then the best thing is to translate the second IR instruction into a "mov $1,...". In other words, the non-translated IR instruction needs to be available at all times.
So I fail to see what is gained by mixing the two levels of abstraction (IR and x86).
Marc
I'm not clear on what you are proposing. Are you saying that the IR will contain X86 instructions and "normal" IR instructions at the same time?
No. The point would be to translate the IR into these instructions by replacing the IR instructions in the CFG by x86 instructions, with the benefit of reusing facilities of the CFG/IR framework in this process. This will make it easier to work on the generated x86 code once it's been allocated... We want to perform instruction selection, peephole optimizations and register allocation *before* passing the x86 instructions to the assembler. The CFG/IR framework seems like it could support these things.
Mixing these levels is not "clean". It mixes two semantic levels where concepts from one level don't exist or are awkward at the other level. I fear the IR code will become tainted with X86-specific concerns, and that it will be extremely tedious to port the system to a different processor.
There will be no mixing. Just an HIR -> x86 translation for now, and eventually, hopefully, HIR -> MIR/LIR -> x86 for further added portability. The CFG containing x86 instructions will also be a separate entity from the original CFG.
For example, consider the 2 IR instructions
(1) t1 := x + y (2) t2 := t1 + 1
The best x86 code to generate for the second IR instruction will depend on which CPU resources t1 and t2 map to. If t1 and t2 are mapped to the same register (because t1 becomes dead after IR instruction 2), say %eax, then the best x86 code will be
inc %eax
If on the other hand t1 and t2 map to 2 different registers, say %eax and %ebx, then the best x86 code will be
lea 1(%ebx),%eax
If on the other hand t1 is in memory, say stack slot 8(%ebp), and t2 is in a registers, say %eax, then the best x86 code will be
mov 8(%ebp),%eax inc %eax
Erick and I discussed implementing a naive HIR -> x86 translator, and then having a linear scan register allocator and a peephole optimization pass to try and optimize the instruction choice through pattern matching and replacement. If the second approach works as well or better than the first, it may be advantageous, as it separates instruction selection from register allocation a little more.
I think it is better to maintain a high-level of abstraction for the IR instructions, and to have an independent pass which translates the IR instructions to x86 code. The x86 specific knowledge can be in this "x86 back-end". As we discussed in the last meeting, to keep things simple, we should use an on-the-fly register allocator to attribute registers/memory locations to the IR temporaries. On-the-fly register allocation requires that the IR instructions are translated to x86 in sequence.
That's pretty much exactly what Erick and I have in mind, although we aren't clear about whether it's best to translate to x86 while doing register allocation or translating first, doing register allocation (and inserting load/stores for spills), and then doing peephole optimizations.
The idea so far is:
HIR -> pseudo x86 (SSA/CFG) -> register allocation -> "true" x86 (x86 in CFG annotated with register/mem uses) -> peephole optimizer (working on CFG) -> assembler -> machine code bytes
You didn't give me feedback on whether you think it's a good idea to refactor the instructions to have branch and side effect flags, what do you think about that?
- Maxime
Marc, do you have any time to fix the parser issues we discussed before leaving? There are a few things I'd like to fix in the IR conversion that I'd can't really do until those fixes are applied.
Things to fix: - Identifier objects not the same for parameters vs locals that are parameters - Scope of var declarations inside catch - Function declarations inside catch resolve their variable to the outer containing function, not the catch scope (but function expressions behave as you would expect)
Feature requests (lesser priority): - conservative eval use detection - conservative arguments object use detection - function prologue annotations extraction
- Maxime