Context
Principle
{, class="wikitable floatright" , + Different number of scalar registers in the most common architectures , - ! Architecture ! scope="col" , 32 bits ! scope="col" , 64 bits , - ! scope="row" , ARM , 15 , 31 , - ! scope="row" , Intel x86 , 8 , 16 , - ! scope="row" , MIPS , 32 , 32 , - ! scope="row" , POWER/PowerPC , 32 , 32 , - ! scope="row" , RISC-V , 16/32 , 32 , - ! scope="row" , SPARC , 31 , 31 , - In manymove
instruction whenever possible. This is especially important if the compiler is using an move
instructions.
Components of register allocation
Register allocation consists therefore of choosing where to store the variables at runtime, i.e. inside or outside registers. If the variable is to be stored in registers, then the allocator needs to determine in which register(s) this variable will be stored. Eventually, another challenge is to determine the duration for which a variable should stay at the same location. A register allocator, disregarding the chosen allocation strategy, can rely on a set of core actions to address these challenges. These actions can be gathered in several different categories: ;Move insertion: This action consists of increasing the number of move instructions between registers, i.e. make a variable live in different registers during its lifetime, instead of one. This occurs in the split live range approach. ;Spilling: This action consists of storing a variable into memory instead of registers. ;Assignment: This action consists of assigning a register to a variable. ;Coalescing: This action consists of limiting the number of moves between registers, thus limiting the total number of instructions. For instance, by identifying a variable live across different methods, and storing it into one register during its whole lifetime. Many register allocation approaches optimize for one or more specific categories of actions.Common problems raised in register allocation
Register allocation raises several problems that can be tackled (or avoided) by different register allocation approaches. Three of the most common problems are identified as follows: ;Aliasing: In some architectures, assigning a value to one register can affect the value of another: this is called aliasing. For example, theRegister allocation techniques
Register allocation can happen over aGraph-coloring allocation
Graph-coloring allocation is the predominant approach to solve register allocation. It was first proposed by Chaitin et al. In this approach, nodes in thePrinciple
The main phases in a Chaitin-style graph-coloring register allocator are: # Renumber: discover live range information in the source program. # Build: build the interference graph. # Coalesce: merge the live ranges of non-interfering variables related by copy instructions. # Spill cost: compute the spill cost of each variable. This assesses the impact of mapping a variable to memory on the speed of the final program. # Simplify: construct an ordering of the nodes in the inferences graph # Spill Code: insert spill instructions, i.e. loads and stores to commute values between registers and memory. # Select: assign a register to each variable.Drawbacks and further improvements
The graph-coloring allocation has three major drawbacks. First, it relies on graph-coloring, which is an NP-complete problem, to decide which variables are spilled. Finding a minimal coloring graph is indeed an NP-complete problem. Second, unless live-range splitting is used, evicted variables are spilled everywhere: store (respectively load) instructions are inserted as early (respectively late) as possible, i.e., just after (respectively before) variable definitions (respectively uses). Third, a variable that is not spilled is kept in the same register throughout its whole lifetime. On the other hand, a single register name may appear in multiple register classes, where a class is a set of register names that are interchangeable in a particular role. Then, multiple register names may be aliases for a single hardware register. Finally, graph coloring is an aggressive technique for allocating registers, but is computationally expensive due to its use of the interference graph, which can have a worst-case size that is quadratic in the number of live ranges. The traditional formulation of graph-coloring register allocation implicitly assumes a single bank of non-overlapping general-purpose registers and does not handle irregular architectural features like overlapping registers pairs, special purpose registers and multiple register banks. One later improvement of Chaitin-style graph-coloring approach was found by Briggs et al.: it is called conservative coalescing. This improvement adds a criterion to decide when two live ranges can be merged. Mainly, in addition to the non-interfering requirements, two variables can only be coalesced if their merging will not cause further spilling. Briggs et al. introduces a second improvement to Chaitin's works which is biased coloring. Biased coloring tries to assign the same color in the graph-coloring to live range that are copy related.Linear scan
Linear scan is another global register allocation approach. It was first proposed by Poletto et al. in 1999. In this approach, the code is not turned into a graph. Instead, all the variables are linearly scanned to determine their live range, represented as an interval. Once the live ranges of all variables have been figured out, the intervals are traversed chronologically. Although this traversal could help identifying variables whose live ranges interfere, no interference graph is being built and the variables are allocated in a greedy way. The motivation for this approach is speed; not in terms of execution time of the generated code, but in terms of time spent in code generation. Typically, the standard graph coloring approaches produce quality code, but have a significant overhead, the used graph coloring algorithm having a quadratic cost. Owing to this feature, linear scan is the approach currently used in several JIT compilers, like the Hotspot client compiler, V8 andPseudocode
This describes the algorithm as first proposed by Poletto et al., where: * R is the number of available registers. * active is the list, sorted in order of increasing end point, of live intervals overlapping the current point and placed in registers. LinearScanRegisterAllocation active ← {} for each live interval ''i'', in order of increasing start point do ExpireOldIntervals(i) if length(active) = R then SpillAtInterval(i) else register ← a register removed from pool of free registers add ''i'' to active, sorted by increasing end point ExpireOldIntervals(i) for each interval ''j'' in active, in order of increasing end point do if endpoint ≥ startpoint then return remove ''j'' from active add register to pool of free registers SpillAtInterval(i) spill ← last interval in active if endpointDrawbacks and further improvements
However, the linear scan presents two major drawbacks. First, due to its greedy aspect, it does not take lifetime holes into account, i.e. "ranges where the value of the variable is not needed". Besides, a spilled variable will stay spilled for its entire lifetime. Many other research works followed up on the Poletto's linear scan algorithm. Traub et al., for instance, proposed an algorithm called second-chance binpacking aiming at generating code of better quality. In this approach, spilled variables get the opportunity to be stored later in a register by using a differentRematerialization
The problem of optimal register allocation is NP-complete. As a consequence, compilers employ heuristic techniques to approximate its solution. Chaitin et al. discuss several ideas for improving the quality of spill code. They point out that certain values can be recomputed in a single instruction and that required operand will always be available for the computation. They call these exceptional values "never-killed" and note that such values should be recalculated instead of being spilled and reloaded. They further note that an uncoalesced copy of a never-killed value can be eliminated by recomputing it directly into the desired register. These techniques are termed rematerialization. In practice, opportunities for rematerialization include: * immediate loads of integer constants and, on some machines, floating-point constants, * computing a constant offset from the frame pointer or the static data area, and * loading non-local frame pointers from a display. Briggs et al. extend Chaitin's work to take advantage of rematerialization opportunities for complex, multi-valued live ranges. They found that each value needs to be tagged with enough information to allow the allocator to handle it correctly. Briggs's approach is the following: first, split each live range into its component values, then propagate rematerialization tags to each value, and form new live ranges from connected values having identical tags.Coalescing
In the context of register allocation, coalescing is the act of merging variable-to-variable move operations by allocating those two variables to the same location. The coalescing operation takes place after the interference graph is built. Once two nodes have been coalesced, they must get the same color and be allocated to the same register, once the copy operation becomes unnecessary. Doing coalescing might have both positive and negative impacts on the colorability of the interference graph. For example, one negative impact that coalescing could have on graph inference colorability is when two nodes are coalesced, as the result node will have a union of the edges of those being coalesced. A positive impact of coalescing on inference graph colorability is, for example, when a node interferes with both nodes being coalesced, the degree of the node is reduced by one which leads to improving the overall colorability of the interference graph. There are several coalescing heuristics available: ; Aggressive coalescing: it was first introduced by Chaitin original register allocator. This heuristic aims at coalescing any non-interfering, copy-related nodes. From the perspective of copy elimination, this heuristic has the best results. On the other hand, aggressive coalescing could impact the colorability of the inference graph. ;Conservative Coalescing: it mainly uses the same heuristic as aggressive coalescing but it merges moves if, and only if, it does not compromise the colorability of the interference graph. ;Iterated coalescing: it removes one particular move at the time, while keeping the colorability of the graph. ;Optimistic coalescing: it is based on aggressive coalescing, but if the inference graph colorability is compromised, then it gives up as few moves as possible.Mixed approaches
Hybrid allocation
Some other register allocation approaches do not limit to one technique to optimize register's use. Cavazos et al., for instance, proposed a solution where it is possible to use both the linear scan and the graph coloring algorithms. In this approach, the choice between one or the other solution is determined dynamically: first, aSplit allocation
Split allocation is another register allocation technique that combines different approaches, usually considered as opposite. For instance, the hybrid allocation technique can be considered as split because the first heuristic building stage is performed offline, and the heuristic use is performed online. In the same fashion, B. Diouf et al. proposed an allocation technique relying both on offline and online behaviors, namely static and dynamic compilation. During the offline stage, an optimal spill set is first gathered usingcompressAnnotation
algorithm which relies on the previously identified optimal spill set. Register allocation is performed afterwards during the online stage, based on the data collected in the offline phase.
In 2007, Bouchez et al. suggested as well to split the register allocation in different stages, having one stage dedicated to spilling, and one dedicated to coloring and coalescing.
Comparison between the different techniques
Several metrics have been used to assess the performance of one register allocation technique against the other. Register allocation has typically to deal with a trade-off between code quality, i.e. code that executes quickly, and analysis overhead, i.e. the time spent determining analyzing the source code to generate code with optimized register allocation. From this perspective, execution time of the generated code and time spent in liveness analysis are relevant metrics to compare the different techniques. Once relevant metrics have been chosen, the code on which the metrics will be applied should be available and relevant to the problem, either by reflecting the behavior of real-world application, or by being relevant to the particular problem the algorithm wants to address. The more recent articles about register allocation uses especially the Dacapo benchmark suite.See also
* Strahler number, the minimum number of registers needed to evaluate an expression tree. * Register (keyword), the hint in C and C++ for a variable to be placed in a register.References
Sources
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *External links