2014-10-13 55 views
9

Durante il tentativo di utilizzare i file mappati in memoria per creare un file da più gigabyte (circa 13gb), mi sono imbattuto in quello che sembra essere un problema con mmap(). L'implementazione iniziale è stata eseguita in C++ su Windows usando boost :: iostreams :: mapped_file_sink e tutto andava bene. Il codice è stato quindi eseguito su Linux e ciò che ha richiesto minuti su Windows è diventato ore su Linux.Prestazioni di file mappati in memoria male di Linux con accesso casuale C++ e Python

Le due macchine sono cloni dello stesso hardware: Dell R510 2,4 GHz 8 M Cache 16 GB Ram 1TB Disco PERC H200 Controller.

Linux è Oracle Enterprise Linux 6.5 che utilizza il kernel 3.8 e g ++ 4.83.

C'era qualche preoccupazione che potesse esserci un problema con la libreria boost, quindi le implementazioni sono state eseguite con boost :: interprocess :: file_mapping e di nuovo con nativo mmap(). Tutti e tre mostrano lo stesso comportamento. Le prestazioni di Windows e Linux sono pari a un certo punto quando le prestazioni di Linux diminuiscono.

Il codice sorgente completo ei numeri delle prestazioni sono collegati di seguito.

// C++ code using boost::iostreams 
void IostreamsMapping(size_t rowCount) 
{ 
    std::string outputFileName = "IoStreamsMapping.out"; 
    boost::iostreams::mapped_file_params params(outputFileName); 
    params.new_file_size = static_cast<boost::iostreams::stream_offset>(sizeof(uint64_t) * rowCount); 
    boost::iostreams::mapped_file_sink fileSink(params); // NOTE: using this form of the constructor will take care of creating and sizing the file. 
    uint64_t* dest = reinterpret_cast<uint64_t*>(fileSink.data()); 
    DoMapping(dest, rowCount); 
} 

void DoMapping(uint64_t* dest, size_t rowCount) 
{ 
    inputStream->seekg(0, std::ios::beg); 
    uint32_t index, value; 
    for (size_t i = 0; i<rowCount; ++i) 
    { 
     inputStream->read(reinterpret_cast<char*>(&index), static_cast<std::streamsize>(sizeof(uint32_t))); 
     inputStream->read(reinterpret_cast<char*>(&value), static_cast<std::streamsize>(sizeof(uint32_t))); 
     dest[index] = value; 
    } 
} 

Un test finale è stato eseguito in Python per riprodurlo in un'altra lingua. La caduta è avvenuta nello stesso posto, quindi sembra lo stesso problema.

# Python code using numpy 
import numpy as np 
fpr = np.memmap(inputFile, dtype='uint32', mode='r', shape=(count*2)) 
out = np.memmap(outputFile, dtype='uint64', mode='w+', shape=(count)) 
print("writing output") 
out[fpr[::2]]=fpr[::2] 

per il C++ test Windows e Linux hanno simili prestazioni fino a circa 300 milioni di int64s (con Linux guardando un po 'più veloce). Sembra che le prestazioni cadano su Linux attorno a 3Gb (400 milioni * 8 byte per int64 = 3.2Gb) sia per C++ che per Python.

So su Linux a 32 bit che 3Gb è un limite magico, ma non sono a conoscenza di un comportamento simile per Linux a 64 bit.

L'essenza dei risultati è di 1,4 minuti per Windows che diventa 1,7 ore su Linux a 400 milioni di int64. In realtà sto provando a mappare circa 1,3 miliardi di int64s.

Qualcuno può spiegare perché c'è una tale disconnessione nelle prestazioni tra Windows e Linux?

Qualsiasi aiuto o suggerimento sarebbe molto apprezzato!

LoadTest.cpp

Makefile

LoadTest.vcxproj

updated mmap_test.py

original mmap_test.py

Updated Results Con codice Python aggiornato ... velocità Python attualmente comparabili con C++

Original Results NOTA: I risultati Python sono stantio

+1

Quanta memoria hai in la tua macchina Linux? –

+0

@MatsPetersson 16 GB Ram –

+1

Puoi provare a usare madvise() e vedere se cambia qualcosa? Potrebbe essere necessario provare vari parametri di consulenza: http://man7.org/linux/man-pages/man2/madvise.2.html –

risposta

6

Edit: Aggiornamento a "risposta adeguata". Il problema è nel modo in cui le "pagine sporche" sono gestite da Linux. Voglio ancora che il mio sistema svuoti pagine sporche di tanto in tanto, quindi non ho permesso che avesse troppe pagine in sospeso. Ma allo stesso tempo, posso dimostrare che questo è ciò che sta succedendo.

ho fatto questo (con "sudo"):

# echo 80 > /proc/sys/vm/dirty_ratio 
# echo 60 > /proc/sys/vm/dirty_background_ratio 

che dà queste impostazioni VM impostazioni sporchi:

grep^/proc/sys/vm/dirty* 
/proc/sys/vm/dirty_background_bytes:0 
/proc/sys/vm/dirty_background_ratio:60 
/proc/sys/vm/dirty_bytes:0 
/proc/sys/vm/dirty_expire_centisecs:3000 
/proc/sys/vm/dirty_ratio:80 
/proc/sys/vm/dirty_writeback_centisecs:500 

Questo rende la mia corsa di riferimento come questo:

$ ./a.out m64 200000000 
Setup Duration 33.1042 seconds 
Linux: mmap64 
size=1525 MB 
Mapping Duration 30.6785 seconds 
Overall Duration 91.7038 seconds 

Confronta con "prima":

$ ./a.out m64 200000000 
Setup Duration 33.7436 seconds 
Linux: mmap64 
size=1525 
Mapping Duration 1467.49 seconds 
Overall Duration 1501.89 seconds 

che aveva queste VM impostazioni sporchi:

grep^/proc/sys/vm/dirty* 
/proc/sys/vm/dirty_background_bytes:0 
/proc/sys/vm/dirty_background_ratio:10 
/proc/sys/vm/dirty_bytes:0 
/proc/sys/vm/dirty_expire_centisecs:3000 
/proc/sys/vm/dirty_ratio:20 
/proc/sys/vm/dirty_writeback_centisecs:500 

io non sono sicuro di quello che impostazioni devo usare per ottenere prestazioni ideali pur non lasciando tutte le pagine sporche seduti intorno a memoria per sempre (il che significa che se il il sistema si blocca, ci vuole molto più tempo per scrivere sul disco).

Per la storia: Ecco quello che ho scritto in origine come un "non-risposta" - alcuni commenti qui si applicano ancora ...

Non

proprio una risposta, ma trovo piuttosto interessante che se cambio il codice per prima leggi l'intero array, e lo scrivi, è SIGNIFICANTLY più veloce, che fare entrambi nello stesso ciclo. Apprezzo che questo sia del tutto inutile se hai bisogno di gestire set di dati davvero enormi (più grandi della memoria). Con il codice originale pubblicato, il tempo per i valori di 100 uint 64 è 134 secondi. Quando divido il ciclo di lettura e scrittura, sono i 43 secondi.

Questa è la funzione di DoMapping [solo codice che ho cambiato] dopo la modifica:

struct VI 
{ 
    uint32_t value; 
    uint32_t index; 
}; 


void DoMapping(uint64_t* dest, size_t rowCount) 
{ 
    inputStream->seekg(0, std::ios::beg); 
    std::chrono::system_clock::time_point startTime = std::chrono::system_clock::now(); 
    uint32_t index, value; 
    std::vector<VI> data; 
    for(size_t i = 0; i < rowCount; i++) 
    { 
     inputStream->read(reinterpret_cast<char*>(&index), static_cast<std::streamsize>(sizeof(uint32_t))); 
     inputStream->read(reinterpret_cast<char*>(&value), static_cast<std::streamsize>(sizeof(uint32_t))); 
     VI d = {index, value}; 
     data.push_back(d); 
    } 
    for (size_t i = 0; i<rowCount; ++i) 
    { 
     value = data[i].value; 
     index = data[i].index; 
     dest[index] = value; 
    } 
    std::chrono::duration<double> mappingTime = std::chrono::system_clock::now() - startTime; 
    std::cout << "Mapping Duration " << mappingTime.count() << " seconds" << std::endl; 
    inputStream.reset(); 
} 

Sono attualmente in esecuzione di un test con 200M registri, che sulla mia macchina prende una notevole quantità di tempo (2000 + secondi senza modifiche al codice). È molto chiaro che il tempo impiegato è da I/O su disco, e sto vedendo un IO-rate di 50-70MB/s, che è abbastanza buono, in quanto non mi aspetto che la mia configurazione piuttosto semplice possa fornire molto più di quello. Il miglioramento non è buono con le dimensioni più grandi, ma con un miglioramento decente: 1502 secondi, contro 2021 per "leggere e scrivere nello stesso ciclo".

Inoltre, vorrei sottolineare che questo è un test piuttosto terribile per qualsiasi sistema - il fatto che Linux sia notevolmente peggiore di Windows è fuori luogo - NON si vuole veramente mappare un file di grandi dimensioni e scrivere 8 byte [vale a dire che la pagina 4KB deve essere letta] su ciascuna pagina a caso. Se questo riflette la tua applicazione REALE, allora dovresti seriamente riconsiderare il tuo approccio in qualche modo. Funzionerà correttamente quando si ha abbastanza memoria libera che l'intera area mappata in memoria si adatta alla RAM.

C'è molto RAM nel mio sistema, quindi credo che il problema sia che a Linux non piacciono troppe pagine mappate "sporche".

ho la sensazione che questo potrebbe avere qualcosa a che fare con esso: https://serverfault.com/questions/126413/limit-linux-background-flush-dirty-pages Più spiegazione: http://www.westnet.com/~gsmith/content/linux-pdflush.htm

Purtroppo, sono anche molto stanco, e hanno bisogno di dormire. Vedrò se posso sperimentare con questi domani - ma non trattenere il respiro.Come ho detto, questo non è VERAMENTE una risposta, ma piuttosto un lungo commento che non si adatta in un commento (e contiene il codice, che è completamente spazzatura da leggere in un commento)