From 465979550d58288f6bee28c49064d9c841a6f45f Mon Sep 17 00:00:00 2001 From: rich Date: Wed, 10 Oct 2007 13:01:05 +0000 Subject: [PATCH] Assembly code. Performance code (start of). More documentation fixes / clarifications. --- .cvsignore | 3 +- Makefile | 9 +++- jonesforth.S | 36 ++++++++------- jonesforth.f | 120 ++++++++++++++++++++++++++++++++++++------------- perf_dupdrop.c | 33 ++++++++++++++ perf_dupdrop.f | 5 +++ 6 files changed, 156 insertions(+), 50 deletions(-) create mode 100644 perf_dupdrop.c create mode 100644 perf_dupdrop.f diff --git a/.cvsignore b/.cvsignore index c7227f0..e926f66 100644 --- a/.cvsignore +++ b/.cvsignore @@ -1 +1,2 @@ -jonesforth \ No newline at end of file +jonesforth +perf_dupdrop \ No newline at end of file diff --git a/Makefile b/Makefile index 78e6c72..f5a7494 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# $Id: Makefile,v 1.6 2007-10-07 11:07:15 rich Exp $ +# $Id: Makefile,v 1.7 2007-10-10 13:01:05 rich Exp $ SHELL := /bin/bash @@ -13,6 +13,8 @@ run: clean: rm -f jonesforth *~ core .test_* +# Tests. + TESTS := $(patsubst %.f,%.test,$(wildcard test_*.f)) test check: $(TESTS) @@ -27,6 +29,11 @@ test_%.test: test_%.f jonesforth @rm -f .$@ @echo "ok" +# Performance. + +perf_dupdrop: perf_dupdrop.c + gcc -O3 -Wall -Werror -o $@ $< + .SUFFIXES: .f .test .PHONY: test check diff --git a/jonesforth.S b/jonesforth.S index 081b537..c7309fd 100644 --- a/jonesforth.S +++ b/jonesforth.S @@ -1,11 +1,11 @@ /* A sometimes minimal FORTH compiler and tutorial for Linux / i386 systems. -*- asm -*- By Richard W.M. Jones http://annexia.org/forth This is PUBLIC DOMAIN (see public domain release statement below). - $Id: jonesforth.S,v 1.42 2007-10-07 11:07:15 rich Exp $ + $Id: jonesforth.S,v 1.43 2007-10-10 13:01:05 rich Exp $ gcc -m32 -nostdlib -static -Wl,-Ttext,0 -Wl,--build-id=none -o jonesforth jonesforth.S */ - .set JONES_VERSION,42 + .set JONES_VERSION,43 /* INTRODUCTION ---------------------------------------------------------------------- @@ -102,9 +102,9 @@ Secondly make sure TABS are set to 8 characters. The following should be a vertical line. If not, sort out your tabs. - | - | - | + | + | + | Thirdly I assume that your screen is at least 50 characters high. @@ -151,7 +151,8 @@ mov 2,%eax reads the 32 bit word from address 2 into %eax (ie. most likely a mistake) (4) gas has a funky syntax for local labels, where '1f' (etc.) means label '1:' "forwards" - and '1b' (etc.) means label '1:' "backwards". + and '1b' (etc.) means label '1:' "backwards". Notice that these labels might be mistaken + for hex numbers (eg. you might confuse 1b with $0x1b). (5) 'ja' is "jump if above", 'jb' for "jump if below", 'je' "jump if equal" etc. @@ -269,8 +270,8 @@ caches than those early computers had in total, but the execution model still has some useful properties]. - Of course this code won't run directly any more. Instead we need to write an interpreter - which takes each pair of bytes and calls it. + Of course this code won't run directly on the CPU any more. Instead we need to write an + interpreter which takes each set of bytes and calls it. On an i386 machine it turns out that we can write this interpreter rather easily, in just two assembly instructions which turn into just 3 bytes of machine code. Let's store the @@ -455,10 +456,10 @@ Because we will need to restore the old %esi at the end of DOUBLE (this is, after all, like a function call), we will need a stack to store these "return addresses" (old values of %esi). - As you will have read, when reading the background documentation, FORTH has two stacks, - an ordinary stack for parameters, and a return stack which is a bit more mysterious. But - our return stack is just the stack I talked about in the previous paragraph, used to save - %esi when calling from a FORTH word into another FORTH word. + As you will have seen in the background documentation, FORTH has two stacks, an ordinary + stack for parameters, and a return stack which is a bit more mysterious. But our return + stack is just the stack I talked about in the previous paragraph, used to save %esi when + calling from a FORTH word into another FORTH word. In this FORTH, we are using the normal stack pointer (%esp) for the parameter stack. We will use the i386's "other" stack pointer (%ebp, usually called the "frame pointer") @@ -598,6 +599,7 @@ cold_start: // High-level code without a codeword. unsure of them). The long way would be: + .int .byte 6 // len .ascii "DOUBLE" // string @@ -661,6 +663,7 @@ name_\label : LINK in next word Again, for brevity in writing the header I'm going to write an assembler macro called defcode. + As with defword above, don't worry about the complicated details of the macro. */ .macro defcode name, namelen, flags=0, label @@ -783,7 +786,7 @@ code_\label : // assembler code follows NEXT /* - Lots of comparison operations. + Lots of comparison operations like =, <, >, etc.. ANS FORTH says that the comparison words should return all (binary) 1's for TRUE and all 0's for FALSE. However this is a bit of a strange convention @@ -1221,7 +1224,7 @@ var_\name : and compiling code, we might be reading words to execute, we might be asking for the user to type their name -- ultimately it all comes in through KEY. - The implementation of KEY uses an input buffer of a certain size (defined at the start of this + The implementation of KEY uses an input buffer of a certain size (defined at the end of this file). It calls the Linux read(2) system call to fill this buffer and tracks its position in the buffer using a couple of variables, and if it runs out of input buffer then it refills it automatically. The other thing that KEY does is if it detects that stdin has closed, it @@ -1238,7 +1241,6 @@ var_\name : currkey (next character to read) <---------------------- BUFFER_SIZE (4096 bytes) ----------------------> - */ defcode "KEY",3,,KEY @@ -1250,9 +1252,9 @@ _KEY: cmp (bufftop),%ebx jge 1f // exhausted the input buffer? xor %eax,%eax - mov (%ebx),%al + mov (%ebx),%al // get next key from input buffer inc %ebx - mov %ebx,(currkey) + mov %ebx,(currkey) // increment currkey ret 1: // Out of input; use read(2) to fetch more input from stdin. diff --git a/jonesforth.f b/jonesforth.f index 025d9b0..2d62e45 100644 --- a/jonesforth.f +++ b/jonesforth.f @@ -2,7 +2,7 @@ \ A sometimes minimal FORTH compiler and tutorial for Linux / i386 systems. -*- asm -*- \ By Richard W.M. Jones http://annexia.org/forth \ This is PUBLIC DOMAIN (see public domain release statement below). -\ $Id: jonesforth.f,v 1.13 2007-10-07 11:07:15 rich Exp $ +\ $Id: jonesforth.f,v 1.14 2007-10-10 13:01:05 rich Exp $ \ \ The first part of this tutorial is in jonesforth.S. Get if from http://annexia.org/forth \ @@ -24,9 +24,9 @@ \ Secondly make sure TABS are set to 8 characters. The following should be a vertical \ line. If not, sort out your tabs. \ -\ | -\ | -\ | +\ | +\ | +\ | \ \ Thirdly I assume that your screen is at least 50 characters high. \ @@ -65,10 +65,6 @@ : 2DUP OVER OVER ; : 2DROP DROP DROP ; -\ More standard FORTH words. -: 2* 2 * ; -: 2/ 2 / ; - \ NEGATE leaves the negative of a number on the stack. : NEGATE 0 SWAP - ; @@ -658,8 +654,9 @@ want a variable which is read often, and written infrequently. 20 VALUE VAL creates VAL with initial value 20 - VAL pushes the value directly on the stack + VAL pushes the value (20) directly on the stack 30 TO VAL updates VAL, setting it to 30 + VAL pushes the value (30) directly on the stack Notice that 'VAL' on its own doesn't return the address of the value, but the value itself, making values simpler and more obvious to use than variables (no indirection through '@'). @@ -833,10 +830,10 @@ ) : DUMP ( addr len -- ) BASE @ ROT ( save the current BASE at the bottom of the stack ) - HEX ( and switch the hexadecimal mode ) + HEX ( and switch to hexadecimal mode ) BEGIN - DUP 0> ( while len > 0 ) + ?DUP ( while len > 0 ) WHILE OVER 8 U.R ( print the address ) SPACE @@ -845,19 +842,19 @@ 2DUP ( addr len addr len ) 1- 15 AND 1+ ( addr len addr linelen ) BEGIN - DUP 0> ( while linelen > 0 ) + ?DUP ( while linelen > 0 ) WHILE SWAP ( addr len linelen addr ) DUP C@ ( addr len linelen addr byte ) 2 .R SPACE ( print the byte ) 1+ SWAP 1- ( addr len linelen addr -- addr len addr+1 linelen-1 ) REPEAT - 2DROP ( addr len ) + DROP ( addr len ) ( print the ASCII equivalents ) 2DUP 1- 15 AND 1+ ( addr len addr linelen ) BEGIN - DUP 0> ( while linelen > 0) + ?DUP ( while linelen > 0) WHILE SWAP ( addr len linelen addr ) DUP C@ ( addr len linelen addr byte ) @@ -868,7 +865,7 @@ THEN 1+ SWAP 1- ( addr len linelen addr -- addr len addr+1 linelen-1 ) REPEAT - 2DROP ( addr len ) + DROP ( addr len ) CR DUP 1- 15 AND 1+ ( addr len linelen ) @@ -880,7 +877,7 @@ SWAP ( addr-linelen len-linelen ) REPEAT - 2DROP ( restore stack ) + DROP ( restore stack ) BASE ! ( restore saved BASE ) ; @@ -891,13 +888,13 @@ agreed syntax for this, so I've gone for the syntax mandated by the ISO standard FORTH (ANS-FORTH). - ( some value on the stack ) - CASE - test1 OF ... ENDOF - test2 OF ... ENDOF - testn OF ... ENDOF - ... ( default case ) - ENDCASE + ( some value on the stack ) + CASE + test1 OF ... ENDOF + test2 OF ... ENDOF + testn OF ... ENDOF + ... ( default case ) + ENDCASE The CASE statement tests the value on the stack by comparing it for equality with test1, test2, ..., testn and executes the matching piece of code within OF ... ENDOF. @@ -912,14 +909,14 @@ An example (assuming that 'q', etc. are words which push the ASCII value of the letter on the stack): - 0 VALUE QUIT - 0 VALUE SLEEP - KEY CASE - 'q' OF 1 TO QUIT ENDOF - 's' OF 1 TO SLEEP ENDOF - ( default case: ) - ." Sorry, I didn't understand key <" DUP EMIT ." >, try again." CR - ENDCASE + 0 VALUE QUIT + 0 VALUE SLEEP + KEY CASE + 'q' OF 1 TO QUIT ENDOF + 's' OF 1 TO SLEEP ENDOF + ( default case: ) + ." Sorry, I didn't understand key <" DUP EMIT ." >, try again." CR + ENDCASE (In some versions of FORTH, more advanced tests are supported, such as ranges, etc. Other versions of FORTH need you to write OTHERWISE to indicate the default case. @@ -1630,6 +1627,67 @@ . CR ; +( + ASSEMBLER CODE ---------------------------------------------------------------------- + + This is just the outline of a simple assembler, allowing you to write FORTH primitives + in assembly language. + + Assembly primitives begin ': NAME' in the normal way, but are ended with ;CODE. ;CODE + updates the header so that the codeword isn't DOCOL, but points instead to the assembled + code (in the DFA part of the word). + + We provide a convenience macro NEXT (you guessed the rest). + + The rest consists of some immediate words which expand into machine code appended to the + definition of the word. Only a very tiny part of the i386 assembly space is covered, just + enough to write a few assembler primitives below. +) + +: ;CODE IMMEDIATE + ALIGN ( machine code is assembled in bytes so isn't necessarily aligned at the end ) + LATEST @ DUP + HIDDEN ( unhide the word ) + DUP >DFA SWAP >CFA ! ( change the codeword to point to the data area ) + [COMPILE] [ ( go back to immediate mode ) +; + +HEX + +( Equivalent to the NEXT macro ) +: NEXT IMMEDIATE AD C, FF C, 20 C, ; + +( The i386 registers ) +: EAX IMMEDIATE 0 ; +: ECX IMMEDIATE 1 ; +: EDX IMMEDIATE 2 ; +: EBX IMMEDIATE 3 ; +: ESP IMMEDIATE 4 ; +: EBP IMMEDIATE 5 ; +: ESI IMMEDIATE 6 ; +: EDI IMMEDIATE 7 ; + +( i386 stack instructions ) +: PUSH IMMEDIATE 50 + C, ; +: POP IMMEDIATE 58 + C, ; + +( RDTSC instruction ) +: RDTSC IMMEDIATE 0F C, 31 C, ; + +DECIMAL + +( + RDTSC is an assembler primitive which reads the Pentium timestamp counter (a very fine- + grained counter which counts processor clock cycles). Because the TSC is 64 bits wide + we have to push it onto the stack in two slots. +) +: RDTSC ( -- lsb msb ) + RDTSC ( writes the result in %edx:%eax ) + EAX PUSH ( push lsb ) + EDX PUSH ( push msb ) + NEXT +;CODE + ( NOTES ---------------------------------------------------------------------- diff --git a/perf_dupdrop.c b/perf_dupdrop.c new file mode 100644 index 0000000..a1f3786 --- /dev/null +++ b/perf_dupdrop.c @@ -0,0 +1,33 @@ +/* Ideal DUP DROP * 1000 assuming perfect inlining. + $Id: perf_dupdrop.c,v 1.1 2007-10-10 13:01:05 rich Exp $ +*/ + +#include +#include + +#define DUP \ + asm volatile ("mov (%%esp),%%eax\n" \ + "\tpush %%eax" \ + : : : "eax") +#define DROP \ + asm volatile ("pop %%eax" \ + : : : "eax") + +#define DUPDROP DUP; DROP; +#define DUPDROP10 DUPDROP DUPDROP DUPDROP DUPDROP DUPDROP DUPDROP DUPDROP DUPDROP DUPDROP DUPDROP +#define DUPDROP100 DUPDROP10 DUPDROP10 DUPDROP10 DUPDROP10 DUPDROP10 DUPDROP10 DUPDROP10 DUPDROP10 DUPDROP10 DUPDROP10 +#define DUPDROP1000 DUPDROP100 DUPDROP100 DUPDROP100 DUPDROP100 DUPDROP100 DUPDROP100 DUPDROP100 DUPDROP100 DUPDROP100 DUPDROP100 + +int +main (int argc, char *argv[]) +{ + unsigned long long start_time, end_time; + + asm volatile ("rdtsc" : "=A" (start_time)); + DUPDROP1000 + asm volatile ("rdtsc" : "=A" (end_time)); + + printf ("%llu\n", end_time - start_time); + + exit (0); +} diff --git a/perf_dupdrop.f b/perf_dupdrop.f new file mode 100644 index 0000000..8b0d911 --- /dev/null +++ b/perf_dupdrop.f @@ -0,0 +1,5 @@ +( -*- text -*- + FORTH repeated DUP DROP * 1000 using ordinary indirect threaded code + and the assembler primitives. + $Id: perf_dupdrop.f,v 1.1 2007-10-10 13:01:05 rich Exp $ ) + -- 2.39.2