2013-03-01 7 views
8

Questa routine viene chiamata un tempo di zillion per creare file csv di grandi dimensioni pieni di numeri. C'è un modo più efficiente per questo?da doppio a stringa senza notazione scientifica o zeri finali, efficacemente

static std::string dbl2str(double d) 
    { 
     std::stringstream ss; 
     ss << std::fixed << std::setprecision(10) << d;    //convert double to string w fixed notation, hi precision 
     std::string s = ss.str();         //output to std::string 
     s.erase(s.find_last_not_of('0') + 1, std::string::npos);  //remove trailing 000s (123.1200 => 123.12, 123.000 => 123.) 
     return (s[s.size()-1] == '.') ? s.substr(0, s.size()-1) : s; //remove dangling decimal (123. => 123) 
    } 
+1

Il titolo sembra sbagliato, dovrebbe essere il doppio della stringa? – hyde

+0

oops: il titolo è a ritroso. . . naturalmente è doppio rispetto alla stringa – tpascale

+0

Possibile duplicato di [Formattazione di cifre significative in C++ senza notazione scientifica] (http://stackoverflow.com/questions/17211122/formatting-n-significant-digits-in-c-without-scientific- notazione) – mirams

risposta

7

Prima di iniziare, controllare se si impiega tempo significativo in questa funzione. Fatelo misurando, con un profiler o altro. Sapendo che lo chiami un miliardo di volte è tutto molto buono, ma se si scopre che il tuo programma spende ancora solo l'1% del suo tempo in questa funzione, niente di ciò che fai qui può migliorare le prestazioni del tuo programma di oltre l'1%. Se così fosse, la risposta alla tua domanda sarebbe "per i tuoi scopi no, questa funzione non può essere resa significativamente più efficiente e stai sprecando il tuo tempo se provi".

Per prima cosa, evitare s.substr(0, s.size()-1). Questa copia la maggior parte della stringa e rende la tua funzione non idonea per NRVO, quindi penso che in genere riceverai una copia al reso. Così il primo cambiamento che vorrei fare è quello di sostituire l'ultima riga con:

if(s[s.size()-1] == '.') { 
    s.erase(s.end()-1); 
} 
return s; 

Ma se le prestazioni è un problema serio, allora ecco come lo farei. Non sto promettendo che questo è il più veloce possibile, ma evita alcuni problemi con allocazioni e copie non necessarie. Qualsiasi approccio che coinvolge stringstream richiederà una copia da stringstream al risultato, quindi vogliamo un'operazione più di basso livello, snprintf.

static std::string dbl2str(double d) 
{ 
    size_t len = std::snprintf(0, 0, "%.10f", d); 
    std::string s(len+1, 0); 
    // technically non-portable, see below 
    std::snprintf(&s[0], len+1, "%.10f", d); 
    // remove nul terminator 
    s.pop_back(); 
    // remove trailing zeros 
    s.erase(s.find_last_not_of('0') + 1, std::string::npos); 
    // remove trailing point 
    if(s.back() == '.') { 
     s.pop_back(); 
    } 
    return s; 
} 

La seconda chiamata a snprintf presuppone che std::string utilizza archiviazione contiguo. Questo è garantito in C++ 11. Non è garantito in C++ 03, ma è vero per tutte le implementazioni attivamente mantenute di std::string note al comitato C++. Se le prestazioni sono davvero importanti, penso che sia ragionevole fare un'ipotesi non portabile, dal momento che scrivere direttamente in una stringa salva la copia in una stringa in un secondo momento.

s.pop_back() è il modo in C++ 11 di dire s.erase(s.end()-1), e s.back() è s[s.size()-1]

Per un altro possibile miglioramento, si potrebbe eliminare la prima chiamata a snprintf e invece dimensioni della vostra s a un valore come std::numeric_limits<double>::max_exponent10 + 14 (in pratica, la lunghezza richiesta da -DBL_MAX). Il problema è che questo alloca e azzera molta più memoria di quanto sia tipicamente necessario (322 byte per un doppio IEEE). La mia intuizione è che questo sarà più lento della prima chiamata a snprintf, per non parlare di uno spreco di memoria nel caso in cui il valore di restituzione della stringa viene tenuto in giro per un po 'dal chiamante. Ma puoi sempre provarlo.

In alternativa, std::max((int)std::log10(d), 0) + 14 calcola un limite superiore ragionevolmente stretto sulla dimensione necessaria e potrebbe essere più veloce di snprintf può calcolarlo esattamente.

Infine, è possibile migliorare le prestazioni modificando l'interfaccia della funzione. Ad esempio, invece di restituire una nuova stringa si potrebbe forse aggiungere a una stringa passata dal chiamante:

void append_dbl2str(std::string &s, double d) { 
    size_t len = std::snprintf(0, 0, "%.10f", d); 
    size_t oldsize = s.size(); 
    s.resize(oldsize + len + 1); 
    // technically non-portable 
    std::snprintf(&s[oldsize], len+1, "%.10f", d); 
    // remove nul terminator 
    s.pop_back(); 
    // remove trailing zeros 
    s.erase(s.find_last_not_of('0') + 1, std::string::npos); 
    // remove trailing point 
    if(s.back() == '.') { 
     s.pop_back(); 
    } 
} 

Poi il chiamante può reserve() un sacco di spazio, chiamare la funzione più volte (presumibilmente con altra stringa aggiunge in tra) e scrivere il blocco di dati risultante nel file tutto in una volta, senza alcuna allocazione di memoria diversa dallo reserve. "Plenty" non deve essere l'intero file, potrebbe essere una riga o "paragrafo" alla volta, ma tutto ciò che evita un'allocazione di memoria di zillion è un potenziale incremento delle prestazioni.

+0

grazie per questa spiegazione molto dettagliata – tpascale

1
  • uso snprintf e una serie di char anziché stringstream e string
  • passare un puntatore a char buffer dbl2str in cui esso stampa (per evitare il costruttore di copia di string chiamata al ritorno) . Montare la stringa da stampare in un buffer di caratteri (o convertire il char buffer quando chiamato a una stringa o aggiungerlo a una stringa esistente)
  • dichiarare la funzione inline in un file di intestazione

    #include <cstdio> 
    inline void dbl2str(char *buffer, int bufsize, double d) 
    { 
        /** the caller must make sure that there is enough memory allocated for buffer */ 
        int len = snprintf(buffer, bufsize, "%lf", d); 
    
        /* len is the number of characters put into the buffer excluding the trailing \0 
        so buffer[len] is the \0 and buffer[len-1] is the last 'visible' character */ 
    
        while (len >= 1 && buffer[len-1] == '0') 
        --len; 
    
        /* terminate the string where the last '0' character was or overwrite the existing 
        0 if there was no '0' */ 
        buffer[len] = 0; 
    
        /* check for a trailing decimal point */ 
        if (len >= 1 && buffer[len-1] == '.') 
        buffer[len-1] = 0; 
    } 
    
+0

Parola chiave * inline * non influisce direttamente sull'ottimizzazione con "inlining", è un'istruzione per il linker che questo simbolo può apparire molte volte nel collegamento e non è un errore. La funzione è già * statica * in questione. – hyde

4

Efficiente in termini di velocità o brevità?

char buf[64]; 
sprintf(buf, "%-.*G", 16, 1.0); 
cout << buf << endl; 

Visualizza "1". Formatta fino a 16 cifre significative, senza zero finali, prima di tornare alla notazione scientifica.

+0

Il - non è strettamente necessario (a sinistra giustificato) –