Ho un programma "server" che aggiorna molti elenchi collegati nella memoria condivisa in risposta a eventi esterni. Voglio che i programmi client notino un aggiornamento su uno qualsiasi degli elenchi il più rapidamente possibile (latenza più bassa). Il server contrassegna lo state_
del nodo di una lista collegata come FILLED
una volta che i suoi dati sono stati compilati e il puntatore successivo è stato impostato su una posizione valida. Fino ad allora, il suo state_
è NOT_FILLED_YET
. Sto usando le barriere della memoria per assicurarmi che i client non vedano lo state_
come FILLED
prima che i dati all'interno siano effettivamente pronti (e sembra funzionare, non vedo mai dati corrotti). Inoltre, state_
è volatile per essere sicuro che il compilatore non risolva il controllo da parte del client dei loop.Di questi 3 metodi per leggere le liste collegate dalla memoria condivisa, perché il 3 ° più veloce?
Mantenendo il codice server esattamente lo stesso, ho trovato 3 metodi diversi per il client per la scansione degli elenchi collegati per le modifiche. La domanda è: perché il 3 ° metodo è più veloce?
Metodo 1: Round robin su tutte le liste collegate (chiamati 'canali') in modo continuo, cercando di vedere se tutti i nodi sono cambiate a 'RIEMPITO':
void method_one()
{
std::vector<Data*> channel_cursors;
for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
{
Data* current_item = static_cast<Data*>(i->get(segment)->tail_.get(segment));
channel_cursors.push_back(current_item);
}
while(true)
{
for(std::size_t i = 0; i < channel_list.size(); ++i)
{
Data* current_item = channel_cursors[i];
ACQUIRE_MEMORY_BARRIER;
if(current_item->state_ == NOT_FILLED_YET) {
continue;
}
log_latency(current_item->tv_sec_, current_item->tv_usec_);
channel_cursors[i] = static_cast<Data*>(current_item->next_.get(segment));
}
}
}
Metodo 1 ha una latenza molto bassa quando l'allora il numero di canali era piccolo. Ma quando il numero di canali è cresciuto (250 K +) è diventato molto lento a causa del loop su tutti i canali. Così ho provato ...
Metodo 2: Assegna un ID a ciascun elenco collegato. Tenere una "lista aggiornamenti" separata a lato. Ogni volta che uno degli elenchi collegati viene aggiornato, inserire il relativo ID nell'elenco degli aggiornamenti. Ora dobbiamo solo monitorare l'elenco degli aggiornamenti singoli e controllare gli ID che otteniamo da esso.
void method_two()
{
std::vector<Data*> channel_cursors;
for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
{
Data* current_item = static_cast<Data*>(i->get(segment)->tail_.get(segment));
channel_cursors.push_back(current_item);
}
UpdateID* update_cursor = static_cast<UpdateID*>(update_channel.tail_.get(segment));
while(true)
{
ACQUIRE_MEMORY_BARRIER;
if(update_cursor->state_ == NOT_FILLED_YET) {
continue;
}
::uint32_t update_id = update_cursor->list_id_;
Data* current_item = channel_cursors[update_id];
if(current_item->state_ == NOT_FILLED_YET) {
std::cerr << "This should never print." << std::endl; // it doesn't
continue;
}
log_latency(current_item->tv_sec_, current_item->tv_usec_);
channel_cursors[update_id] = static_cast<Data*>(current_item->next_.get(segment));
update_cursor = static_cast<UpdateID*>(update_cursor->next_.get(segment));
}
}
Il metodo 2 ha dato una latenza TERRIBILE. Considerando che il Metodo 1 potrebbe dare meno di 10 secondi di latenza, il Metodo 2 inesplicabilmente dovrebbe avere una latenza di 8 ms! Usando gettimeofday sembra che la modifica in update_cursor-> state_ sia stata molto lenta per propagare dalla vista del server al client (sono su una scatola multicore, quindi presumo che il ritardo sia dovuto alla cache). Così ho provato un approccio ibrido ...
Metodo 3: mantenere l'elenco di aggiornamento. Ma passa continuamente su tutti i canali, e all'interno di ogni iterazione controlla se l'elenco degli aggiornamenti è stato aggiornato. Se è così, vai con il numero premuto su di esso. In caso contrario, controlla il canale a cui è attualmente iterata.
void method_three()
{
std::vector<Data*> channel_cursors;
for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
{
Data* current_item = static_cast<Data*>(i->get(segment)->tail_.get(segment));
channel_cursors.push_back(current_item);
}
UpdateID* update_cursor = static_cast<UpdateID*>(update_channel.tail_.get(segment));
while(true)
{
for(std::size_t i = 0; i < channel_list.size(); ++i)
{
std::size_t idx = i;
ACQUIRE_MEMORY_BARRIER;
if(update_cursor->state_ != NOT_FILLED_YET) {
//std::cerr << "Found via update" << std::endl;
i--;
idx = update_cursor->list_id_;
update_cursor = static_cast<UpdateID*>(update_cursor->next_.get(segment));
}
Data* current_item = channel_cursors[idx];
ACQUIRE_MEMORY_BARRIER;
if(current_item->state_ == NOT_FILLED_YET) {
continue;
}
found_an_update = true;
log_latency(current_item->tv_sec_, current_item->tv_usec_);
channel_cursors[idx] = static_cast<Data*>(current_item->next_.get(segment));
}
}
}
La latenza di questo metodo era equivalente al Metodo 1, ma ridimensionata a un numero elevato di canali. Il problema è che non ho idea del perché. Solo per dare una svolta alle cose: se disapprovo la parte 'found via update', stampa tra OGNI LATENCY LOG MESSAGE. Il che significa che le cose si trovano sempre solo nella lista degli aggiornamenti! Quindi non capisco come questo metodo può essere più veloce rispetto al metodo 2.
Il codice compilabile completo (richiede GCC ed amplificare-1.41) che genera stringhe casuali come dati di test e ': http://pastebin.com/0kuzm3Uf
Aggiornamento: Tutti e 3 i metodi sono effettivamente spinlocking fino a quando si verifica un aggiornamento. La differenza sta nel tempo impiegato per notare che l'aggiornamento si è verificato. Loro all tassano continuamente il processore, in modo che non spieghi la differenza di velocità. Sto testando su una macchina a 4 core con nient'altro in esecuzione, quindi il server e il client non hanno nulla con cui competere. Ho persino creato una versione del codice in cui gli aggiornamenti segnalano una condizione e i client sono in attesa di una condizione: non ha aiutato la latenza di nessuno dei metodi.
Update2: Nonostante ci siano 3 metodi, ho provato solo 1 alla volta, quindi solo 1 server e 1 cliente sono in competizione per il membro dello stato.
Sì, è per evitare le condizioni di gara. Doveva essere nel metodo 2, ho modificato il mio post per risolverlo (i tempi sono gli stessi). I processori moderni (non solo il compilatore) sono liberi di riordinare i negozi e caricarli attraverso i thread, quindi un thread potrebbe impostare A e poi B, ma un altro thread potrebbe "vedere" che B è stato impostato prima di vedere che A era impostato. Il server riempie i dati del nodo, esegue un RELEASE_MEMORY_BARRIER, quindi imposta lo stato su FILLED. Combinato con il client che esegue ACQUIRE_MEMORY_BARRIER, questo garantisce che il client non percepisca mai lo stato_ come RIEMPITO prima che i dati del nodo siano compilati. –
Devo aggiungere che questa è la mia prima volta che uso le barriere di memoria, ed è così che penso * che dovrebbero per funzionare;) Se si segue il collegamento pastebin, il primo file, list.H definisce RELEASE_MEMORY_BARRIER e ACQUIRE_MEMORY_BARRIER a primitive documentate qui: http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins .html –
Mi spiace nello spam; p Ho modificato il mio post (vedere in fondo) per risolvere il problema del martellamento del processore. –