2014-11-30 15 views
6

Ho molti valori datetime ricevuti come stringa nel mio programma golang. Il formato è fisso in numero di cifre:Il modo migliore per analizzare data e ora in golang

2006/01/02 15:04:05 

ho iniziato ad analizzare queste date con la funzione di time.Parse

const dtFormat = "2006/01/02 15:04:05" 

func ParseDate1(strdate string) (time.Time, error) { 
    return time.Parse(dtFormat, strdate) 
} 

ma ho avuto qualche problema prestazioni con il mio programma. Così ho cercato di sintonizzare da scrittura la mia funzione di parsing, tenendo conto che il mio formato è tipo di fisso:

func ParseDate2(strdate string) (time.Time, error) { 
    year, _ := strconv.Atoi(strdate[:4]) 
    month, _ := strconv.Atoi(strdate[5:7]) 
    day, _ := strconv.Atoi(strdate[8:10]) 
    hour, _ := strconv.Atoi(strdate[11:13]) 
    minute, _ := strconv.Atoi(strdate[14:16]) 
    second, _ := strconv.Atoi(strdate[17:19]) 

    return time.Date(year, time.Month(month), day, hour, minute, second, 0, time.UTC), nil 
} 

finalmente ho fatto un punto di riferimento in cima a queste 2 funzioni ed ho ottenuto il seguente risultato:

BenchmarkParseDate1  5000000    343 ns/op 
BenchmarkParseDate2  10000000    248 ns/op 

Questo è un miglioramento delle prestazioni del 27%. Esiste un modo migliore in termini di prestazioni che potrebbe migliorare l'analisi di data e ora?

+0

Quante date devono essere analizzate? –

+1

Sto ricevendo i log live tramite zmq push/pull. È un flusso continuo. I bulk log lines per limitare il numero di transazioni. Il programma principale registra a 5000 TPS al picco. In condizioni normali è 500 TPS. – dbenque

+1

Puoi semplicemente memorizzare le date come una stringa e convertirle in date effettive dopo, se hai mai bisogno di leggere i dati? Se si tratta di un aggiornamento live di un dashboard di qualche tipo, è necessario aggiornare la dashboard 5000 volte al secondo? –

risposta

3

Mi aspetto di rendere l'intero programma molto più veloce.Ad esempio, ParseDate3,

func ParseDate3(date []byte) (time.Time, error) { 
    year := (((int(date[0])-'0')*10+int(date[1])-'0')*10+int(date[2])-'0')*10 + int(date[3]) - '0' 
    month := time.Month((int(date[5])-'0')*10 + int(date[6]) - '0') 
    day := (int(date[8])-'0')*10 + int(date[9]) - '0' 
    hour := (int(date[11])-'0')*10 + int(date[12]) - '0' 
    minute := (int(date[14])-'0')*10 + int(date[15]) - '0' 
    second := (int(date[17])-'0')*10 + int(date[18]) - '0' 
    return time.Date(year, month, day, hour, minute, second, 0, time.UTC), nil 
} 

Benchmark:

$ go test -bench=. 
testing: warning: no tests to run 
PASS 
BenchmarkParseDate1 5000000   308 ns/op 
BenchmarkParseDate2 10000000   225 ns/op 
BenchmarkParseDate3 30000000   44.9 ns/op 
ok  so/test 5.741s 
$ go test -bench=. 
testing: warning: no tests to run 
PASS 
BenchmarkParseDate1 5000000   308 ns/op 
BenchmarkParseDate2 10000000   226 ns/op 
BenchmarkParseDate3 30000000   45.4 ns/op 
ok  so/test 5.757s 
$ go test -bench=. 
testing: warning: no tests to run 
PASS 
BenchmarkParseDate1 5000000   312 ns/op 
BenchmarkParseDate2 10000000   225 ns/op 
BenchmarkParseDate3 30000000   45.0 ns/op 
ok  so/test 5.761s 
$ 

Riferimento:

Profiling Go Programs


Se ti ostini a usare date string, utilizzare ParseDate4,

func ParseDate4(date string) (time.Time, error) { 
    year := (((int(date[0])-'0')*10+int(date[1])-'0')*10+int(date[2])-'0')*10 + int(date[3]) - '0' 
    month := time.Month((int(date[5])-'0')*10 + int(date[6]) - '0') 
    day := (int(date[8])-'0')*10 + int(date[9]) - '0' 
    hour := (int(date[11])-'0')*10 + int(date[12]) - '0' 
    minute := (int(date[14])-'0')*10 + int(date[15]) - '0' 
    second := (int(date[17])-'0')*10 + int(date[18]) - '0' 
    return time.Date(year, month, day, hour, minute, second, 0, time.UTC), nil 
} 
+0

Dannazione, ho iniziato a lavorare su questo per curiosità, ma non sono arrivato da nessuna parte vicino a 45ns/op. Ho imparato a non pensare troppo complesso immagino :) Grazie! – user918176

+0

Non getthe stessi valori nel mio punto di riferimento: BenchmarkParseData1 5000000 331 ns/op BenchmarkParseData2 10000000 256 ns/op BenchmarkParseData3 20000000 143 ns/op Ancora dà risultati migliori. Grazie – dbenque

+0

@dbenque: Questo perché stai usando 'stringa' invece di' [] byte', quindi c'è un costo di conversione. Usa 'ParseDate4' per' date string'. Vedi la mia risposta rivista. Quali sono i tuoi risultati adesso? – peterSO

6

Da quello che hai già mostrato, l'utilizzo di strconv.Atoi ha migliorato direttamente le tue prestazioni. Puoi spingerlo ulteriormente e caricare il tuo atoi per il tuo caso d'uso particolare.

Si aspetta che ogni articolo sia un numero di base-10 positivo. Sai anche che non può overflow, perché la lunghezza massima della rappresentazione della stringa passata è 4. L'unico errore possibile è quindi un carattere non numerico nella stringa. Sapendo questo, possiamo semplicemente effettuare le seguenti operazioni:

var atoiError = errors.New("invalid number") 
func atoi(s string) (x int, err error) { 
    i := 0 
    for ; i < len(s); i++ { 
     c := s[i] 
     if c < '0' || c > '9' { 
      err = atoiError 
      return 
     } 
     x = x*10 + int(c) - '0' 
    } 
    return 
} 

Wrapping questo in ParseDate3, ho il seguente risultato:

BenchmarkParseDate1 5000000   355 ns/op 
BenchmarkParseDate2 10000000   278 ns/op 
BenchmarkParseDate3 20000000   88 ns/op 

si potrebbe rendere più veloce non restituendo un errore di atoi, ma io incoraggiati a testare l'input in ogni caso (a meno che non sia convalidato da qualche altra parte nel tuo codice).

approccio atoi alternativa dopo aver visto la soluzione inline:

Premendo questo ancora di più, si potrebbe approfittare del fatto che tutti, ma una delle stringhe passate sono lunghi 2 cifre (l'anno è a 4 cifre, ma è moltiplicato di due). Creare atoi prendendo una stringa di 2 cifre eliminerebbe il ciclo for. Esempio:

// Converts string of 2 characters into a positive integer, returns -1 on error 
func atoi2(s string) int { 
    x := uint(s[0]) - uint('0') 
    y := uint(s[1]) - uint('0') 
    if x > 9 || y > 9 { 
     return -1 // error 
    } 
    return int(x*10 + y) 
} 

Conversione anno nel numero avrebbe bisogno di approccio 2-step poi:

year := atoi2(strdate[0:2])*100 + atoi2(strdate[2:4]) 

Questo dà ulteriore miglioramento:

BenchmarkParseDate4 50000000   61 ns/op 

Nota che inline versione proposta da @peterSO è solo leggermente più veloce (54 ns/op nel mio caso), ma la soluzione sopra ti dà la possibilità di controllare gli errori, mentre la versione in linea prenderebbe ciecamente tutti i personaggi convertendoli in date.

+0

L'importanza dei nanosecondi aggiuntivi dipende dal caso d'uso, ma preferisco questa soluzione per essere chiara e semplice da leggere, oltre che veloce. –

Problemi correlati