{"id":13629176,"url":"https://github.com/spencertipping/jit-tutorial","last_synced_at":"2025-05-16T01:05:17.706Z","repository":{"id":46067614,"uuid":"84356596","full_name":"spencertipping/jit-tutorial","owner":"spencertipping","description":"How to write a very simple JIT compiler","archived":false,"fork":false,"pushed_at":"2021-05-03T20:33:50.000Z","size":25,"stargazers_count":1836,"open_issues_count":0,"forks_count":100,"subscribers_count":28,"default_branch":"master","last_synced_at":"2025-04-08T11:15:55.374Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spencertipping.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-03-08T19:09:33.000Z","updated_at":"2025-04-06T22:29:26.000Z","dependencies_parsed_at":"2022-08-12T12:40:28.892Z","dependency_job_id":null,"html_url":"https://github.com/spencertipping/jit-tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spencertipping%2Fjit-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spencertipping%2Fjit-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spencertipping%2Fjit-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spencertipping%2Fjit-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spencertipping","download_url":"https://codeload.github.com/spencertipping/jit-tutorial/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254448579,"owners_count":22072764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T22:01:03.860Z","updated_at":"2025-05-16T01:05:17.681Z","avatar_url":"https://github.com/spencertipping.png","language":"C","funding_links":[],"categories":["C"],"sub_categories":[],"readme":"# How to write a JIT compiler\nFirst up, you probably don't want to. JIT, or more accurately \"dynamic code\ngeneration,\" is typically not the most effective way to optimize a project, and\ncommon techniques end up trading away a lot of portability and require fairly\ndetailed knowledge about processor-level optimization.\n\nThat said, though, writing JIT compiler is a lot of fun and a great way to\nlearn stuff. The first thing to do is to write an interpreter.\n\n**NOTE:** If you don't have solid grasp of UNIX system-level programming, you\nmight want to read about [how to write a\nshell](https://github.com/spencertipping/shell-tutorial), which covers a lot of\nthe fundamentals.\n\n## MandelASM\nGPUs are fine for machine learning, but serious fractal enthusiasts design\ntheir own processors to generate Mandelbrot sets. And the first step in\nprocessor design, of course, is to write an emulator for it. Our emulator will\ninterpret the machine code we want to run and emit an image to stdout.\n\nTo keep it simple, our processor has four complex-valued registers called `a`,\n`b`, `c`, and `d`, and it supports three in-place operations:\n\n- `=ab`: assign register `a` to register `b`\n- `+ab`: add register `a` to register `b`\n- `*ab`: multiply register `b` by register `a`\n\nFor each pixel, the interpreter will zero all of the registers and then set `a`\nto the current pixel's coordinates. It then iterates the machine code for up to\n256 iterations waiting for register `b` to \"overflow\" (i.e. for its complex\nabsolute value to exceed 2). That means that the code for a standard Mandelbrot\nset is `*bb+ab`.\n\n### Simple interpreter\nThe first thing to do is write up a bare-bones interpreter in C. It would be\nsimpler to use `complex.h` here, but I'm going to write it in terms of\nindividual numbers because the JIT compiler will end up generating the longhand\nlogic. In production code we'd include bounds-checks and stuff, but I'm\nomitting those here for simplicity.\n\n```c\n// simple.c\n#include \u003cstdio.h\u003e\n#include \u003cstdlib.h\u003e\n\n#define sqr(x) ((x) * (x))\n\ntypedef struct { double r; double i; } complex;\n\nvoid interpret(complex *registers, char const *code) {\n  complex *src, *dst;\n  double r, i;\n  for (; *code; code += 3) {\n    dst = \u0026registers[code[2] - 'a'];\n    src = \u0026registers[code[1] - 'a'];\n    switch (*code) {\n      case '=':\n        dst-\u003er = src-\u003er;\n        dst-\u003ei = src-\u003ei;\n        break;\n      case '+':\n        dst-\u003er += src-\u003er;\n        dst-\u003ei += src-\u003ei;\n        break;\n      case '*':\n        r = dst-\u003er * src-\u003er - dst-\u003ei * src-\u003ei;\n        i = dst-\u003er * src-\u003ei + dst-\u003ei * src-\u003er;\n        dst-\u003er = r;\n        dst-\u003ei = i;\n        break;\n      default:\n        fprintf(stderr, \"undefined instruction %s (ASCII %x)\\n\", code, *code);\n        exit(1);\n    }\n  }\n}\n\nint main(int argc, char **argv) {\n  complex registers[4];\n  int i, x, y;\n  char line[1600];\n  printf(\"P5\\n%d %d\\n%d\\n\", 1600, 900, 255);\n  for (y = 0; y \u003c 900; ++y) {\n    for (x = 0; x \u003c 1600; ++x) {\n      registers[0].r = 2 * 1.6 * (x / 1600.0 - 0.5);\n      registers[0].i = 2 * 0.9 * (y /  900.0 - 0.5);\n      for (i = 1; i \u003c 4; ++i) registers[i].r = registers[i].i = 0;\n      for (i = 0; i \u003c 256 \u0026\u0026 sqr(registers[1].r) + sqr(registers[1].i) \u003c 4; ++i)\n        interpret(registers, argv[1]);\n      line[x] = i;\n    }\n    fwrite(line, 1, sizeof(line), stdout);\n  }\n  return 0;\n}\n```\n\nNow we can see the results by using `display` from ImageMagick\n(`apt-get install imagemagick`), or by saving to a file:\n\n```sh\n$ gcc simple.c -o simple\n$ ./simple *bb+ab | display -           # imagemagick version\n$ ./simple *bb+ab \u003e output.pgm          # save a grayscale PPM image\n$ time ./simple *bb+ab \u003e /dev/null      # quick benchmark\nreal\t0m2.369s\nuser\t0m2.364s\nsys\t0m0.000s\n$\n```\n\n![image](http://spencertipping.com/mandelbrot-output.png)\n\n### Performance analysis\n**In the real world, JIT is absolutely the wrong move for this problem.**\n\nArray languages like APL, Matlab, and to a large extent Perl, Python, etc,\nmanage to achieve reasonable performance by having interpreter operations that\napply over a large number of data elements at a time. We've got exactly that\nsituation here: in the real world it's a lot more practical to vectorize the\noperations to apply simultaneously to a screen-worth of data at a time -- then\nwe'd have nice options like offloading stuff to a GPU, etc.\n\nHowever, since the point here is to compile stuff, on we go.\n\nJIT can basically eliminate the interpreter overhead, which we can easily model\nhere by replacing `interpret()` with a hard-coded Mandelbrot calculation. This\nwill provide an upper bound on realistic JIT performance, since we're unlikely\nto optimize as well as `gcc` does.\n\n```c\n// hardcoded.c\n#include \u003cstdio.h\u003e\n#include \u003cstdlib.h\u003e\n\n#define sqr(x) ((x) * (x))\n\ntypedef struct { double r; double i; } complex;\n\nvoid interpret(complex *registers, char const *code) {\n  complex *a = \u0026registers[0];\n  complex *b = \u0026registers[1];\n  double r, i;\n  r = b-\u003er * b-\u003er - b-\u003ei * b-\u003ei;\n  i = b-\u003er * b-\u003ei + b-\u003ei * b-\u003er;\n  b-\u003er = r;\n  b-\u003ei = i;\n  b-\u003er += a-\u003er;\n  b-\u003ei += a-\u003ei;\n}\n\nint main(int argc, char **argv) {\n  complex registers[4];\n  int i, x, y;\n  char line[1600];\n  printf(\"P5\\n%d %d\\n%d\\n\", 1600, 900, 255);\n  for (y = 0; y \u003c 900; ++y) {\n    for (x = 0; x \u003c 1600; ++x) {\n      registers[0].r = 2 * 1.6 * (x / 1600.0 - 0.5);\n      registers[0].i = 2 * 0.9 * (y /  900.0 - 0.5);\n      for (i = 1; i \u003c 4; ++i) registers[i].r = registers[i].i = 0;\n      for (i = 0; i \u003c 256 \u0026\u0026 sqr(registers[1].r) + sqr(registers[1].i) \u003c 4; ++i)\n        interpret(registers, argv[1]);\n      line[x] = i;\n    }\n    fwrite(line, 1, sizeof(line), stdout);\n  }\n  return 0;\n}\n```\n\nThis version runs about twice as fast as the simple interpreter:\n\n```sh\n$ gcc hardcoded.c -o hardcoded\n$ time ./hardcoded *bb+ab \u003e /dev/null\nreal\t0m1.329s\nuser\t0m1.328s\nsys\t0m0.000s\n$\n```\n\n### JIT design and the x86-64 calling convention\nThe basic strategy is to replace `interpret(registers, code)` with a function\n`compile(code)` that returns a pointer to a function whose signature is this:\n`void compiled(registers*)`. The memory for the function needs to be allocated\nusing `mmap` so we can set permission for the processor to execute it.\n\nThe easiest way to start with something like this is probably to emit the\nassembly for `simple.c` to see how it works:\n\n```sh\n$ gcc -S simple.c\n```\n\nEdited/annotated highlights from the assembly `simple.s`, which is much more\ncomplicated than what we'll end up generating:\n\n```s\ninterpret:\n        // The stack contains local variables referenced to the \"base pointer\"\n        // stored in hardware register %rbp. Here's the layout:\n        //\n        //   double i  = -8(%rbp)\n        //   double r  = -16(%rbp)\n        //   src       = -24(%rbp)\n        //   dst       = -32(%rbp)\n        //   registers = -40(%rbp)      \u003c- comes in as an argument in %rdi\n        //   code      = -48(%rbp)      \u003c- comes in as an argument in %rsi\n\n        pushq   %rbp\n        movq    %rsp, %rbp              // standard x86-64 function header\n        subq    $48, %rsp               // allocate space for six local vars\n        movq    %rdi, -40(%rbp)         // registers arg -\u003e local var\n        movq    %rsi, -48(%rbp)         // code arg -\u003e local var\n        jmp     for_loop_condition      // commence loopage\n```\n\nBefore getting to the rest, I wanted to call out the `%rsi` and `%rdi` stuff\nand explain a bit about how calls work on x86-64. `%rsi` and `%rdi` seem\narbitrary, which they are to some extent -- C obeys a platform-specific calling\nconvention that specifies how arguments get passed in. On x86-64, up to six\narguments come in as registers; after that they get pushed onto the stack. If\nyou're returning a value, it goes into `%rax`.\n\nThe return address is automatically pushed onto the stack by `call`\ninstructions like `e8 \u003c32-bit relative\u003e`. So internally, `call` is the same as\n`push ADDRESS; jmp \u003ccall-site\u003e; ADDRESS: ...`. `ret` is the same as `pop %rip`,\nexcept that you can't pop into `%rip`. This means that the return address is\nalways the most immediate value on the stack.\n\nPart of the calling convention also requires callees to save a couple of\nregisters and use `%rbp` to be a copy of `%rsp` at function-call-time, but our\nJIT can mostly ignore this stuff because it doesn't call back into C.\n\n```s\nfor_loop_body:\n        // (a bunch of stuff to set up *src and *dst)\n\n        cmpl    $43, %eax               // case '+'\n        je      add_branch\n        cmpl    $61, %eax               // case '='\n        je      assign_branch\n        cmpl    $42, %eax               // case '*'\n        je      mult_branch\n        jmp     switch_default          // default\n\nassign_branch:\n        // the \"bunch of stuff\" above calculated *src and *dst, which are\n        // stored in -24(%rbp) and -32(%rbp).\n        movq    -24(%rbp), %rax         // %rax = src\n        movsd   (%rax), %xmm0           // %xmm0 = src.r\n        movq    -32(%rbp), %rax         // %rax = dst\n        movsd   %xmm0, (%rax)           // dst.r = %xmm0\n\n        movq    -24(%rbp), %rax         // %rax = src\n        movsd   8(%rax), %xmm0          // %xmm0 = src.i\n        movq    -32(%rbp), %rax         // %rax = dst\n        movsd   %xmm0, 8(%rax)          // dst.i = %xmm0\n\n        jmp     for_loop_step\n\nadd_branch:\n        movq    -32(%rbp), %rax         // %rax = dst\n        movsd   (%rax), %xmm1           // %xmm1 = dst.r\n        movq    -24(%rbp), %rax         // %rax = src\n        movsd   (%rax), %xmm0           // %xmm0 = src.r\n        addsd   %xmm1, %xmm0            // %xmm0 += %xmm1\n        movq    -32(%rbp), %rax         // %rax = dst\n        movsd   %xmm0, (%rax)           // dst.r = %xmm0\n\n        movq    -32(%rbp), %rax         // same thing for src.i and dst.i\n        movsd   8(%rax), %xmm1\n        movq    -24(%rbp), %rax\n        movsd   8(%rax), %xmm0\n        addsd   %xmm1, %xmm0\n        movq    -32(%rbp), %rax\n        movsd   %xmm0, 8(%rax)\n\n        jmp     for_loop_step\n\nmult_branch:\n        movq    -32(%rbp), %rax\n        movsd   (%rax), %xmm1\n        movq    -24(%rbp), %rax\n        movsd   (%rax), %xmm0\n        mulsd   %xmm1, %xmm0\n        movq    -32(%rbp), %rax\n        movsd   8(%rax), %xmm2\n        movq    -24(%rbp), %rax\n        movsd   8(%rax), %xmm1\n        mulsd   %xmm2, %xmm1\n        subsd   %xmm1, %xmm0\n        movsd   %xmm0, -16(%rbp)        // double r = src.r*dst.r - src.i*dst.i\n\n        movq    -32(%rbp), %rax\n        movsd   (%rax), %xmm1\n        movq    -24(%rbp), %rax\n        movsd   8(%rax), %xmm0\n        mulsd   %xmm0, %xmm1\n        movq    -32(%rbp), %rax\n        movsd   8(%rax), %xmm2\n        movq    -24(%rbp), %rax\n        movsd   (%rax), %xmm0\n        mulsd   %xmm2, %xmm0\n        addsd   %xmm1, %xmm0\n        movsd   %xmm0, -8(%rbp)         // double i = src.r*dst.i + src.i*dst.r\n\n        movq    -32(%rbp), %rax\n        movsd   -16(%rbp), %xmm0\n        movsd   %xmm0, (%rax)           // dst.r = r\n        movq    -32(%rbp), %rax\n        movsd   -8(%rbp), %xmm0\n        movsd   %xmm0, 8(%rax)          // dst.i = i\n        jmp     for_loop_step\n\nfor_loop_step:\n        addq    $3, -48(%rbp)\n\nfor_loop_condition:\n        movq    -48(%rbp), %rax         // %rax = code (the pointer)\n        movzbl  (%rax), %eax            // %eax = *code (move one byte)\n        testb   %al, %al                // is %eax 0?\n        jne     for_loop_body           // if no, then continue\n\n        leave                           // otherwise rewind stack\n        ret                             // pop and jmp\n```\n\n#### Compilation strategy\nMost of the above is register-shuffling fluff that we can get rid of. We're\ncompiling the code up front, which means all of our register addresses are\nknown quantities and we won't need any unknown indirection at runtime. So all\nof the shuffling into and out of `%rax` can be replaced by a much simpler move\ndirectly to or from `N(%rdi)` -- since `%rdi` is the argument that points to\nthe first register's real component.\n\nIf you haven't already, at this point I'd recommend downloading the [Intel\nsoftware developer's\nmanual](https://software.intel.com/en-us/articles/intel-sdm), of which volume 2\ndescribes the semantics and machine code representation of every instruction.\n\n**NOTE:** GCC uses AT\u0026T assembly syntax, whereas the Intel manuals use Intel\nassembly syntax. An important difference is that AT\u0026T reverses the arguments:\n`mov %rax, %rbx` (AT\u0026T syntax) assigns to `%rbx`, whereas `mov rax, rbx` (Intel\nsyntax) assigns to `rax`. All of my code examples use AT\u0026T, and none of this\nwill matter once we're working with machine code.\n\n##### Example: the Mandelbrot function `*bb+ab`\n```s\n// Step 1: multiply register B by itself\nmovsd 16(%rdi), %xmm0                   // %xmm0 = b.r\nmovsd 24(%rdi), %xmm1                   // %xmm1 = b.i\nmovsd 16(%rdi), %xmm2                   // %xmm2 = b.r\nmovsd 24(%rdi), %xmm3                   // %xmm3 = b.i\nmovsd %xmm0, %xmm4                      // %xmm4 = b.r\nmulsd %xmm2, %xmm4                      // %xmm4 = b.r*b.r\nmovsd %xmm1, %xmm5                      // %xmm5 = b.i\nmulsd %xmm3, %xmm5                      // %xmm5 = b.i*b.i\nsubsd %xmm5, %xmm4                      // %xmm4 = b.r*b.r - b.i*b.i\nmovsd %xmm4, 16(%rdi)                   // b.r = %xmm4\n\nmulsd %xmm0, %xmm3                      // %xmm3 = b.r*b.i\nmulsd %xmm1, %xmm2                      // %xmm2 = b.i*b.r\naddsd %xmm3, %xmm2                      // %xmm2 = b.r*b.i + b.i*b.r\nmovsd %xmm2, 24(%rdi)                   // b.i = %xmm2\n\n// Step 2: add register A to register B\nmovpd (%rdi), %xmm0                     // %xmm0 = (a.r, a.i)\naddpd %xmm0, 16(%rdi)                   // %xmm0 += (b.r, b.i)\nmovpd %xmm0, 16(%rdi)                   // (b.r, b.i) = %xmm0\n```\n\nThe multiplication code isn't optimized for the squaring-a-register use case;\ninstead, I left it fully general so we can use it as a template when we start\ngenerating machine code.\n\n### JIT mechanics\nBefore we compile a real language, let's just get a basic code generator\nworking.\n\n```c\n// jitproto.c\n#include \u003cstdio.h\u003e\n#include \u003cstdlib.h\u003e\n#include \u003csys/mman.h\u003e\n\ntypedef long(*fn)(long);\n\nfn compile_identity(void) {\n  // Allocate some memory and set its permissions correctly. In particular, we\n  // need PROT_EXEC (which isn't normally enabled for data memory, e.g. from\n  // malloc()), which tells the processor it's ok to execute it as machine\n  // code.\n  char *memory = mmap(NULL,             // address\n                      4096,             // size\n                      PROT_READ | PROT_WRITE | PROT_EXEC,\n                      MAP_PRIVATE | MAP_ANONYMOUS,\n                      -1,               // fd (not used here)\n                      0);               // offset (not used here)\n  if (memory == MAP_FAILED) {\n    perror(\"failed to allocate memory\");\n    exit(1);\n  }\n\n  int i = 0;\n\n  // mov %rdi, %rax\n  memory[i++] = 0x48;           // REX.W prefix\n  memory[i++] = 0x8b;           // MOV opcode, register/register\n  memory[i++] = 0xc7;           // MOD/RM byte for %rdi -\u003e %rax\n\n  // ret\n  memory[i++] = 0xc3;           // RET opcode\n\n  return (fn) memory;\n}\n\nint main() {\n  fn f = compile_identity();\n  int i;\n  for (i = 0; i \u003c 10; ++i)\n    printf(\"f(%d) = %ld\\n\", i, (*f)(i));\n  munmap(f, 4096);\n  return 0;\n}\n```\n\nThis does what we expect: we've just produced an identity function.\n\n```sh\n$ gcc jitproto.c -o jitproto\n$ ./jitproto\nf(0) = 0\nf(1) = 1\nf(2) = 2\nf(3) = 3\nf(4) = 4\nf(5) = 5\nf(6) = 6\nf(7) = 7\nf(8) = 8\nf(9) = 9\n```\n\n**TODO:** explanation about userspace page mapping/permissions, and how ELF\ninstructions tie into this (maybe also explain stuff like the FD table while\nwe're at it)\n\n#### Generating MandelASM machine code\nThis is where we start to get some serious mileage out of the Intel manuals. We\nneed encodings for the following instructions:\n\n- `f2 0f 11`: `movsd reg -\u003e memory`\n- `f2 0f 10`: `movsd memory -\u003e reg`\n- `f2 0f 59`: `mulsd reg -\u003e reg`\n- `f2 0f 58`: `addsd reg -\u003e reg`\n- `f2 0f 5c`: `subsd reg -\u003e reg`\n- `66 0f 11`: `movpd reg -\u003e memory` (technically `movupd` for unaligned move)\n- `66 0f 10`: `movpd memory -\u003e reg`\n- `66 0f 58`: `addpd memory -\u003e reg`\n\n##### The gnarly bits: how operands are specified\nChapter 2 of the Intel manual volume 2 contains a roundabout, confusing\ndescription of operand encoding, so I'll try to sum up the basics here.\n(**TODO**)\n\nFor the operators above, we've got two ModR/M configurations:\n\n- `movsd reg \u003c-\u003e X(%rdi)`: mod = 01, r/m = 111, disp8 = X\n- `addsd reg -\u003e reg`: mod = 11\n\nAt the byte level, they're written like this:\n\n```\nmovsd %xmm0, 16(%rdi)           # f2 0f 11 47 10\n  # modr/m = b01 000 111 = 47\n  # disp   = 16          = 10\n\naddsd %xmm3, %xmm4              # f2 0f 58 e3\n  # modr/m = b11 100 011 = e3\n```\n\n##### A simple micro-assembler\n```h\n// micro-asm.h\n#include \u003cstdarg.h\u003e\ntypedef struct {\n  char *dest;\n} microasm;\n\n// this makes it more obvious what we're doing later on\n#define xmm(n) (n)\n\nvoid asm_write(microasm *a, int n, ...) {\n  va_list bytes;\n  int i;\n  va_start(bytes, n);\n  for (i = 0; i \u003c n; ++i) *(a-\u003edest++) = (char) va_arg(bytes, int);\n  va_end(bytes);\n}\n\nvoid movsd_reg_memory(microasm *a, char reg, char disp)\n{ asm_write(a, 5, 0xf2, 0x0f, 0x11, 0x47 | reg \u003c\u003c 3, disp); }\n\nvoid movsd_memory_reg(microasm *a, char disp, char reg)\n{ asm_write(a, 5, 0xf2, 0x0f, 0x10, 0x47 | reg \u003c\u003c 3, disp); }\n\nvoid movsd_reg_reg(microasm *a, char src, char dst)\n{ asm_write(a, 4, 0xf2, 0x0f, 0x11, 0xc0 | src \u003c\u003c 3 | dst); }\n\nvoid mulsd(microasm *a, char src, char dst)\n{ asm_write(a, 4, 0xf2, 0x0f, 0x59, 0xc0 | dst \u003c\u003c 3 | src); }\n\nvoid addsd(microasm *a, char src, char dst)\n{ asm_write(a, 4, 0xf2, 0x0f, 0x58, 0xc0 | dst \u003c\u003c 3 | src); }\n\nvoid subsd(microasm *a, char src, char dst)\n{ asm_write(a, 4, 0xf2, 0x0f, 0x5c, 0xc0 | dst \u003c\u003c 3 | src); }\n\nvoid movpd_reg_memory(microasm *a, char reg, char disp)\n{ asm_write(a, 5, 0x66, 0x0f, 0x11, 0x47 | reg \u003c\u003c 3, disp); }\n\nvoid movpd_memory_reg(microasm *a, char disp, char reg)\n{ asm_write(a, 5, 0x66, 0x0f, 0x10, 0x47 | reg \u003c\u003c 3, disp); }\n\nvoid addpd_memory_reg(microasm *a, char disp, char reg)\n{ asm_write(a, 5, 0x66, 0x0f, 0x58, 0x47 | reg \u003c\u003c 3, disp); }\n```\n\n##### Putting it all together\nNow that we can write assembly-level stuff, we can take the structure from the\nprototype JIT compiler and modify it to compile MandelASM.\n\n```c\n// mandeljit.c\n#include \u003cstdio.h\u003e\n#include \u003cstdlib.h\u003e\n#include \u003csys/mman.h\u003e\n\n#include \"micro-asm.h\"\n\n#define sqr(x) ((x) * (x))\n\ntypedef struct { double r; double i; } complex;\ntypedef void(*compiled)(complex*);\n\n#define offsetof(type, field) ((unsigned long) \u0026(((type *) 0)-\u003efield))\n\ncompiled compile(char *code) {\n  char *memory = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC,\n                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n  microasm a = { .dest = memory };\n  char src_dsp, dst_dsp;\n  char const r = offsetof(complex, r);\n  char const i = offsetof(complex, i);\n\n  for (; *code; code += 3) {\n    src_dsp = sizeof(complex) * (code[1] - 'a');\n    dst_dsp = sizeof(complex) * (code[2] - 'a');\n    switch (*code) {\n      case '=':\n        movpd_memory_reg(\u0026a, src_dsp, xmm(0));\n        movpd_reg_memory(\u0026a, xmm(0), dst_dsp);\n        break;\n\n      case '+':\n        movpd_memory_reg(\u0026a, src_dsp, xmm(0));\n        addpd_memory_reg(\u0026a, dst_dsp, xmm(0));\n        movpd_reg_memory(\u0026a, xmm(0), dst_dsp);\n        break;\n\n      case '*':\n        movsd_memory_reg(\u0026a, src_dsp + r, xmm(0));\n        movsd_memory_reg(\u0026a, src_dsp + i, xmm(1));\n        movsd_memory_reg(\u0026a, dst_dsp + r, xmm(2));\n        movsd_memory_reg(\u0026a, dst_dsp + i, xmm(3));\n        movsd_reg_reg   (\u0026a, xmm(0), xmm(4));\n        mulsd           (\u0026a, xmm(2), xmm(4));\n        movsd_reg_reg   (\u0026a, xmm(1), xmm(5));\n        mulsd           (\u0026a, xmm(3), xmm(5));\n        subsd           (\u0026a, xmm(5), xmm(4));\n        movsd_reg_memory(\u0026a, xmm(4), dst_dsp + r);\n\n        mulsd           (\u0026a, xmm(0), xmm(3));\n        mulsd           (\u0026a, xmm(1), xmm(2));\n        addsd           (\u0026a, xmm(3), xmm(2));\n        movsd_reg_memory(\u0026a, xmm(2), dst_dsp + i);\n        break;\n\n      default:\n        fprintf(stderr, \"undefined instruction %s (ASCII %x)\\n\", code, *code);\n        exit(1);\n    }\n  }\n\n  // Return to caller (important! otherwise we'll segfault)\n  asm_write(\u0026a, 1, 0xc3);\n\n  return (compiled) memory;\n}\n\nint main(int argc, char **argv) {\n  compiled fn = compile(argv[1]);\n  complex registers[4];\n  int i, x, y;\n  char line[1600];\n  printf(\"P5\\n%d %d\\n%d\\n\", 1600, 900, 255);\n  for (y = 0; y \u003c 900; ++y) {\n    for (x = 0; x \u003c 1600; ++x) {\n      registers[0].r = 2 * 1.6 * (x / 1600.0 - 0.5);\n      registers[0].i = 2 * 0.9 * (y /  900.0 - 0.5);\n      for (i = 1; i \u003c 4; ++i) registers[i].r = registers[i].i = 0;\n      for (i = 0; i \u003c 256 \u0026\u0026 sqr(registers[1].r) + sqr(registers[1].i) \u003c 4; ++i)\n        (*fn)(registers);\n      line[x] = i;\n    }\n    fwrite(line, 1, sizeof(line), stdout);\n  }\n  return 0;\n}\n```\n\nNow let's benchmark the interpreted and JIT-compiled versions:\n\n```sh\n$ gcc mandeljit.c -o mandeljit\n$ time ./simple *bb+ab \u003e /dev/null\nreal\t0m2.348s\nuser\t0m2.344s\nsys\t0m0.000s\n$ time ./mandeljit *bb+ab \u003e /dev/null\nreal    0m1.462s\nuser    0m1.460s\nsys     0m0.000s\n```\n\nVery close to the limit performance of the hardcoded version. And, of course,\nthe JIT-compiled result is identical to the interpreted one:\n\n```sh\n$ ./simple *bb+ab | md5sum\n12a1013d55ee17998390809ffd671dbc  -\n$ ./mandeljit *bb+ab | md5sum\n12a1013d55ee17998390809ffd671dbc  -\n```\n\n## Further reading\n### Debugging JIT compilers\nFirst, you need a good scotch; this one should work.\n\n![image](https://cdn1.masterofmalt.com/whiskies/p-2813/laphroaig-quarter-cask-whisky.jpg?ss=2.0)\n\nOnce you've got that set up, `gdb` can probably be scripted to do what you\nneed. I've [used it somewhat\nsuccessfully](https://github.com/spencertipping/canard/blob/circular/bin/canard.debug.gdb)\nto debug a bunch of hand-written self-modifying machine code with no debugging\nsymbols -- the limitations of the approach ended up being whiskey-related\nrather than any deficiency of GDB itself.\n\nI've also had some luck using [radare2](http://www.radare.org/r/) to figure out\nwhen I was generating bogus instructions.\n\nOffline disassemblers like NASM and YASM won't help you.\n\n### Low-level\n- The Intel guides cover a lot of stuff we didn't end up using here: addressing\n  modes, instructions, etc. If you're serious about writing JIT compilers, it's\n  worth an in-depth read.\n\n- [Agner Fog's guides to processor-level\n  optimization](http://www.agner.org/optimize/): an insanely detailed tour\n  through processor internals, instruction parsing pipelines, and pretty much\n  every variant of every processor in existence.\n\n- [The V8 source\n  code](https://github.com/v8/v8/blob/master/src/codegen/x64/assembler-x64.h): how JIT\n  assemblers are actually written\n\n- [The JVM source\n  code](https://github.com/openjdk/jdk/tree/master/src/hotspot/)\n\n- [Jonesforth](http://git.annexia.org/?p=jonesforth.git;a=blob;f=jonesforth.S;h=45e6e854a5d2a4c3f26af264dfce56379d401425;hb=HEAD):\n  a well-documented example of low-level code generation and interpreter\n  structure (sort of a JIT alternative)\n\n- [Canard machine\n  code](https://github.com/spencertipping/canard/blob/circular/bin/canard.md#introduction):\n  similar to jonesforth, but uses machine code for its data structures\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspencertipping%2Fjit-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspencertipping%2Fjit-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspencertipping%2Fjit-tutorial/lists"}