Лучший способ анализа даты и времени в golang

у меня есть много значений datetime, входящих в качестве строки в мою программу golang. Формат фиксирован по количеству цифр:

2006/01/02 15:04:05

Я начал разбирать эти даты с времени.Разбор функции

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

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

но у меня были некоторые проблемы с моей программой. Таким образом, я попытался настроить его, написав свою собственную функцию синтаксического анализа, учитывая, что мой формат является фиксированным:

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
}

наконец, я сделал тест поверх этих 2 функции и получили следующий результат:

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

это улучшение производительности на 27%. Есть ли лучший способ с точки зрения производительности, который может улучшить такой синтаксический анализ datetime ?

2 ответов


Я ожидал бы сделать всю вашу программу намного быстрее. Например, 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
}

критерии:

$ 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
$ 

ссылки:

Профилирование Go Программы


если вы настаиваете на использовании date string используйте 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
}

от того, что вы уже показали, используя strconv.Atoi сразу улучшил ваше представление. Вы можете пойти дальше и свернуть свой собственный atoi для вашего конкретного случая использования.

вы ожидаете, что каждый элемент будет положительным числом базы-10. Вы также знаете, что он не может переполняться, потому что максимальная длина переданного строкового представления равна 4. Единственная возможная ошибка - это незначный символ в строке. Зная это, мы можем просто сделать следующее:

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
}

накрутка это в ParseDate3 у меня следующие результаты:

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

вы можете сделать это быстрее, не возвращая ошибку в atoi, но я рекомендую вам проверить ввод в любом случае (если он не проверен где-то еще в вашем коде).

альтернативный подход atoi после просмотра встроенного решения:

продвигая это еще дальше, вы можете воспользоваться тем фактом, что все, кроме одной из переданных строк, имеют 2-значную длину (год-4-значный, но это умножить на два). Создание atoi с 2-значной строкой устранит for петли. Пример:

// 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)
}

преобразование года в число потребуется 2-шаговый подход, то:

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

это дает дополнительное улучшение:

BenchmarkParseDate4 50000000            61 ns/op

обратите внимание, что встроенная версия, предложенная @peterSO, только немного быстрее (54 НС / ОП в моем случае), но решение выше дает вам возможность проверки ошибок, в то время как встроенная версия слепо возьмет все символы, преобразующие их в даты.