Come posso migliorare / sostituire sprintf, che ho valutato essere un hotspot delle prestazioni?

StackOverflow https://stackoverflow.com/questions/271971

Domanda

Attraverso la profilazione ho scoperto che lo sprintf qui richiede molto tempo. Esiste un'alternativa con prestazioni migliori che gestisce ancora gli zeri iniziali nei campi y / m / d h / m / s?

SYSTEMTIME sysTime;
GetLocalTime( &sysTime );
char buf[80];
for (int i = 0; i < 100000; i++)
{

    sprintf(buf, "%4d-%02d-%02d %02d:%02d:%02d",
        sysTime.wYear, sysTime.wMonth, sysTime.wDay, 
        sysTime.wHour, sysTime.wMinute, sysTime.wSecond);

}

Nota: L'OP spiega nei commenti che questo è un esempio ridotto. Il "reale" il ciclo contiene codice aggiuntivo che utilizza valori temporali variabili da un database. La profilazione ha individuato sprintf () come offensore.

È stato utile?

Soluzione

Se scrivessi la tua funzione per svolgere il lavoro, una tabella di ricerca con i valori di stringa di 0 .. 61 eviterebbe di dover fare alcuna aritmetica per tutto tranne l'anno.

modifica: tieni presente che per far fronte ai secondi bisestili (e per abbinare strftime () ) dovresti essere in grado di stampare i valori dei secondi di 60 e 61.

char LeadingZeroIntegerValues[62][] = { "00", "01", "02", ... "59", "60", "61" };

In alternativa, che ne dici di strftime () ? Non ho idea di come si confrontino le prestazioni (potrebbe anche essere semplicemente chiamare sprintf ()), ma vale la pena guardarlo (e potrebbe anche fare la ricerca di cui sopra).

Altri suggerimenti

Potresti provare a riempire a turno ciascun carattere nell'output.

buf[0] = (sysTime.wYear / 1000) % 10 + '0' ;
buf[1] = (sysTime.wYear / 100) % 10 + '0';
buf[2] = (sysTime.wYear / 10) % 10 + '0';
buf[3] = sysTime.wYear % 10 + '0';
buf[4] = '-';

... ecc ...

Non carino, ma ottieni l'immagine. Se non altro, potrebbe aiutare a spiegare perché sprintf non sarà così veloce.

OTOH, forse potresti mettere in cache l'ultimo risultato. In questo modo avresti solo bisogno di generarne uno ogni secondo.

Printf deve gestire molti formati diversi. Potresti sicuramente prendere la fonte per printf e usarla come base per rotolare la tua versione che si occupa specificamente della struttura sysTime . In questo modo si passa a un argomento, e fa esattamente il lavoro che deve essere fatto e niente di più.

Cosa intendi con un "lungo"? time - poiché sprintf () è l'unica istruzione nel tuo ciclo e il " impianto idraulico " del ciclo (incremento, confronto) è trascurabile, il sprintf () deve consumare più tempo.

Ricordi la vecchia battuta sull'uomo che ha perso la sua fede nuziale in 3rd Street una notte, ma l'ha cercata in 5th perché la luce era più luminosa lì? Hai creato un esempio progettato per " provare " la tua ipotesi che sprintf () sia inefficace.

I tuoi risultati saranno più precisi se si profila "effettivo" codice che contiene sprintf () oltre a tutte le altre funzioni e algoritmi utilizzati. In alternativa, prova a scrivere la tua versione che si rivolge alla conversione numerica specifica con riempimento zero richiesta.

Potresti essere sorpreso dai risultati.

Sembra che Jaywalker stia suggerendo un metodo molto simile (battimi di meno di un'ora).

Oltre al metodo della tabella di ricerca già suggerito (array n2s [] di seguito), che ne dici di generare il tuo buffer di formato in modo che il solito sprintf sia meno intenso? Il codice seguente dovrà solo inserire i minuti e i secondi ogni volta che il ciclo non è cambiato, a meno che l'anno / mese / giorno / ora non siano cambiati. Ovviamente, se qualcuno di questi è cambiato, subisci un altro colpo di sprintf, ma nel complesso potrebbe non essere più di quello a cui stai attualmente assistendo (se combinato con la ricerca dell'array).


static char fbuf[80];
static SYSTEMTIME lastSysTime = {0, ..., 0};  // initialize to all zeros.

for (int i = 0; i < 100000; i++)
{
    if ((lastSysTime.wHour != sysTime.wHour)
    ||  (lastSysTime.wDay != sysTime.wDay)
    ||  (lastSysTime.wMonth != sysTime.wMonth)
    ||  (lastSysTime.wYear != sysTime.wYear))
    {
        sprintf(fbuf, "%4d-%02s-%02s %02s:%%02s:%%02s",
                sysTime.wYear, n2s[sysTime.wMonth],
                n2s[sysTime.wDay], n2s[sysTime.wHour]);

        lastSysTime.wHour = sysTime.wHour;
        lastSysTime.wDay = sysTime.wDay;
        lastSysTime.wMonth = sysTime.wMonth;
        lastSysTime.wYear = sysTime.wYear;
    }

    sprintf(buf, fbuf, n2s[sysTime.wMinute], n2s[sysTime.wSecond]);

}

Che ne dici di memorizzare nella cache i risultati? Non è una possibilità? Considerando che questa particolare chiamata sprintf () viene effettuata troppo spesso nel tuo codice, presumo che tra la maggior parte di queste chiamate consecutive, l'anno, il mese e il giorno non cambino.

Pertanto, possiamo implementare qualcosa di simile al seguente. Dichiara una struttura SYSTEMTIME vecchia e una corrente:

SYSTEMTIME sysTime, oldSysTime;

Inoltre, dichiarare parti separate per contenere la data e l'ora:

char datePart[80];
char timePart[80];

Per la prima volta, dovrai compilare sia sysTime, oldSysTime che datePart e timePart. Ma i successivi sprintf () possono essere resi più velocemente come indicato di seguito:

sprintf (timePart, "%02d:%02d:%02d", sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
if (oldSysTime.wYear == sysTime.wYear && 
  oldSysTime.wMonth == sysTime.wMonth &&
  oldSysTime.wDay == sysTime.wDay) 
  {
     // we can reuse the date part
     strcpy (buff, datePart);
     strcat (buff, timePart);
  }
else {
     // we need to regenerate the date part as well
     sprintf (datePart, "%4d-%02d-%02d", sysTime.wYear, sysTime.wMonth, sysTime.wDay);
     strcpy (buff, datePart);
     strcat (buff, timePart);
}

memcpy (&oldSysTime, &sysTime, sizeof (SYSTEMTIME));

Il codice sopra ha una certa ridondanza per renderlo più facile da capire. Puoi fattorizzare facilmente. Puoi accelerare ulteriormente se sai che anche l'ora e i minuti non cambieranno più velocemente della tua chiamata alla routine.

Vorrei fare alcune cose ...

  • memorizza nella cache l'ora corrente in modo da non dover rigenerare il timestamp ogni volta
  • esegue manualmente la conversione temporale. La parte più lenta delle funzioni della famiglia printf è l'analisi della stringa di formato, ed è sciocco dedicare cicli a tale analisi ad ogni esecuzione del ciclo.
  • prova a utilizzare le tabelle di ricerca a 2 byte per tutte le conversioni ( {" 00 " ;, " 01 " ;, " 02 " ;, ..., " 99 "} ). Questo perché vuoi evitare l'aritmetica moduluar e una tabella a 2 byte significa che devi usare solo un modulo, per l'anno.

Probabilmente otterresti un aumento della velocità eseguendo a mano una routine che stabilisce le cifre nel buf di ritorno, poiché potresti evitare di analizzare ripetutamente una stringa di formato e non dovresti occuparti di molti dei casi più complessi che gestiscono gli sprintf . Detesto comunque raccomandare di farlo.

Consiglio di provare a capire se è possibile in qualche modo ridurre la quantità necessaria per generare queste stringhe, se sono in alcuni momenti opzionali, possono essere memorizzate nella cache, ecc.

Al momento sto lavorando a un problema simile.

Devo registrare le istruzioni di debug con data / ora, nome file, numero di riga ecc. su un sistema incorporato. Abbiamo già un logger in atto ma quando giro la manopola su 'full logging', mangia tutti i nostri cicli proc e mette il nostro sistema in condizioni terribili, afferma che nessun dispositivo di elaborazione dovrebbe mai provare.

Qualcuno ha detto " Non puoi misurare / osservare qualcosa senza cambiare ciò che stai misurando / osservando. "

Quindi sto cambiando le cose per migliorare le prestazioni. Lo stato attuale delle cose è che sono 2 volte più veloce della chiamata di funzione originale (il collo di bottiglia in quel sistema di registrazione non è nella chiamata di funzione ma nel lettore di log che è un eseguibile separato, che posso scartare se scrivo il mio stack di registrazione).

L'interfaccia che devo fornire è qualcosa di simile a void log (int channel, char * nomefile, int lineno, formato, ...) . Devo aggiungere il nome del canale (che attualmente esegue una ricerca lineare all'interno di un elenco! Per ogni singola istruzione di debug!) E il timestamp incluso il contatore dei millisecondi. Ecco alcune delle cose che sto facendo per renderlo più veloce-

  • Stringi il nome del canale in modo che io possa strcpy piuttosto che cercare l'elenco. definire la macro LOG (canale, ... ecc.) come log (#channel, ... ecc) . Puoi utilizzare memcpy se correggi la lunghezza della stringa definendo LOG (canale, ...) log (" .... " # channel - sizeof (" .... " #channel) + * 11 *) per ottenere 10 lunghezze di canale byte fissate
  • Genera una stringa timestamp un paio di volte al secondo. Puoi usare asctime o qualcosa del genere. Quindi memcpy la stringa di lunghezza fissa per ogni istruzione di debug.
  • Se vuoi generare la stringa del timestamp in tempo reale, una tabella di ricerca con assegnazione (non memcpy!) è perfetta. Ma questo funziona solo per numeri a 2 cifre e forse per l'anno.
  • Che dire di tre cifre (millisecondi) e cinque cifre (lineno)? Non mi piace itoa e non mi piace itoa personalizzato ( digit = ((value / = value)% 10) ) sia perché div e mod sono lenti . Ho scritto le funzioni seguenti e in seguito ho scoperto che qualcosa di simile è nel manuale di ottimizzazione AMD (in assembly) che mi dà la sicurezza che si tratti delle implementazioni C più veloci.

    void itoa03(char *string, unsigned int value)
    {
       *string++ = '0' + ((value = value * 2684355) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = ' ';/* null terminate here if thats what you need */
    }
    

    Allo stesso modo, per i numeri di riga,

    void itoa05(char *string, unsigned int value)
    {
       *string++ = ' ';
       *string++ = '0' + ((value = value * 26844 + 12) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = ' ';/* null terminate here if thats what you need */
    }
    

Nel complesso, il mio codice è piuttosto veloce ora. Il vsnprintf () che devo usare richiede circa il 91% delle volte e il resto del mio codice ne prende solo il 9% (mentre il resto del codice vale a dire tranne vsprintf () era usato prima del 54%)

I due formattatori veloci che ho testato sono FastFormat e Karma :: generate (parte di Boost Spirit ).

Potresti anche trovare utile confrontarlo o almeno cercare benchmark esistenti.

Ad esempio questo (anche se manca FastFormat):

 Conversione rapida da intero a stringa in C ++

StringStream è il suggerimento che ho ricevuto da Google.

http://bytes.com/forum/thread132583.html

È difficile immaginare di battere sprintf nella formattazione di numeri interi. Sei sicuro che sprintf sia il tuo problema?

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top