Assembly code.
authorrich <rich>
Wed, 10 Oct 2007 13:01:05 +0000 (13:01 +0000)
committerrich <rich>
Wed, 10 Oct 2007 13:01:05 +0000 (13:01 +0000)
Performance code (start of).

More documentation fixes / clarifications.

.cvsignore
Makefile
jonesforth.S
jonesforth.f
perf_dupdrop.c [new file with mode: 0644]
perf_dupdrop.f [new file with mode: 0644]

index c7227f0..e926f66 100644 (file)
@@ -1 +1,2 @@
-jonesforth
\ No newline at end of file
+jonesforth
+perf_dupdrop
\ No newline at end of file
index 78e6c72..f5a7494 100644 (file)
--- 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
 
index 081b537..c7309fd 100644 (file)
@@ -1,11 +1,11 @@
 /*     A sometimes minimal FORTH compiler and tutorial for Linux / i386 systems. -*- asm -*-
        By Richard W.M. Jones <rich@annexia.org> 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 ----------------------------------------------------------------------
 
        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.
 
            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.
 
        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
        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 <link to previous word>
        .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.
index 025d9b0..2d62e45 100644 (file)
@@ -2,7 +2,7 @@
 \      A sometimes minimal FORTH compiler and tutorial for Linux / i386 systems. -*- asm -*-
 \      By Richard W.M. Jones <rich@annexia.org> 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.
 \
 : 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 - ;
 
        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 '@').
 )
 : 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
                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 )
                        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 )
                SWAP            ( addr-linelen len-linelen )
        REPEAT
 
-       2DROP                   ( restore stack )
+       DROP                    ( restore stack )
        BASE !                  ( restore saved BASE )
 ;
 
        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.
        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.
 ;
 
 (
+       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 ----------------------------------------------------------------------
 
        DOES> isn't possible to implement with this FORTH because we don't have a separate
diff --git a/perf_dupdrop.c b/perf_dupdrop.c
new file mode 100644 (file)
index 0000000..a1f3786
--- /dev/null
@@ -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 <stdio.h>
+#include <stdlib.h>
+
+#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 (file)
index 0000000..8b0d911
--- /dev/null
@@ -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 $ )
+