Perché "lasciare" più velocemente con l'ambito lessicale?


31

Durante la lettura del codice sorgente per la dolistmacro, ho riscontrato il seguente commento.

;; Questo non è un test affidabile, ma non importa perché entrambe le semantiche sono accettabili, anche se una è leggermente più veloce con scoping dinamico e l'altra è leggermente più veloce (e ha una semantica più pulita) con scoping lessicale .

Che si riferiva a questo frammento (che ho semplificato per chiarezza).

(if lexical-binding
    (let ((temp list))
      (while temp
        (let ((it (car temp)))
          ;; Body goes here
          (setq temp (cdr temp)))))
  (let ((temp list)
        it)
    (while temp
      (setq it (car temp))
      ;; Body goes here
      (setq temp (cdr temp)))))

Mi ha sorpreso vedere un letmodulo utilizzato all'interno di un loop. Pensavo che fosse lento rispetto all'uso ripetuto setqsulla stessa variabile esterna (come nel secondo caso sopra).

Lo avrei respinto come niente, se non per il commento immediatamente sopra di esso dicendo esplicitamente che è più veloce dell'alternativa (con legame lessicale). Quindi ... Perché?

  1. Perché il codice sopra riportato differisce nelle prestazioni sul legame lessicale vs dinamico?
  2. Perché la letforma è più veloce con lessicale?

Risposte:


38

Rilegatura lessicale e rilegatura dinamica in generale

Considera il seguente esempio:

(let ((lexical-binding nil))
  (disassemble
   (byte-compile (lambda ()
                   (let ((foo 10))
                     (message foo))))))

Compila e disassembla immediatamente un semplice lambdacon una variabile locale. Con lexical-bindingdisabilitato, come sopra, il codice byte ha il seguente aspetto:

0       constant  10
1       varbind   foo
2       constant  message
3       varref    foo
4       call      1
5       unbind    1
6       return    

Nota le istruzioni varbinde varref. Queste istruzioni associano e cercano rispettivamente le variabili in base al loro nome in un ambiente di rilegatura globale nella memoria dell'heap . Tutto ciò ha un effetto negativo sulle prestazioni: comporta hashing e confronto delle stringhe , sincronizzazione per l'accesso ai dati globali e accesso ripetuto alla memoria dell'heap che gioca male con la cache della CPU. Inoltre, le associazioni di variabili dinamiche devono essere ripristinate alla loro variabile precedente alla fine di let, il che aggiunge nulteriori ricerche per ogni letblocco con nassociazioni.

Se si legano lexical-bindinga tnell'esempio di cui sopra, il bytecode sembra un po 'diversa:

0       constant  10
1       constant  message
2       stack-ref 1
3       call      1
4       return    

Si noti che varbinde varrefsono completamente spariti. La variabile locale viene semplicemente inserita nello stack e definita da un offset costante tramite l' stack-refistruzione. In sostanza, la variabile viene rilegata e letta con tempo costante , letture e scritture di memoria nello stack , che è interamente locale e quindi gioca bene con la concorrenza e la cache della CPU e non comporta alcuna stringa.

In generale, con le ricerche di associazione lessicale delle variabili locali (ad es let. setq, Ecc.) Hanno una durata di funzionamento e una complessità della memoria molto inferiori .

Questo esempio specifico

Con l'associazione dinamica, ciascuno lascia incorrere in una penalità di prestazione, per motivi di cui sopra. Più permessi, più associazioni di variabili dinamiche.

In particolare, con un ulteriore letall'interno del loopcorpo, la variabile associata dovrebbe essere ripristinata ad ogni iterazione del ciclo , aggiungendo una ricerca di variabili aggiuntiva ad ogni iterazione . Quindi, è più veloce tenere fuori il corpo del ciclo, in modo che la variabile di iterazione venga ripristinata una sola volta , dopo che l'intero ciclo è terminato. Tuttavia, questo non è particolarmente elegante, poiché la variabile di iterazione è legata molto prima che sia effettivamente richiesta.

Con il legame lessicale, le lets sono economiche. In particolare, un letbody all'interno di un loop non è peggio (dal punto di vista delle prestazioni) di un letesterno di un body loop. Pertanto, è perfettamente corretto associare le variabili il più localmente possibile e mantenere la variabile di iterazione confinata al corpo del loop.

È anche leggermente più veloce, perché compila molto meno istruzioni. Considera il successivo smontaggio affiancato (let locale sul lato destro):

0       varref    list            0       varref    list         
1       constant  nil             1:1     dup                    
2       varbind   it              2       goto-if-nil-else-pop 2 
3       dup                       5       dup                    
4       varbind   temp            6       car                    
5       goto-if-nil-else-pop 2    7       stack-ref 1            
8:1     varref    temp            8       cdr                    
9       car                       9       discardN-preserve-tos 2
10      varset    it              11      goto      1            
11      varref    temp            14:2    return                 
12      cdr       
13      dup       
14      varset    temp
15      goto-if-not-nil 1
18      constant  nil
19:2    unbind    2
20      return    

Non ho idea, tuttavia, che cosa sta causando la differenza.


7

In breve, l'associazione dinamica è molto lenta. L'associazione lessicale è estremamente veloce in fase di esecuzione. Il motivo fondamentale è che il legame lessicale può essere risolto in fase di compilazione, mentre il legame dinamico non può essere risolto.

Considera il seguente codice:

(let ((x 42))
    (foo)
    (message "%d" x))

Durante la compilazione di let, il compilatore non può sapere se fooaccederà alla variabile (legata dinamicamente) x, quindi deve creare un'associazione per xe deve conservare il nome della variabile. Con l'associazione lessicale, il compilatore scarica semplicemente il valore dello xstack di associazioni, senza il suo nome, e accede direttamente alla voce corretta.

Ma aspetta - c'è di più. Con l'associazione lessicale, il compilatore è in grado di verificare che questa associazione specifica di xvenga utilizzata solo nel codice message; poiché xnon viene mai modificato, è sicuro inline xe cedere

(progn
  (foo)
  (message "%d" 42))

Non credo che l'attuale compilatore bytecode esegua questa ottimizzazione, ma sono fiducioso che lo farà in futuro.

Quindi in breve:

  • l'associazione dinamica è un'operazione pesante che consente poche opportunità di ottimizzazione;
  • il legame lessicale è un'operazione leggera;
  • l'associazione lessicale di un valore di sola lettura può spesso essere ottimizzata.

3

Questo commento non suggerisce che il legame lessicale sia né più veloce né più lento del legame dinamico. Piuttosto, suggerisce che queste diverse forme hanno caratteristiche prestazionali diverse in associazione lessicale e dinamica, ad esempio, una di esse è preferibile in una disciplina di associazione e l'altra preferibile nell'altra.

Quindi l'ambito lessicale è più veloce dell'ambito dinamico? Sospetto che in questo caso non ci siano molte differenze, ma non lo so - dovresti davvero misurarlo.


1
Non esiste un varbindcodice compilato in associazione lessicale. Questo è il punto e lo scopo.
lunaryorn,

Hmm. Ho creato un file contenente l'origine di cui sopra, a partire da ;; -*- lexical-binding: t -*-, caricato e chiamato (byte-compile 'sum1), supponendo che ha prodotto una definizione compilata in associazione lessicale. Tuttavia, non sembra avere.
gsg

Rimossi i commenti sul codice byte in quanto basati su tale presupposto errato.
gsg

la risposta di lunaryon mostra che questo codice è chiaramente più veloce in associazione lessicale (anche se ovviamente solo a livello micro).
shosti,

@gsg Questa dichiarazione è solo una variabile di file standard, che non ha alcun effetto sulle funzioni invocate dall'esterno del corrispondente buffer di file. IOW, ha effetto solo se visiti il ​​file sorgente e poi invochi byte-compileil buffer corrispondente essendo attuale, che è - a proposito - esattamente quello che sta facendo il compilatore di byte. Se invochi byte-compileseparatamente, devi impostare esplicitamente lexical-binding, come ho fatto nella mia risposta.
lunaryorn,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.