8 General Purpose Machines
Total Page:16
File Type:pdf, Size:1020Kb
DIGITAL SYSTEMS 63 8 General purpose machines Section 5 introduced the idea of a general purpose piece of hardware on which can be imposed a specific piece of software. Suppose we want to build a machine to evaluate an expression, like a + (b × c). We could build an adder for + and a multiplier for ×, and wire them together in a special purpose circuit, or we could try to build a programmable circuit. One strategy would be to build an FPGA-like circuit with lots of switches which could be configured like the special purpose circuit, and which would perform the evaluation in one cycle. An alternative would be to build a programmable circuit which can perform one (or more generally some constant number) of the operations in a single clock cycle, and to make it sequence through several steps to evaluate the expression. This machine needs some state to hold partial results from one cycle to the next. 8.1 Expression evaluation First of all, let us make precise what an expression is, and what it is to evaluate it. For a small example, take expressions with integer values, made up of named variables and integer numerals, combined by a small range of binary operations: type Value = Int type Name = String data Expr = Num Value | Var Name | Bin Expr Op Expr data Op = Add | Sub | Mul | Div The values of type Expr are things like Bin (Var “a”) Mul (Bin (Num 2) Add (Var “b”)) but for convenience we will assume a parsing function Main> parse "a*2+b" Bin (Var "a") Mul (Bin (Num 2) Add (Var "b")) (Notice that the parser does not try to get operator precedence right; you have to use parentheses.) An expression has a value once values have been assigned to all its free variables. Suppose there is a Store indexed by Names giving the value of variables: type Store = [(String, Value)] then a straightforward evaluator can assign a value to an expression by recursion on the structure of the expression 11:09pm 21st January 2021 GPM 64 8 GENERAL PURPOSE MACHINES eval :: Store → Expr → Value eval store (Num n) = n eval store (Var v) = store ‘at‘ v eval store (Bin l op r) = operate op (eval store l)(eval store r) operate Add a b = a + b operate Sub a b = a − b operate Mul a b = a × b operate Div a b = a ‘div‘ b We will build sequential machines whose correctness can be measured against the results of eval. 8.2 Stack evaluation One such machine has a stack of partial results. The idea is that we write a function compileS :: Expr → [Instr] which translates expressions data Expr = Num Value | Var String | Bin Expr Op Expr into a sequence of instructions data Instr = PushN Value | PushV String | Do Op data Op = Add | Sub | Mul | Div ... for a small machine exec :: Store → [Instr] → Value This factorisation of eval into a compiler which flattens the structure of the ex- pression, and a machine which executes the instructions is the essence of some portable implementations of languages like Java or Scala. The compiler trans- lates the source language into a bytecode, and a virtual machine interprets that bytecode.10 The compiler translates the expression tree into reverse Polish notation:11 10The first bytecode compiler was possibly Martin Richards’ BCPL compiler in the 1960s, which produced intermediate code for the O-machine. The O-code might be interpreted, or it might be translated into the native machine code of particular hardware. The translation into natice code might eb all-at-once, or it might be performed just in time as each bytecode instruction is executed. 11(Forward) Polish notation is attributed to JanLukasiewicz (1878–1956) in the 1920s, as a way of writing logical formulae without parentheses by putting the operators before the operands. RPN emerged in the 1950s and 1960s as an implementation technique. GPM 11:09pm 21st January 2021 8.2 Stack evaluation 65 compileS (Num n) = [PushN n] compileS (Var v) = [PushV v] compileS (Bin l op r) = compileS l ++ compileS r ++[Do op] What makes us happy to call the exec function a ‘machine’? It had better be finite state, and its next state had better be given by a function of its present state. Here is the next state function: type State = (Store, [Value]) step :: State → Instr → State step (store, stack)(PushN n) = (store, n : stack) step (store, stack)(PushV v) = (store, store ‘at‘ v : stack) step (store, b : a : stack)(Do op) = (store, operate op a b : stack) Executing a sequence of instructions will push a value onto the top of the stack: exec store prog = answer (foldl step (store, [ ]) prog) where answer (store, [v]) = v Correctness of this machine, that is eval store expr = exec store (compileS expr) can be proved by using the hypothesis foldl step (store, stack)(compileS expr) = (store, eval store expr : stack) in a proof by induction on the structure of expr. Exercises 8.1 Redefine eval and compileS as instances of the natural fold foldE :: (Value → a) → (String → a) → (a → Op → a → a) → Expr → a foldE num var bin = f where f (Num n) = num n f (Var v) = var v f (Bin l op r) = bin (f l) op (f r) for Expr. 8.2 Complete the proof that eval store expr = exec store (compileS expr) You may find it useful to use that foldl f e (xs ++ ys) = foldl f (foldl f e xs) ys 11:09pm 21st January 2021 GPM 66 8 GENERAL PURPOSE MACHINES 8.3 Modify the stack expression compiler to reduce the number of stack locations needed by evaluating the arguments of an operation in the right order. In the case of non-commutative operators you will also have to add instructions to the machine: either a Swap instruction which swaps the two values at the top of the stack, or reversed versions of each of the instructions corresponding to a non-commutative operator. What are the advantages and disadvantages of each of these two solutions? 8.3 Register machines The stack machine is not obviously finite state: however it will only use a finite amount of stack for any given (finite) expression. Moreover the pattern of access to the stack is predetermined, so it might be helpful to treat it as a list of registers, and work out which registers are accessed by each operation. This style of machine would have instructions data Instr = Load Reg Value | Move Reg Reg | Do Op Reg Reg Reg to load a constant, to copy one register into another, and to operate on the contents of two registers leaving the answer in a third (not all of which need to be distinct). The state of the state machine will be a mapping from registers to values type State = [(Reg, Value)] and the step function is straightforward step :: State → Instr → State step regs (Load d n) = update regs d n step regs (Move d s) = update regs d (regs ‘at‘ s) step regs (Do op d s t) = update regs d (operate op (regs ‘at‘ s)(regs ‘at‘ t) Rather than identifying elements of the store by name at run time, we will allocate registers to hold the values of the named variables. The mapping from names to addresses is usually called the environment type Env = [(String, Reg)] and exists only at compile time. The compiler allocates registers from a (finite) list of free registers in a way that corresponds to the locations on the stack from the previous compiler, and the result is left in the first of those free registers. GPM 11:09pm 21st January 2021 8.3 Register machines 67 compileR :: Env → [Reg] → Expr → [Instr] compileR env (r : free)(Num n) = [Load r n] compileR env (r : free)(Var v) = [Move r (env ‘at‘ v)] compileR env (r0 : r1 : free)(Bin l op r) = compileR env (r0 : r1 : free) l ++ compileR env (r1 : free) r ++ [Do op r0 r0 r1] The machine has to be modified to look for the answer in the right register. exec :: State → Reg → [Instr] → Value exec regs r prog = answer (foldl step regs prog) where answer regs = regs ‘at‘ r The initial state of the machine must be a mapping from registers to values which matches the original store: regs ‘at‘(env ‘at‘ v) = store ‘at‘ v For example Main> env [("a",10),("b",11),("c",12),("d",13)] Main> compileR env [0..9] (parse "(a+b)-(c*d)") [Move 0 10,Move 1 11,Do Add 0 0 1, Move 1 12,Move 2 13,Do Mul 1 1 2, Do Sub 0 0 1] Main> regs [(10,2),(11,4),(12,6),(13,8)] Main> exec regs 0 (compileR env [0..9] (parse "(a+b)-(c*d)")) -42 Code for the register machine can be improved by eliminating unnecessary moves from register to register, and the same sort of depth minimisation as for the stack machine. Here is code from a modified compiler, which minimises register use. This compiler returns (as well as the code) the identity of the register containing the answer, a list of unused registers, and the maximum number of registers used. Main> compileR’ env [0..5] (parse "(a+b)-(c*d)") (0,[Do Add 0 10 11,Do Mul 1 12 13,Do Sub 0 0 1],[1,2,3,4,5],2) Main> exec regs 0 [Do Add 0 10 11,Do Mul 1 12 13,Do Sub 0 0 1] -42 11:09pm 21st January 2021 GPM 68 8 GENERAL PURPOSE MACHINES The beauty of the register machine code is that it is easy to see how to construct a state machine to execute it.