diff --git a/Makefile b/Makefile index 3c244eb..bcdc113 100644 --- a/Makefile +++ b/Makefile @@ -3,15 +3,15 @@ BUILDDIR=bin/ PREFIX=/usr/local/bin/ SOURCES=$(wildcard src/*.c) MAIN=main.c -override CFLAGS+=-std=c11 -O2 -Wno-gnu -lpthread -DEVFLAGS=-Werror -Wall -g -fPIC -DNDEBUG -Wfloat-equal -Wundef -Wwrite-strings -Wuninitialized -pedantic -fsanitize=address -O0 +override CFLAGS+=-std=c11 -O2 -g -Wno-gnu -lpthread +DEVFLAGS=-Werror -Wall -g -fPIC -DNDEBUG -Wfloat-equal -Wundef -Wwrite-strings -Wuninitialized -pedantic -O0 all: main.c mkdir -p $(BUILDDIR) $(CC) $(MAIN) $(SOURCES) -o $(BUILDDIR)$(TARGET) $(CFLAGS) no_wrap: main.c - CFLAGS+=-DNO_WRAP make + CFLAGS=-DNO_WRAP make dev: main.c mkdir -p $(BUILDDIR) diff --git a/README.md b/README.md index ac283a0..01aae1b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ actor that is effectively a Brainfuck program with added primitives for receiving from and sending to other actors. +This is the JIT version. Read [my write-up](https://blog.veitheller.de/Speeding_up_an_Interpreter.html) +about it. + ## Usage You can build the program through its Makefile by typing `make`. This will leave @@ -43,35 +46,21 @@ And that’s all that `cspfuck` is. I’m a terrible Brainfuck programmer, so I only provide one proof-of-concept example for now. It is contained in a directory named -`example/`—appropriately singular—, in which I provide a hello world program. -It is three actors, printing hello world concurrently. At the end of the -program, the first actor goes back to the cell containing `d`, and sends it -to the actor below, which will receive it into a new cell, print it, and print -a newline to make it pretty. +`example/`—appropriately singular—, in which I provide a ping pong program. +It contains two actors, one printing ping, then waiting for the other, the +other waiting, and then printing pong, ten times. ## Implementation -Actors are implemented as pthreads. The virtual machine is a simple direct -threaded bytecode VM that offers 30,000 elements to each Brainfuck program. It -will wrap around if you go past the low or high threshold—this can be turned -off by passing `-DNO_WRAP` to the compiler or calling `make no_wrap`, as -disabling it might buy you a little bit of performance, depending on your -workfload. - -It should be reasonably performant, but who cares? I hope noone’s going to run -their MapReduce jobs on it. There are some low-hanging fruits for optimization, -like making the VM loop use direct threaded code, but I chose not to for now. -Feel free to hack on it you want to! I’m happy to help you get started. +Actors are implemented as pthreads. The virtual machine is a simple handrolled +JIT that offers 30,000 elements to each Brainfuck program. It will segfault if +you go past the low or high threshold. This version is about four times faster +than the bytecode VM provided in the [`master`](https://github.com/hellerve/cspfuck/tree/master) branch. -The VM does seem to execute [ridiculous programs](http://www.clifford.at/bfcpu/hanoi.html) -in standard Brainfuck pretty efficiently, which makes me unreasonably happy. - -It’s only about 400 lines of C, so it should be reasonably consumable. The +It’s only about 550 lines of C, so it should be reasonably consumable. The code isn’t necessarily pretty, but it seems to work well. It is not incredibly battle-tested, though. -If you want to know more, read [my blog post](http://blog.veitheller.de/Brainfuck_and_Actors.html)! - **Disclaimer**: I know approximately as much about concurrent programming in C as I know about writing production-grade Brainfuck. The system should be expected to be brittle. diff --git a/example/hello_world.bf b/example/hello_world.bf deleted file mode 100644 index 3a503a4..0000000 --- a/example/hello_world.bf +++ /dev/null @@ -1,5 +0,0 @@ -++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.<+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.>u.<. - -++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>. diff --git a/example/ping.bf b/example/ping.bf new file mode 100644 index 0000000..0667a0e --- /dev/null +++ b/example/ping.bf @@ -0,0 +1,5 @@ ++++++++++++++++++++++++++++++++++++[>++>++>++>++<<<<-]>++++++++++>+++>++++++++>+>++++++++++<<<<< +++++++++++[>.>.>.>.>.>vu<<<<<<-] + ++++++++++++++++++++++++++++++++++++[>++>++>++>++<<<<-]>++++++++++>+++++++++>++++++++>+>++++++++++<<<<< +++++++++++[>>>>>>u<<<<<<>.>.>.>.>.^<<<<<-] diff --git a/main.c b/main.c index 16a8cd0..f40d552 100644 --- a/main.c +++ b/main.c @@ -26,9 +26,11 @@ int main(int argc, char** argv) { fseek(f, 0, SEEK_SET); char *string = malloc(fsize + 1); - fread(string, fsize, 1, f); + int read = fread(string, 1, fsize, f); fclose(f); + if (read != fsize) return err("couldn’t read file %s.\n", argv[1]); + string[fsize] = 0; actors* ac = parse(string); diff --git a/src/debug.c b/src/debug.c index 70011d1..9203806 100644 --- a/src/debug.c +++ b/src/debug.c @@ -1,3 +1,4 @@ +#include #include #include @@ -86,3 +87,76 @@ void print_src(actors* ac) { printf("\n\n"); } } + +uint32_t calc_offset(uint32_t jump_frm, uint32_t jump_to) { + if (jump_to >= jump_frm) return jump_to - jump_frm; + else return ~((uint32_t)jump_frm-jump_to) + 1; +} + +void print_faux_assembly_bytecode(long offset, bytecode* code) { +#define print(s) (printf("0x%lx: %s\n", idx+offset, s)) +#define print_arg(s) (printf("0x%lx: %s $0x%x, (%%r13)\n", idx+offset, s, c.arg)) +#define print_arg_ref(s) (printf("0x%lx: %s $0x%x, %%r13b\n", idx+offset, s, c.arg)) +#define print_syscall(d) {\ + printf("0x%lx: movq 0x200000%d, %%rax\n", idx+offset, d);\ + idx+=7;\ + print("movq 0x1, %%rdi");\ + idx+=7;\ + print("movq %%r13, %%rsi");\ + idx+=3;\ + print("movq 0x1, %%rdx");\ + idx+=7;\ + print("syscall ");\ + idx+=2;\ +} + int i = 0; + long idx = 10; + size_t loop_stack[256]; + int loop_depth = 0; + printf("0x%lx: movabsq $random-addr, %%r13\n", offset); + while(1) { + bytecode c = code[i++]; + switch (c.code) { + case INC: print_arg("addb"); idx+=5;break; + case DEC: print_arg("subb"); idx+=5;break; + case FWD: print_arg_ref("addb"); idx+=4;break; + case BCK: print_arg_ref("subb"); idx+=4;break; + case PRN: print_syscall(4); break; + case ZERO: print("movb $0x0, (%r13)"); idx+=5;break; + case READ: print_syscall(3); break; + case STARTL: + print("cmpb $0x0, (%r13)"); + idx+=5; + printf("0x%lx: je 0x%lx\n", idx+offset, c.arg+offset); + loop_stack[loop_depth++] = idx; + idx+=6; + break; + case ENDL: { + size_t begin = loop_stack[--loop_depth]; + + print("cmpb $0x0, (%r13)"); + idx+=5; + + size_t jmp_frm = idx + 6; + size_t jmp_to = begin + 6; + uint32_t pcrel_offset = calc_offset(jmp_frm, jmp_to); + printf("0x%lx: jne 0x%x\n", idx+offset, pcrel_offset); + idx+=6; + break; + } + case HALT: + print("retq"); + return; + } + } +#undef print +#undef print_arg +#undef print_arg_ref +#undef print_syscall +} + +void print_faux_assembly(long offset, actors* ac) { + for (int i = 0; i < ac->num; i++) { + print_faux_assembly_bytecode(offset, ac->code[i]); + } +} diff --git a/src/debug.h b/src/debug.h index cd8963d..44e6a77 100644 --- a/src/debug.h +++ b/src/debug.h @@ -5,5 +5,6 @@ void print_actors(actors*); void print_src(actors*); +void print_faux_assembly(long, actors*); #endif diff --git a/src/eval.c b/src/eval.c index 40542c0..7a8c390 100644 --- a/src/eval.c +++ b/src/eval.c @@ -1,7 +1,12 @@ #define _DEFAULT_SOURCE +#include #include +#include #include +#include +#include #include +#include #include #include "eval.h" @@ -10,139 +15,256 @@ typedef struct { int num; - int* up; - int* up_written; - int* down; - int* down_written; + int* up_in; + int* up_written_in; + int* up_out; + int* up_written_out; + int* down_in; + int* down_written_in; + int* down_out; + int* down_written_out; bytecode* code; } actor_ctx; -/* - We’re using direct threading here. Basically, we avoid looping and case - dispatch by using a computed GOTO instead. This comes at the cost of using a - GNU extension, but I love those anyway. -*/ -void* eval(void* arg) { -#define DISPATCH() { c = ctx->code[i++]; goto *dispatch_table[c.code]; } +void recv(actor_ctx* ctx, int* cl) { + while ((!ctx->up_written_in || !(*ctx->up_written_in)) && + (!ctx->down_written_in || !(*ctx->down_written_in))) usleep(100); + if (ctx->up_written_in && *ctx->up_written_in) { + *cl = *ctx->up_in; + *ctx->up_written_in = 0; + } else if (ctx->down_written_in && *ctx->down_written_in) { + *cl = *ctx->down_in; + *ctx->down_written_in = 0; + } +} + +void send_down(actor_ctx* ctx, int* cl) { + if (!ctx->down_out) { + fprintf(stderr, + "Actor %d tried to write to non-existant down channel, ignoring.", + ctx->num + ); + return; + } + *ctx->down_out = *cl; + *ctx->down_written_out = 1; +} + +void send_up(actor_ctx* ctx, int* cl) { + if (!ctx->up_out) { + fprintf(stderr, + "Actor %d tried to write to non-existant up channel, ignoring.", + ctx->num + ); + return; + } + *ctx->up_out = *cl; + *ctx->up_written_out = 1; +} + +uint32_t relative_32bit_offset(uint32_t jump_frm, uint32_t jump_to) { + if (jump_to >= jump_frm) { + size_t diff = jump_to - jump_frm; + assert(diff < (1ull << 31)); + return diff; + } else { + size_t diff = jump_frm - jump_to; + assert(diff-1 < (1ull << 31)); + uint32_t diff_unsigned = (uint32_t)diff; + return ~diff_unsigned + 1; + } +} + +typedef void (*jit_fn)(void); + +void* jit(void* arg) { +#define add_at(x, i) {\ + sys[i] = x;\ +} +#define add(x) {\ + sys = realloc(sys, ++syslen);\ + sys[syslen-1] = x;\ +} +#define add32_at(x, i) {\ + add_at((x) & 0xff, i);\ + add_at(((x) >> 8) & 0xff, i+1);\ + add_at(((x) >> 16) & 0xff, i+2);\ + add_at(((x) >> 24) & 0xff, i+3);\ +} +#define add32(x) {\ + add((x) & 0xff);\ + add(((x) >> 8) & 0xff);\ + add(((x) >> 16) & 0xff);\ + add(((x) >> 24) & 0xff);\ +} +#define add64(x) {\ + add32((x) & 0xffffffff);\ + add32(((x) >> 32) & 0xffffffff);\ +} int i = 0; - unsigned int h = 0; - unsigned int t[TAPE_LEN]; + uint8_t t[TAPE_LEN]; actor_ctx* ctx = (actor_ctx*) arg; bytecode c; - static void* dispatch_table[] = { - &&do_zero, &&do_inc, &&do_dec, &&do_fwd, &&do_bck, &&do_prn, &&do_read, - &&do_startl, &&do_endl, &&do_send, &&do_recv, &&do_move_ptr, &&do_move_data, - &&do_halt - }; + size_t syslen = 0; + uint8_t* sys = NULL; + size_t loop_stack[2048]; + int loop_depth = 0; for (int idx = 0; idx < TAPE_LEN; idx++) t[idx] = 0; - DISPATCH(); -do_zero: t[h] = 0; DISPATCH(); -do_inc: t[h]+=c.arg; DISPATCH(); -do_dec: t[h]-=c.arg; DISPATCH(); -#ifdef NO_WRAP -do_fwd: h += c.arg; DISPATCH(); -do_bck: h -= c.arg; DISPATCH(); + add(0x49); + add(0xbd); + add64((uint64_t)&t); + + while (1) { + c = ctx->code[i++]; + switch (c.code) { + case INC: add(0x41); add(0x80); add(0x45); add(0x00); add(c.arg); break; + case DEC: add(0x41); add(0x80); add(0x6d); add(0x00); add(c.arg); break; + case ZERO: add(0x41); add(0xc6); add(0x45); add(0x00); add(0x00); break; + case FWD: add(0x49); add(0x83); add(0xc5); add(c.arg); break; + case BCK: add(0x49); add(0x83); add(0xed); add(c.arg); break; + case PRN: +#ifdef __APPLE__ + add(0x48); add(0xc7); add(0xc0); add(0x04); add(0x00); add(0x00); add(0x02); #else -// modulo would be prettier here, but slows the code down by A LOT; somehow -// the C compilers can’t optimize uncoditional modulos here -do_fwd: h += c.arg; if (h >= TAPE_LEN-1) h %=TAPE_LEN; DISPATCH(); -do_bck: h -= c.arg; if (h < 0) h %= TAPE_LEN; DISPATCH(); + add(0x48); add(0xc7); add(0xc0); add(0x01); add(0x00); add(0x00); add(0x00); #endif -do_prn: printf("%c", t[h]); DISPATCH(); -do_read: scanf("%c", (char*)&t[h]); DISPATCH(); -do_startl: if(!t[h]) i = c.arg; DISPATCH(); -do_endl: if(t[h]) i = c.arg; DISPATCH(); - -do_send: - if (c.arg) { - if (!ctx->up) { - fprintf(stderr, - "Actor %d tried to write to non-existant up channel, ignoring.", - ctx->num - ); - DISPATCH(); - } - *ctx->up = t[h]; - *ctx->up_written = 1; - DISPATCH(); - } else { - if (!ctx->down) { - fprintf(stderr, - "Actor %d tried to write to non-existant down channel, ignoring.", - ctx->num - ); - DISPATCH(); + add(0x48); add(0xc7); add(0xc7); add(0x01); add(0x00); add(0x00); add(0x00); + add(0x4c); add(0x89); add(0xee); + add(0x48); add(0xc7); add(0xc2); add(0x01); add(0x00); add(0x00); add(0x00); + add(0x0f); add(0x05); + break; + case READ: +#ifdef __APPLE__ + add(0x48); add(0xc7); add(0xc0); add(0x03); add(0x00); add(0x00); add(0x02); +#else + add(0x48); add(0xc7); add(0xc0); add(0x00); add(0x00); add(0x00); add(0x00); +#endif + add(0x48); add(0xc7); add(0xc7); add(0x00); add(0x00); add(0x00); add(0x00); + add(0x4c); add(0x89); add(0xee); + add(0x48); add(0xc7); add(0xc2); add(0x01); add(0x00); add(0x00); add(0x00); + add(0x0f); add(0x05); + break; + case STARTL: + add(0x41); add(0x80); add(0x7d); add(0x00); add(0x00); + + loop_stack[loop_depth++] = syslen; + add(0x0f); add(0x84); add32(0); + break; + case ENDL: { + size_t begin = loop_stack[--loop_depth]; + add(0x41); add(0x80); add(0x7d); add(0x00); add(0x00); + + size_t jmp_frm = syslen + 6; + size_t jmp_to = begin + 6; + uint32_t pcrel_offset = relative_32bit_offset(jmp_frm, jmp_to); + + add(0x0f); add(0x85); + add32(pcrel_offset); + + jmp_frm = begin + 6; + jmp_to = syslen; + pcrel_offset = relative_32bit_offset(jmp_frm, jmp_to); + add32_at(pcrel_offset, begin+2); + break; + } + case RECV: { + add(0x55); + add(0x4c); add(0x89); add(0xee); + add(0x48); add(0xbf); add64((uint64_t)ctx); + add(0x49); add(0xbe); add64((uint64_t)recv); + add(0x41); add(0xff); add(0xd6); + add(0x5d); + break; + } + case SEND: { + add(0x55); + add(0x48); add(0xbf); add64((uint64_t)ctx); + add(0x49); add(0xbe); add64((uint64_t)(c.arg ? send_up : send_down)); + add(0x41); add(0xff); add(0xd6); + add(0x5d); + break; + } + case HALT: + goto jit_start; + default: + break; } - *ctx->down = t[h]; - *ctx->down_written = 1; - DISPATCH(); } +jit_start: + add(0xc3); -do_recv: - while ((ctx->up_written && !(*ctx->up_written)) && - (ctx->down_written && !(*ctx->down_written))) usleep(100); - if (ctx->up_written && *ctx->up_written) { - t[h] = *ctx->up; - *ctx->up_written = 0; - } else if (ctx->down_written && *ctx->down_written) { - t[h] = *ctx->down; - *ctx->down_written = 0; - } - DISPATCH(); + void* mem = mmap(0, syslen, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + memcpy(mem, sys, syslen); + mprotect(mem, syslen, PROT_READ | PROT_EXEC); -do_move_ptr: while (t[h]) h+=c.arg; DISPATCH(); + jit_fn f = (jit_fn)mem; -do_move_data: - if (t[h]) { - t[h+c.arg] += t[h]; - t[h] = 0; - } - DISPATCH(); + f(); + + munmap(mem, syslen); + //free(sys); -do_halt: return NULL; -#undef DISPATCH +#undef add +#undef add32 +#undef add64 +#undef add32_at } void eval_actors(actors* ac) { int i; pthread_t ts[ac->num]; actor_ctx ctxs[ac->num]; - int* down = NULL; - int* down_written = NULL; + int* down_in = NULL; + int* down_written_in = NULL; + int* down_out = NULL; + int* down_written_out = NULL; for (i = 0; i < ac->num; i++) { actor_ctx ctx; ctx.num = i; - ctx.down = down; - ctx.down_written = down_written; + ctx.down_in = down_in; + ctx.down_written_in = down_written_in; + ctx.down_out = down_out; + ctx.down_written_out = down_written_out; if (i < ac->num-1) { - ctx.up = malloc(sizeof(int)); - ctx.up_written = malloc(sizeof(int)); - *ctx.up_written = 0; + ctx.up_in = malloc(sizeof(int)); + ctx.up_written_in = malloc(sizeof(int)); + *ctx.up_written_in = 0; + ctx.up_out = malloc(sizeof(int)); + ctx.up_written_out = malloc(sizeof(int)); + *ctx.up_written_out = 0; } else { - ctx.up = NULL; - ctx.up_written = NULL; + ctx.up_in = NULL; + ctx.up_written_in = NULL; + ctx.up_out = NULL; + ctx.up_written_out = NULL; } ctx.code = ac->code[i]; ctxs[i] = ctx; if(ac->num == 1) { // if we just have one thread, we eval it directly; buys us about 2% perf - eval(&ctx); - free(ctx.up); - free(ctx.up_written); + jit(&ctx); + free(ctx.up_in); + free(ctx.up_written_in); + free(ctx.up_out); + free(ctx.up_written_out); return; } else { - pthread_create(&ts[i], NULL, eval, &ctxs[i]); + pthread_create(&ts[i], NULL, jit, &ctxs[i]); } - down = ctx.up; - down_written = ctx.up_written; + down_in = ctx.up_out; + down_written_in = ctx.up_written_out; + down_out = ctx.up_in; + down_written_out = ctx.up_written_in; } for (i = 0; i < ac->num; i++) { pthread_join(ts[i], NULL); + } + for (i = 0; i < ac->num; i++) { actor_ctx ctx = ctxs[i]; - if (ctx.up) { free(ctx.up); free(ctx.up_written); } + if (ctx.up_in) { free(ctx.up_in); free(ctx.up_written_in); free(ctx.up_out); free(ctx.up_written_out); } } } diff --git a/src/parser.c b/src/parser.c index 7cf298b..ff0aaac 100644 --- a/src/parser.c +++ b/src/parser.c @@ -49,27 +49,6 @@ char* optimize_zero(char* inpt) { return inpt; } -int optimize_loop(bytecode* s, int begin, int end) { - bytecode rep = s[begin]; - if (end-begin == 2 && (rep.code == FWD || rep.code == BCK)) { - // [>] || [<] == MOVE_PTR - int narg = rep.code == FWD ? rep.arg : -rep.arg; - s[begin-1] = (bytecode){.code=MOVE_PTR, .arg=narg}; - return 2; - } else if (end-begin == 5 && rep.code == DEC && s[begin+2].code == INC && - rep.arg == s[begin+2].arg == 1 && s[begin+1].arg == s[begin+3].arg) { - // [->+<] || [-<+>] == MOVE_DATA - if (s[begin+1].code == FWD && s[begin+3].code == BCK) { - s[begin-1] = (bytecode){.code=MOVE_DATA, .arg=s[begin+1].arg}; - return 5; - } else if (s[begin+1].code == BCK && s[begin+3].code == FWD) { - s[begin-1] = (bytecode){.code=MOVE_DATA, .arg=-s[begin+1].arg}; - return 5; - } - } - return 0; -} - actors* parse(char* inpt) { #define build_op(c, a) {\ res = realloc(res, sizeof(bytecode)*(++idx));\ @@ -81,7 +60,6 @@ actors* parse(char* inpt) { } int idx = 0; int dup = 0; - char* str = inpt; int loop_stack[256]; int loop_depth = 0; actors* ac = malloc(sizeof(actors)); @@ -114,16 +92,6 @@ actors* parse(char* inpt) { } build_op(ENDL, matching_begin+1); - int removed = optimize_loop(res, matching_begin+1, idx); - idx -= removed; - dup += removed; - res = realloc(res, sizeof(bytecode)*idx); - if (!removed) { - res[matching_begin].arg = idx; - if (res[matching_begin].code != STARTL && idx < 30) { - printf("wat: %d %d\n", matching_begin, idx); - } - } break; } @@ -140,7 +108,6 @@ actors* parse(char* inpt) { res = NULL; } idx = 0; - str = inpt+1; } else if (*inpt == '\0') continue; break; }