Domanda

Sto sviluppando un linguaggio di scripting che viene compilato per la propria macchina virtuale, un semplice con istruzioni per lavorare con alcuni tipi di dati come punti , vettori , float e così via .. la cella di memoria è rappresentata in questo modo:

struct memory_cell
{
    u32 id;
    u8 type;

    union
    {
        u8 b; /* boolean */
        double f; /* float */
        struct { double x, y, z; } v; /* vector */
        struct { double r, g, b; } c; /* color */
        struct { double r, g, b; } cw; /* color weight */
        struct { double x, y, z; } p; /* point variable */
        struct { u16 length; memory_cell **cells; } l; /* list variable */
    };  
};

Le istruzioni sono generiche e sono in grado di funzionare su molti operandi diversi. Ad esempio

ADD dest, src1, src2

può funzionare con float, vettori, punti, colori impostando il giusto tipo di destinazione in base agli operandi.

Il ciclo di esecuzione principale controlla semplicemente il opcode dell'istruzione (che è una struttura contenente i sindacati per definire qualsiasi tipo di istruzione) ed esegue. Ho usato un approccio semplificato in cui non ho registri ma solo una vasta gamma di celle di memoria.

Mi chiedevo se JIT potesse aiutarmi a ottenere le migliori performance o meno e come raggiungerlo.

Come ho detto, la migliore implementazione raggiunta finora è qualcosa del genere:

 void VirtualMachine::executeInstruction(instr i)
 {
     u8 opcode = (i.opcode[0] & (u8)0xFC) >> 2;

     if (opcode >= 1 && opcode <= 17) /* RTL instruction */
     {
        memory_cell *dest;
        memory_cell *src1;
        memory_cell *src2;

        /* fetching destination */
        switch (i.opcode[0] & 0x03)
        {
            /* skip fetching for optimization */
            case 0: { break; }
            case MEM_CELL: { dest = memory[stack_pointer+i.rtl.dest.cell]; break; }
            case ARRAY_VAL: { dest = memory[stack_pointer+i.rtl.dest.cell]->l.cells[i.rtl.dest.index]; break; }
            case ARRAY_CELL: { dest = memory[stack_pointer+i.rtl.dest.cell]->l.cells[(int)i.rtl.dest.value]; break; }
        }

     /* omitted code */

     switch (opcode)
     {
         case ADD:
         {
             if (src1->type == M_VECTOR && src2->type == M_VECTOR)
             {
                 dest->type = M_VECTOR;
                 dest->v.x = src1->v.x + src2->v.x;
                 dest->v.y = src1->v.y + src2->v.y;
                 dest->v.z = src1->v.z + src2->v.z;
              }

      /* omitted code */

È facile / conveniente provare la compilation jit? Ma davvero non so da dove cominciare, ecco perché sto chiedendo alcuni consigli.

A parte questo, ci sono altri consigli che dovrei prendere in considerazione per svilupparlo?

Questa macchina virtuale dovrebbe essere abbastanza veloce per calcolare gli shader per un ray tracer ma non ho ancora fatto alcun tipo di benchmark.

È stato utile?

Soluzione

Prima di scrivere un compilatore JIT (" Just-in-time "), dovresti almeno considerare come scrivere un " Way-ahead-of-time " compilatore.

Cioè, dato un programma costituito da istruzioni per la tua VM, come produresti un programma composto da istruzioni x86 (o qualunque altra cosa), che fa lo stesso del programma originale? Come ottimizzeresti l'output per diversi set di istruzioni e diverse versioni della stessa architettura? L'esempio di codice operativo che hai fornito ha un'implementazione piuttosto complicata, quindi quali codici operativi implementeresti "inline" semplicemente emettendo il codice che fa il lavoro e quale implementeresti emettendo una chiamata a un codice condiviso?

Un JIT deve essere in grado di farlo e deve anche prendere decisioni mentre la VM è in esecuzione su quale codice lo fa, quando lo fa e come rappresenta la combinazione risultante di istruzioni VM e native istruzioni.

Se non sei già un assembl-jockey, allora non ti consiglio di scrivere un JIT. Questo non vuol dire che "non farlo mai", ma dovresti diventare un jockey di assemblaggio prima di iniziare sul serio.

Un'alternativa potrebbe essere quella di scrivere un compilatore non JIT per convertire le istruzioni della VM (o il linguaggio di scripting originale) in bytecode Java o LLVM, come afferma Jeff Foster. Quindi lascia che la toolchain per quel bytecode esegua il difficile lavoro dipendente dalla CPU.

Altri suggerimenti

Una VM è un compito importante da considerare. Hai mai pensato di basare la tua VM su qualcosa come LLVM ?

LLVM fornirà una buona base da cui partire e ci sono molti progetti di esempio che puoi usare per comprensione.

Steve Jessop ha ragione: il compilatore JIT è molto più difficile del normale compilatore. E il normale compilatore è difficile da solo.

Ma, leggendo l'ultima parte della domanda, mi chiedo se vuoi davvero un compilatore JIT.

Se il tuo problema è così:

  

Voglio creare un programma di ray tracing che consenta all'utente di fornire le proprie procedure shader ecc.   utilizzando la mia lingua specifica del dominio.   Va bene.   Ho il mio linguaggio definito, l'interprete implementato e funziona bene e correttamente.   Ma è lento: come posso eseguirlo come codice nativo?

Quindi ecco cosa facevo in situazioni simili:

  • Traduci le procedure fornite dall'utente in funzioni C che possono essere richiamate dal tuo programma.

  • Scrivili nel normale file sorgente C con #includes ecc.

  • Compilali come .dll (o .so in * nix) usando il normale compilatore C.

  • Carica .dll dinamicamente nel tuo programma, scopri i puntatori alle funzioni e usali nel tuo ray tracer al posto delle versioni interpretate.

Alcune note:

  • In alcuni ambienti potrebbe essere impossibile: nessun accesso al compilatore C o ai criteri di sistema che ti proibisce di caricare la tua dll. Quindi controlla prima di provarlo.

  • Non scartare l'interprete. Conservalo come implementazione di riferimento della tua lingua.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top