Perché non tutte le lingue hanno la stessa efficienza?

4

Ho appena finito il mio corso per compilatori. Uno degli argomenti trattati era il modo per rendere i compilatori più efficienti. Ad esempio: ricorsione della coda, procedure di allineamento, riduzione della forza, rimozione del codice morto, propagazione costante. Considerando che il motivo principale per cui C è più efficiente di Python è perché non ha tipi dinamici e un garbage collector. Questi svantaggi possono essere rimossi durante la fase di ottimizzazione quando viene generato il codice macchina effettivo. Quindi perché, alla fine della giornata, un linguaggio come C è più efficiente di Python?

    
posta nikolaevra 03.08.2017 - 18:49
fonte

2 risposte

12

Il codice macchina non rende magicamente irrilevante il controllo del tipo. La cosa con molte varietà di codice macchina è che non hanno alcuna comprensione intrinseca dei tipi. Ma questo non significa che non puoi costruire un sistema di tipi su di essi usando le tue convenzioni.

Come esempio banale, potresti decidere che ogni valore è di 16 bit. I primi 8 bit rappresentano il tipo e il secondo 8 bit rappresenta il valore effettivo. Ora hai qualcosa che puoi controllare in fase di esecuzione per verificare che non stai aggiungendo un cavallo alla tua latitudine. Ecco c=b+c in pseudo-assembly:

  enter               // function entry
  loadw [ax], bx      // load 16 bits at [ax] into bx
  loadw [ax+2], cx    // load 16 bits at [ax+2] into cx
  cmp   bl, cl        // compare the low bytes of bx and cx
  jne   ERROR         // if ^ is not equal, jump to ERROR
  addb  bh, ch        // add the high bytes of bx and cx, store in ch
  storw cx, [ax+2]    // store cx back in memory
  ret                 // return to caller
ERROR:
  // Handle error, print warning, throw exception, etc

Questo è un esempio delle istruzioni che un linguaggio di tipo dinamicamente può compilare fino a (in questo caso, un linguaggio veramente fragile, considera che anche se b e c sono uguali tipo, l'aggiunta potrebbe essere un'operazione completamente priva di senso da eseguire su di essi, ad esempio GUID+GUID=huh? ). Ecco cosa può fare una lingua tipizzata staticamente:

  enter               // function entry
  loadb [ax], bh      // load 8 bits at [ax] into bh
  loadb [ax+1], ch    // load 8 bits at [ax+1] into ch
  addb  bh, ch        // add bh and ch, store in ch
  storb ch, [ax+1]    // store ch back in memory
  ret                 // return to caller

Notare che non c'è cmp , jne o gestione degli errori. Perché? Poiché una lingua tipizzata staticamente può dimostrare , senza eseguire il programma, una coppia di tipi non validi non entrerà mai in quella sezione di codice. Pertanto, può tranquillamente scegliere il codice di controllo. E dal momento che non controlla i metadati di tipo extra, può anche lasciarlo fuori, quindi perché carica e memorizza solo byte da 8 bit invece di parole da 16 bit.

Allo stesso modo, il codice macchina non ripulisce magicamente la tua spazzatura per te. Se utilizzi un garbage collector, non scompare solo durante la compilazione, viene tradotto nella lingua di destinazione insieme al resto del tuo programma .

Ma si noti che un garbage collector non è necessariamente meno efficiente delle alternative. malloc() non è necessariamente deterministico.

    
risposta data 03.08.2017 - 20:02
fonte
5

Considering that the main reason why C would be more efficient than Python is because it doesn't have dynamic types and a garbage collector, all of those disadvantages will be removed after actual machine code is generated.

Non esattamente. Sia che tu stia interpretando, eseguendo da un albero di analisi o da un codice byte intermedio o codice macchina compilato, un programma Python deve ancora comportarsi come un programma Python. Ciò significa che viene fornito con tutto il bagaglio di Python.

Considera questa funzione e un codice che la chiama:

def add(a, b):
    return a + b

print add(3, 2)
print add("Three", "Two")
print add("Five", 9)         # This will throw an exception.

Python, essendo digitato in modo dinamico, non può fare alcuna ipotesi sui tipi di a e b quando qualcosa chiama add() . Internamente, la prima chiamata non è semplice invocazione di una funzione con una coppia di costanti intere. Queste costanti hanno in realtà piccoli tag legati a loro che dicono "questo è un numero intero". All'interno della funzione, qualcosa deve guardare cosa c'è sul lato sinistro dell'operatore + , leggere il tipo fuori dal tag, verificare se quell'operatore è definito per quel tipo e consegnare all'operatore entrambe le espressioni. L'operatore deve quindi esaminarli e decidere se il secondo argomento è compatibile o meno con il primo prima di provare a produrre un risultato. La seconda e la terza chiamata funzionano esattamente allo stesso modo, tranne che è il tipo di stringa. Il terzo finisce per provare a consegnare al tipo di stringa un intero per l'operando di destra, il tipo di stringa decide che non può aggiungerlo e lancia un'eccezione.

È un sacco di decisioni che devono essere ripetute ogni volta qualcosa chiama add() .

I compilatori per linguaggi tipizzati staticamente passano attraverso lo stesso processo, ma conoscendo i tipi in anticipo, possono fare tutte le analisi una volta e generare un codice oggetto piacevole e compatto. L'aggiunta di due registri mangia un ciclo o due. L'esempio nella risposta di 8bittree richiede molto di più e digita solo sicurezza . Espandilo per capire quale bit di codice deve essere chiamato di fronte a un tipo arbitrario e diventa ancora più lungo.

    
risposta data 03.08.2017 - 21:55
fonte

Leggi altre domande sui tag