SOFTWARE > Linguaggi di programmazione e scripting

Compilatori vs. Uomo

(1/3) > >>

dsar:
.

cdimauro:

--- Citazione da: "Z80Fan" ---programmazione amichevole
--- Termina citazione ---
e assembly non vanno d'accordo.

Per il resto l'assembly è meglio non usarlo, come giustamente osservava dsar, se non in particolarissimi casi.

Oggi i compilatori fanno veramente un ottimo lavoro. Soltanto alcune volte ho sentito la necessità di riscrivere il codice prodotto in assembly, perché quello generato non mi soddisfaceva.

Ad esempio, ecco il codice generato dal compilatore (Visual Studio C++ 2008):

--- Codice: ---static int long_compare(PyLongObject *a, PyLongObject *b)
{   Py_ssize_t sign;
    Py_ssize_t i = Py_SIZE(a), j = Py_SIZE(b);
          push        ebx  
          mov         ebx,dword ptr [edx+8]
          push        ebp  
          push        esi  
          mov         esi,dword ptr [edi+8]
          mov         eax,ebx
    if (i != j)
          cmp         eax,esi
          je          @else
        sign = (FAST_MSD(a, i) < 0 ? -i : i) - (FAST_MSD(b, j) < 0 ? -j : j);
          cmp         dword ptr [edx+eax*4+8],0
          jge         @a_pos
          neg         eax  
@a_pos:
          cmp         dword ptr [edi+esi*4+8],0
          jge         @b_pos
          neg         esi  
@b_pos:
          sub         eax,esi
          mov         ecx,eax
          jmp         @compute_sign
    else {

--- Termina codice ---
e quello che ho riscritto a mano:

--- Codice: ---static int long_compare(PyLongObject *a, PyLongObject *b)
{
    Py_ssize_t sign;
    Py_ssize_t i = Py_SIZE(a), j = Py_SIZE(b);
          mov         eax,dword ptr [esi+8]
          mov         ecx,dword ptr [edi+8]
    if (i != j)
          cmp         eax,ecx
          je          @else
        sign = (FAST_MSD(a, i) < 0 ? -i : i) - (FAST_MSD(b, j) < 0 ? -j : j);
          cmp         dword ptr [esi+eax*4+8],0
          jge         @a_pos
          neg         eax  
@a_pos:
          cmp         dword ptr [edi+ecx*4+8],0
          jge         @b_pos
          neg         ecx  
@b_pos:
          sub eax,ecx          
          jmp @compute_sign
else {

--- Termina codice ---
Fanno parte di alcune slide che non ho incluso nella presentazione dei talk che ho tenuto al recente EuroPython, per alleggerire la discussione.

Come potete vedere l'allocazione e l'uso di registri manuali risulta migliore. Per il resto ci sono stati un paio di casi in cui sono rimasto sorpreso dalla qualità del codice prodotto, superiore alle mie soluzioni (non me lo sarei mai aspettato, anche se sono anni ormai che non scrivo codice assembly).

cdimauro:

--- Citazione da: "dsar" ---
--- Citazione da: "cdimauro" ---Come potete vedere l'allocazione e l'uso di registri manuali risulta migliore.

--- Termina citazione ---

Posso muovere una critica? :-) Mi permetto di farlo perché i compilatori sono il mio campo (anche se hobbistico).
--- Termina citazione ---
Certamente, ci mancherebbe. ;)

--- Citazione ---Ovviamente è corretto il codice di msvc 2008, prima di operare sui registri si fa sempre il backup sullo stack. Pushare e poppare dallo stack è un'operazione lenta, motivo per cui la chiamata di una procedura ha un certo overhead (tutti i registri vengono backuppati nello stack).
Considera che il compilatore ha sempre una variabile riservata per registro, viene assegnato con il liveness analysis (le variabili più utilizzate), ci sono alcuni casi in cui serve più velocità, quindi prende in prestito un registro da una variabile backuppandola nello stack, e dopo lo ripoppa nel registro.
Quindi non puoi fare un mov su un registro senza prima salvare il contenuto, dipende cosa avviene dopo quel pezzo di codice, l'eseguibile potrebbe avere comportamenti indeterministici.
--- Termina citazione ---
In realtà alcuni registri (come eax, ecx, edx) sono liberi (e liberamente utilizzabili) già in partenza, per cui non è necessario farne il backup e si possono utilizzare immediatamente.

Mostro un esempio a riguardo:

--- Codice: ---static int long_compare(PyLongObject *a, PyLongObject *b)
{
    Py_ssize_t sign;
    if (Py_SIZE(a) != Py_SIZE(b)) {
          mov         edx,dword ptr [ebx+8]
          mov         eax,dword ptr [esi+8]
          push        ebp  
          push        edi  
          cmp         edx,eax
          je          @else
        sign = Py_SIZE(a) - Py_SIZE(b);
          sub         edx,eax
          mov         ecx,edx
          jmp         @compute_sign
    }
    else {

--- Termina codice ---
Questa funzione è quella usata correntemente da CPython (quelle precedenti riguardano la nuova implementazione che ho proposto) e il codice è quello generato dal solito VS 2008.

Come vedi eax ed edx vengono utilizzati immediatamente senza farne il backup; successivamente il compilatore fa il push di ebp ed edi perché probabilmente gli serviranno dopo, anche se io avrei aspettato il risultato del confronto, perché nel caso comune & semplice si deve soltanto calcolare il segno (-1, 0 o 1) e restituirne il valore (quindi ebp ed edi non vengono utilizzati).

Quest'esempio mostra, a mio avviso, come l'algoritmo di register allocation potrebbe essere ulteriormente migliorato da Microsoft (magari l'avranno già fatto nella 2010, ma io sono costretto a usare la 2008 finché gli sviluppatori di Python non decideranno di aggiornare il progetto) procedendo al backup (e successivo restore) dei registri soltanto nel ramo dell'else (quello più complicato).

--- Citazione ---Se vuoi fare in modo che il compilatore usi direttamente i registri dovresti fare una funzione inline (o macro, se è in C). Se le variabili che compari vengono usate abbastanza spesso ovviamente in quel punto te le ritrovi già nei registri e il compilatore fa il folding del codice (essendo inline/macro).
--- Termina citazione ---
Purtroppo quello è codice C (CPython non utilizza C++) per cui non si può utilizzare l'inline (che, comunque, ormai un compilatore può benissimo ignorare perché può decidere che non ne vale la pena; d'altra parte è soltanto un'indicazione).

Inoltre, anche se funzione è definita come static, viene comunque referenziata all'esterno di quel modulo tramite una tabella pubblica che ne contiene un puntatore (in pratica si simula la programmazione a oggetti), per cui comunque il compilatore sarebbe costretto a evitare l'inline e a generarne una copia.

Z80Fan:
Mi inserisco un attimo in questa interessante discussione. :)

--- Citazione da: "cdimauro" ---e assembly non vanno d'accordo.
--- Termina citazione ---
Ovviamente parlavo di "programmazione amichevole" per un programmatore Assembly, non di "programmazione amichevole" in generale.
Poi sappiamo tutti che non c'è nulla di amichevole nell'x86 ma quello è un altro discorso! :mrgreen:


--- Citazione da: "cdimauro" ---Inoltre, anche se funzione è definita come static, viene comunque referenziata all'esterno di quel modulo tramite una tabella pubblica che ne contiene un puntatore (in pratica si simula la programmazione a oggetti), per cui comunque il compilatore sarebbe costretto a evitare l'inline e a generarne una copia.
--- Termina citazione ---
Una volta, giocando con l'output di GCC (v. 4+) ho notato che piccole funzioni venivano messe inline, ma veniva cmq generata una copia a se stante; non era stato specificato nulla di particolare nel codice, solo -O2 (o O3 non ricordo) alla linea di comando.

TheKaneB:

--- Citazione da: "Z80Fan" ---Mi inserisco un attimo in questa interessante discussione. :)

--- Citazione da: "cdimauro" ---e assembly non vanno d'accordo.
--- Termina citazione ---
Ovviamente parlavo di "programmazione amichevole" per un programmatore Assembly, non di "programmazione amichevole" in generale.
Poi sappiamo tutti che non c'è nulla di amichevole nell'x86 ma quello è un altro discorso! :mrgreen:


--- Citazione da: "cdimauro" ---Inoltre, anche se funzione è definita come static, viene comunque referenziata all'esterno di quel modulo tramite una tabella pubblica che ne contiene un puntatore (in pratica si simula la programmazione a oggetti), per cui comunque il compilatore sarebbe costretto a evitare l'inline e a generarne una copia.
--- Termina citazione ---
Una volta, giocando con l'output di GCC (v. 4+) ho notato che piccole funzioni venivano messe inline, ma veniva cmq generata una copia a se stante; non era stato specificato nulla di particolare nel codice, solo -O2 (o O3 non ricordo) alla linea di comando.
--- Termina citazione ---

La copia della funzione verrebbe comunque strippata in fase di linking, a meno che tu non usi un puntatore a quella funzione (quindi il puntatore a funzione deve spuntare da qualche parte del codice come RValue).

Navigazione

[0] Indice dei post

[#] Pagina successiva

Vai alla versione completa