mirror of
				https://github.com/python/cpython.git
				synced 2025-10-25 10:44:55 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			150 lines
		
	
	
	
		
			5.4 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			150 lines
		
	
	
	
		
			5.4 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # The tier 2 execution engine
 | |
| 
 | |
| ## General idea
 | |
| 
 | |
| When execution in tier 1 becomes "hot", that is the counter for that point in
 | |
| the code reaches some threshold, we create an executor and execute that
 | |
| instead of the tier 1 bytecode.
 | |
| 
 | |
| Since each executor must exit, we also track the "hotness" of those
 | |
| exits and attach new executors to those exits.
 | |
| 
 | |
| As the program executes, and the hot parts of the program get optimized,
 | |
| a graph of executors forms.
 | |
| 
 | |
| ## Superblocks and Executors
 | |
| 
 | |
| Once a point in the code has become hot enough, we want to optimize it.
 | |
| Starting from that point we project the likely path of execution,
 | |
| using information gathered by tier 1 to guide that projection to
 | |
| form a "superblock", a mostly linear sequence of micro-ops.
 | |
| Although mostly linear, it may include a single loop.
 | |
| 
 | |
| We then optimize this superblock to form an optimized superblock,
 | |
| which is equivalent but more efficient.
 | |
| 
 | |
| A superblock is a representation of the code we want to execute,
 | |
| but it is not in executable form.
 | |
| The executable form is known as an executor.
 | |
| 
 | |
| Executors are semantically equivalent to the superblock they are
 | |
| created from, but are in a form that can be efficiently executable.
 | |
| 
 | |
| There are two execution engines for executors, and two types of executors:
 | |
| * The hardware which runs machine code executors created by the JIT compiler.
 | |
| * The tier 2 interpreter runs bytecode executors.
 | |
| 
 | |
| It would be very wasteful to support both a tier 2 interpreter and
 | |
| JIT compiler in the same process.
 | |
| For now, we will make the choice of engine a configuration option,
 | |
| but we could make it a command line option in the future if that would prove useful.
 | |
| 
 | |
| 
 | |
| ### Tier 2 Interpreter
 | |
| 
 | |
| For platforms without a JIT and for testing, we need an interpreter
 | |
| for executors. It is similar in design to the tier 1 interpreter, but has a
 | |
| different instruction set, and does not adapt.
 | |
| 
 | |
| ### JIT compiler
 | |
| 
 | |
| The JIT compiler converts superblocks into machine code executors.
 | |
| These have identical behavior to interpreted executors, except that
 | |
| they consume more memory for the generated machine code and are a lot faster.
 | |
| 
 | |
| ## Transferring control
 | |
| 
 | |
| There are three types of control transfer that we need to consider:
 | |
| * Tier 1 to tier 2
 | |
| * Tier 2 to tier 1
 | |
| * One executor to another within tier 2
 | |
| 
 | |
| Since we expect the graph of executors to span most of the hot
 | |
| part of the program, transfers from one executor to another should
 | |
| be the most common.
 | |
| Therefore, we want to make those transfers fast.
 | |
| 
 | |
| ### Tier 2 to tier 2
 | |
| 
 | |
| #### Cold exits
 | |
| 
 | |
| All side exits start cold and most stay cold, but a few become
 | |
| hot. We want to keep the memory consumption small for the many
 | |
| cold exits, but those that become hot need to be fast.
 | |
| However we cannot know in advance, which will be which.
 | |
| 
 | |
| So that tier 2 to tier 2 transfers are fast for hot exits,
 | |
| exits must be implemented as executors. In order to patch
 | |
| executor exits when they get hot, a pointer to the current
 | |
| executor must be passed to the exit executor.
 | |
| 
 | |
| #### Handling reference counts
 | |
| 
 | |
| There must be an implicit reference to the currently executing
 | |
| executor, otherwise it might be freed.
 | |
| Consequently, we must increment the reference count of an
 | |
| executor just before executing it, and decrement it just after
 | |
| executing it.
 | |
| 
 | |
| We want to minimize the amount of data that is passed from
 | |
| one executor to the next. In the JIT, this reduces the number
 | |
| of arguments in the tailcall, freeing up registers for other uses.
 | |
| It is less important in the interpreter, but following the same
 | |
| design as the JIT simplifies debugging and is good for performance.
 | |
| 
 | |
| Provided that we incref the new executor before executing it, we
 | |
| can jump directly to the code of the executor, without needing
 | |
| to pass a reference to that executor object.
 | |
| However, we do need a reference to the previous executor,
 | |
| so that it can be decref'd and for handling of cold exits.
 | |
| To avoid messing up the JIT's register allocation, we pass a
 | |
| reference to the previous executor in the thread state's
 | |
| `previous_executor` field.
 | |
| 
 | |
| #### The interpreter
 | |
| 
 | |
| The tier 2 interpreter has a variable `current_executor` which
 | |
| points to the currently live executor. When transferring from executor
 | |
| `A` to executor `B` we do the following:
 | |
| (Initially `current_executor` points to `A`, and the refcount of
 | |
| `A` is elevated by one)
 | |
| 
 | |
| 1. Set the instruction pointer to start at the beginning of `B`
 | |
| 2. Increment the reference count of `B`
 | |
| 3. Start executing `B`
 | |
| 
 | |
| We also make the first instruction in `B` do the following:
 | |
| 1. Set `current_executor` to point to `B`
 | |
| 2. Decrement the reference count of `A` (`A` is referenced by `tstate->previous_executor`)
 | |
| 
 | |
| The net effect of the above is to safely decrement the refcount of `A`,
 | |
| increment the refcount of `B` and set `current_executor` to point to `B`.
 | |
| 
 | |
| #### In the JIT
 | |
| 
 | |
| Transferring control from one executor to another is done via tailcalls.
 | |
| 
 | |
| The compiled executor should do the same, except that there is no local
 | |
| variable `current_executor`.
 | |
| 
 | |
| ### Tier 1 to tier 2
 | |
| 
 | |
| Since the executor doesn't know if the previous code was tier 1 or tier 2,
 | |
| we need to make a transfer from tier 1 to tier 2 look like a tier 2 to tier 2
 | |
| transfer to the executor.
 | |
| 
 | |
| We can then perform a tier 1 to tier 2 transfer by setting `current_executor`
 | |
| to `None`, and then performing a tier 2 to tier 2 transfer as above.
 | |
| 
 | |
| ### Tier 2 to tier 1
 | |
| 
 | |
| Each micro-op that might exit to tier 1 contains a `target` value,
 | |
| which is the offset of the tier 1 instruction to exit to in the
 | |
| current code object.
 | |
| 
 | |
| ## Counters
 | |
| 
 | |
| TO DO.
 | |
| The implementation will change soon, so there is no point in
 | |
| documenting it until then.
 | |
| 
 | 
