2010-04-30 14 views
5

Sto cercando di capire quanti cicli di clock o le istruzioni totali necessarie per accedere a un puntatore in C. Non penso di sapere come, ad esempio, p-> x = d-> a + f-> bQuante istruzioni per accedere al puntatore in C?

assumerei due carichi per puntatore, solo supponendo che ci sarebbe un carico per il puntatore e un carico per il valore. Quindi, in queste operazioni, la risoluzione del puntatore sarebbe un fattore molto più grande dell'effettiva aggiunta, per quanto riguarda il tentativo di accelerare questo codice, giusto?

Questo può dipendere dal compilatore e dall'architettura implementati, ma sono sulla strada giusta?

Ho visto un certo codice in cui ogni valore utilizzato nel dire, 3 aggiunte, proveniva da una

f2->sum = p1->p2->p3->x + p1->p2->p3->a + p1->p2->p3->m 

tipo di struttura, e sto cercando di definire quanto male questo è

+0

dipende dalla modalità di indirizzo imho - near jump/long jump, address computation ... –

+0

ricorda che il compilatore * dovrebbe * spostare molto di questo nello stack dopo averlo scaricato una volta. Se non lo è, e non devi preoccuparti del multithreading, puoi memorizzare in cache le inseguimenti del puntatore. –

+3

@Robert: se il multithreading influenzerà il dereferenziamento del puntatore nell'esempio, il codice richiede una serializzazione esplicita: un compilatore ottimizzante sarà sempre in grado di memorizzare "p3" in un registro e utilizzarlo per tutti e 3 gli accessi dei membri (supponendo che non ci sia nessun membro 'volatile' in uso). –

risposta

8

Questo dipende l'architettura a portata di mano.

Alcune architetture possono fare riferimento a/dereference memory per un'istruzione senza prima caricarla in un registro, altre no. Alcune architetture non hanno la nozione di istruzioni che calcolano le correzioni per te a dereferenziare e ti faranno caricare l'indirizzo di memoria, aggiungere il tuo offset ad esso, e quindi permetterti di dereferenziare la posizione di memoria. Sono sicuro che ci sono più varianti da chip a chip.

Una volta superati questi, ogni istruzione impiega diverso tempo a seconda dell'architettura. Ad essere onesti, però, è un overhead che è molto, molto minimale.

Per la tua domanda immediata di dereferenziazione di una catena di articoli, la lentezza verrà nel fatto che è probabile che ci sia una scarsa localizzazione di riferimento più lontano si va in una catena di dereferenziamento. Ciò significa più errori di cache, il che significa più colpi alla memoria principale (o al disco!) Per ottenere i dati. La memoria principale è molto lenta rispetto alla CPU.

+2

+1 per menzionare le implicazioni della cache –

+1

Non penso che sia minimo. Nell'ottimizzare il codice come sopra, ho visto 3 - 8x speedups sbarazzarsi dei puntatori e utilizzare il normale accesso agli array. Il problema è ancora peggio se i puntatori sono in realtà strutture. – Derek

+0

@derek Prima di tutto, è solo un sovraccarico potenzialmente negativo se il codice viene costantemente eseguito, nel qual caso, a meno che non si stia cacciando la cache, le continue ricerche di memoria dovrebbero essere memorizzate nella cache nel DTLB (nel caso di x86). È sempre bello usare i registri quando possibile, che è ciò che il compilatore _segue_. L'esempio nella mia risposta mostra che può esserci accesso al puntatore anche quando si assegnano le variabili locali tra loro. –

1

Dipende cosa si sta facendo, un puntatore banale dereference y = *z; dove

int x = 1; 
int* z = &x; 
int y; 

potrebbe montare a qualcosa di simile sul x86:

mov eax, [z] 
mov eax, [eax] 
mov [y], eax 

e y = x sarebbe ancora prendere un dereference di memoria:

mov eax, [x] 
mov [y], eax 

Istruzioni di movimento alla memoria occorrono circa 2-4 cicli IIRC.

Sebbene, se si carica memoria da posizioni completamente casuali, si causeranno molti errori di pagina, con conseguente centinaia di cicli di orologio sprecati.

2

Alcuni IDE come VisualStudio consentono di visualizzare l'assieme generato insieme al codice sorgente.

How to view the assembly behind the code using Visual C++?

Poi si può vedere per la vostra architettura e l'implementazione precisa quello che sembra.

Se si utilizza GDB (Linux, Mac) usa disassemble

(gdb) disas 0x32c4 0x32e4 
Dump of assembler code from 0x32c4 to 0x32e4: 
0x32c4 <main+204>:  addil 0,dp 
0x32c8 <main+208>:  ldw 0x22c(sr0,r1),r26 
0x32cc <main+212>:  ldil 0x3000,r31 
0x32d0 <main+216>:  ble 0x3f8(sr4,r31) 
0x32d4 <main+220>:  ldo 0(r31),rp 
0x32d8 <main+224>:  addil -0x800,dp 
0x32dc <main+228>:  ldo 0x588(r1),r26 
0x32e0 <main+232>:  ldil 0x3000,r31 
End of assembler dump. 
+0

Ho compilato con l'opzione -S e ho trovato qualcosa di molto simile a quello di cui altri hanno parlato. – Derek

1

Dove si può, il compilatore rimuoverà che overhead per voi, mantenendo posizioni di base più volte utilizzati in un registro (ad es. p1->p2->p3 in il tuo esempio).

Tuttavia, a volte il compilatore non può determinare quale puntatori potrebbe alias altri puntatori utilizzati all'interno della vostra funzione - il che significa che deve ripiegare su una posizione molto conservatrice, e ricaricare i valori da puntatori di frequente.

Qui è dove può essere d'aiuto la parola chiavedi C99. Ti consente di informare il compilatore quando determinati puntatori non vengono mai sottoposti ad alias da altri puntatori nell'ambito della funzione, che può in ogni caso migliorare l'ottimizzazione.


Per esempio, prendete questa funzione:

struct xyz { 
    int val1; 
    int val2; 
    int val3; 
}; 

struct abc { 
    struct xyz *p2; 
}; 

int foo(struct abc *p1) 
{ 
    int sum; 

    sum = p1->p2->val1 + p1->p2->val2 + p1->p2->val3; 

    return sum; 
} 

Sotto gcc 4.3.2 con livello di ottimizzazione -O1, si compila a questo codice x86:

foo: 
    pushl %ebp 
    movl %esp, %ebp 
    movl 8(%ebp), %eax 
    movl (%eax), %edx 
    movl 4(%edx), %eax 
    addl (%edx), %eax 
    addl 8(%edx), %eax 
    popl %ebp 
    ret 

Come si può vedere, differisce solo da p1 una volta - mantiene il valore di p1->p2 nel registro %edx e lo utilizza tre volte per recuperare i tre valori da quella struttura.

+0

In realtà ho scritto un programma di test, compilato con l'opzione -S, e ho scoperto che anche per un caso semplice come p1.p2-> p3-> valore o qualcosa del genere, ogni volta veniva ricaricato da p1. Molto prudente senza ottimizzazioni – Derek

+0

@Derek: quale livello di ottimizzazione hai utilizzato? Con '-O1' o superiore, dovrebbe ottimizzare abbastanza bene i casi semplici (vedi l'esempio che ho aggiunto alla mia risposta). – caf

+0

Sì, lo farà, ma perderà parte di questa abilità più il programma diventa complesso. Questo è il mio punto – Derek

Problemi correlati