1
0
mirror of https://github.com/golang/go synced 2024-11-20 10:44:41 -07:00

time: make Format 2.7x faster

benchmark             old ns/op    new ns/op    delta
BenchmarkFormat            2495          937  -62.44%
BenchmarkFormatNow         2308          889  -61.48%

Update #3679.

R=r
CC=golang-dev
https://golang.org/cl/6278047
This commit is contained in:
Russ Cox 2012-06-03 11:08:17 -04:00
parent eb4138f481
commit a76c8b2430
4 changed files with 271 additions and 198 deletions

View File

@ -57,63 +57,74 @@ const (
)
const (
stdLongMonth = "January"
stdMonth = "Jan"
stdNumMonth = "1"
stdZeroMonth = "01"
stdLongWeekDay = "Monday"
stdWeekDay = "Mon"
stdDay = "2"
stdUnderDay = "_2"
stdZeroDay = "02"
stdHour = "15"
stdHour12 = "3"
stdZeroHour12 = "03"
stdMinute = "4"
stdZeroMinute = "04"
stdSecond = "5"
stdZeroSecond = "05"
stdLongYear = "2006"
stdYear = "06"
stdPM = "PM"
stdpm = "pm"
stdTZ = "MST"
stdISO8601TZ = "Z0700" // prints Z for UTC
stdISO8601ColonTZ = "Z07:00" // prints Z for UTC
stdNumTZ = "-0700" // always numeric
stdNumShortTZ = "-07" // always numeric
stdNumColonTZ = "-07:00" // always numeric
_ = iota
stdLongMonth = iota + stdNeedDate // "January"
stdMonth // "Jan"
stdNumMonth // "1"
stdZeroMonth // "01"
stdLongWeekDay // "Monday"
stdWeekDay // "Mon"
stdDay // "2"
stdUnderDay // "_2"
stdZeroDay // "02"
stdHour = iota + stdNeedClock // "15"
stdHour12 // "3"
stdZeroHour12 // "03"
stdMinute // "4"
stdZeroMinute // "04"
stdSecond // "5"
stdZeroSecond // "05"
stdLongYear = iota + stdNeedDate // "2006"
stdYear // "06"
stdPM = iota + stdNeedClock // "PM"
stdpm // "pm"
stdTZ = iota // "MST"
stdISO8601TZ // "Z0700" // prints Z for UTC
stdISO8601ColonTZ // "Z07:00" // prints Z for UTC
stdNumTZ // "-0700" // always numeric
stdNumShortTZ // "-07" // always numeric
stdNumColonTZ // "-07:00" // always numeric
stdFracSecond0 // ".0", ".00", ... , trailing zeros included
stdFracSecond9 // ".9", ".99", ..., trailing zeros omitted
stdNeedDate = 1 << 8 // need month, day, year
stdNeedClock = 2 << 8 // need hour, minute, second
stdArgShift = 16 // extra argument in high bits, above low stdArgShift
stdMask = 1<<stdArgShift - 1 // mask out argument
)
// std0x records the std values for "01", "02", ..., "06".
var std0x = [...]int{stdZeroMonth, stdZeroDay, stdZeroHour12, stdZeroMinute, stdZeroSecond, stdYear}
// nextStdChunk finds the first occurrence of a std string in
// layout and returns the text before, the std string, and the text after.
func nextStdChunk(layout string) (prefix, std, suffix string) {
func nextStdChunk(layout string) (prefix string, std int, suffix string) {
for i := 0; i < len(layout); i++ {
switch layout[i] {
switch c := int(layout[i]); c {
case 'J': // January, Jan
if len(layout) >= i+7 && layout[i:i+7] == stdLongMonth {
return layout[0:i], stdLongMonth, layout[i+7:]
}
if len(layout) >= i+3 && layout[i:i+3] == stdMonth {
if len(layout) >= i+3 && layout[i:i+3] == "Jan" {
if len(layout) >= i+7 && layout[i:i+7] == "January" {
return layout[0:i], stdLongMonth, layout[i+7:]
}
return layout[0:i], stdMonth, layout[i+3:]
}
case 'M': // Monday, Mon, MST
if len(layout) >= i+6 && layout[i:i+6] == stdLongWeekDay {
return layout[0:i], stdLongWeekDay, layout[i+6:]
}
if len(layout) >= i+3 {
if layout[i:i+3] == stdWeekDay {
if layout[i:i+3] == "Mon" {
if len(layout) >= i+6 && layout[i:i+6] == "Monday" {
return layout[0:i], stdLongWeekDay, layout[i+6:]
}
return layout[0:i], stdWeekDay, layout[i+3:]
}
if layout[i:i+3] == stdTZ {
if layout[i:i+3] == "MST" {
return layout[0:i], stdTZ, layout[i+3:]
}
}
case '0': // 01, 02, 03, 04, 05, 06
if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' {
return layout[0:i], layout[i : i+2], layout[i+2:]
return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:]
}
case '1': // 15, 1
@ -123,7 +134,7 @@ func nextStdChunk(layout string) (prefix, std, suffix string) {
return layout[0:i], stdNumMonth, layout[i+1:]
case '2': // 2006, 2
if len(layout) >= i+4 && layout[i:i+4] == stdLongYear {
if len(layout) >= i+4 && layout[i:i+4] == "2006" {
return layout[0:i], stdLongYear, layout[i+4:]
}
return layout[0:i], stdDay, layout[i+1:]
@ -133,35 +144,41 @@ func nextStdChunk(layout string) (prefix, std, suffix string) {
return layout[0:i], stdUnderDay, layout[i+2:]
}
case '3', '4', '5': // 3, 4, 5
return layout[0:i], layout[i : i+1], layout[i+1:]
case '3':
return layout[0:i], stdHour12, layout[i+1:]
case '4':
return layout[0:i], stdMinute, layout[i+1:]
case '5':
return layout[0:i], stdSecond, layout[i+1:]
case 'P': // PM
if len(layout) >= i+2 && layout[i+1] == 'M' {
return layout[0:i], layout[i : i+2], layout[i+2:]
return layout[0:i], stdPM, layout[i+2:]
}
case 'p': // pm
if len(layout) >= i+2 && layout[i+1] == 'm' {
return layout[0:i], layout[i : i+2], layout[i+2:]
return layout[0:i], stdpm, layout[i+2:]
}
case '-': // -0700, -07:00, -07
if len(layout) >= i+5 && layout[i:i+5] == stdNumTZ {
return layout[0:i], layout[i : i+5], layout[i+5:]
if len(layout) >= i+5 && layout[i:i+5] == "-0700" {
return layout[0:i], stdNumTZ, layout[i+5:]
}
if len(layout) >= i+6 && layout[i:i+6] == stdNumColonTZ {
return layout[0:i], layout[i : i+6], layout[i+6:]
if len(layout) >= i+6 && layout[i:i+6] == "-07:00" {
return layout[0:i], stdNumColonTZ, layout[i+6:]
}
if len(layout) >= i+3 && layout[i:i+3] == stdNumShortTZ {
return layout[0:i], layout[i : i+3], layout[i+3:]
if len(layout) >= i+3 && layout[i:i+3] == "-07" {
return layout[0:i], stdNumShortTZ, layout[i+3:]
}
case 'Z': // Z0700, Z07:00
if len(layout) >= i+5 && layout[i:i+5] == stdISO8601TZ {
return layout[0:i], layout[i : i+5], layout[i+5:]
if len(layout) >= i+5 && layout[i:i+5] == "Z0700" {
return layout[0:i], stdISO8601TZ, layout[i+5:]
}
if len(layout) >= i+6 && layout[i:i+6] == stdISO8601ColonTZ {
return layout[0:i], layout[i : i+6], layout[i+6:]
if len(layout) >= i+6 && layout[i:i+6] == "Z07:00" {
return layout[0:i], stdISO8601ColonTZ, layout[i+6:]
}
case '.': // .000 or .999 - repeated digits for fractional seconds.
if i+1 < len(layout) && (layout[i+1] == '0' || layout[i+1] == '9') {
@ -172,12 +189,17 @@ func nextStdChunk(layout string) (prefix, std, suffix string) {
}
// String of digits must end here - only fractional second is all digits.
if !isDigit(layout, j) {
return layout[0:i], layout[i:j], layout[j:]
std := stdFracSecond0
if layout[i+1] == '9' {
std = stdFracSecond9
}
std |= (j - (i + 1)) << stdArgShift
return layout[0:i], std, layout[j:]
}
}
}
}
return layout, "", ""
return layout, 0, ""
}
var longDayNames = []string{
@ -259,27 +281,36 @@ func lookup(tab []string, val string) (int, string, error) {
return -1, val, errBad
}
// appendUint appends the decimal form of x to b and returns the result.
// If x is a single-digit number and pad != 0, appendUint inserts the pad byte
// before the digit.
// Duplicates functionality in strconv, but avoids dependency.
func itoa(x int) string {
func appendUint(b []byte, x uint, pad byte) []byte {
if x < 10 {
if pad != 0 {
b = append(b, pad)
}
return append(b, byte('0'+x))
}
if x < 100 {
b = append(b, byte('0'+x/10))
b = append(b, byte('0'+x%10))
return b
}
var buf [32]byte
n := len(buf)
if x == 0 {
return "0"
return append(b, '0')
}
u := uint(x)
if x < 0 {
u = -u
}
for u > 0 {
for x >= 10 {
n--
buf[n] = byte(u%10 + '0')
u /= 10
buf[n] = byte(x%10 + '0')
x /= 10
}
if x < 0 {
n--
buf[n] = '-'
}
return string(buf[n:])
n--
buf[n] = byte(x + '0')
return append(b, buf[n:]...)
}
// Never printed, just needs to be non-nil for return by atoi.
@ -302,37 +333,30 @@ func atoi(s string) (x int, err error) {
return x, nil
}
func pad(i int, padding string) string {
s := itoa(i)
if i < 10 {
s = padding + s
// formatNano appends a fractional second, as nanoseconds, to b
// and returns the result.
func formatNano(b []byte, nanosec uint, n int, trim bool) []byte {
u := nanosec
var buf [9]byte
for start := len(buf); start > 0; {
start--
buf[start] = byte(u%10 + '0')
u /= 10
}
return s
}
func zeroPad(i int) string { return pad(i, "0") }
// formatNano formats a fractional second, as nanoseconds.
func formatNano(nanosec, n int, trim bool) string {
// User might give us bad data. Make sure it's positive and in range.
// They'll get nonsense output but it will have the right format.
s := itoa(int(uint(nanosec) % 1e9))
// Zero pad left without fmt.
if len(s) < 9 {
s = "000000000"[:9-len(s)] + s
}
if n > 9 {
n = 9
}
if trim {
for n > 0 && s[n-1] == '0' {
for n > 0 && buf[n-1] == '0' {
n--
}
if n == 0 {
return ""
return b
}
}
return "." + s[:n]
b = append(b, '.')
return append(b, buf[:n]...)
}
// String returns the time formatted using the format string
@ -341,16 +365,6 @@ func (t Time) String() string {
return t.Format("2006-01-02 15:04:05.999999999 -0700 MST")
}
type buffer []byte
func (b *buffer) WriteString(s string) {
*b = append(*b, s...)
}
func (b *buffer) String() string {
return string([]byte(*b))
}
// Format returns a textual representation of the time value formatted
// according to layout. The layout defines the format by showing the
// representation of the standard time,
@ -361,161 +375,172 @@ func (b *buffer) String() string {
// definition of the standard time, see the documentation for ANSIC.
func (t Time) Format(layout string) string {
var (
name, offset, abs = t.locabs()
year int = -1
month Month
day int
hour int = -1
min int
sec int
b buffer = make([]byte, 0, len(layout))
b []byte
buf [64]byte
)
max := len(layout) + 10
if max <= len(buf) {
b = buf[:0]
} else {
b = make([]byte, 0, max)
}
// Each iteration generates one std value.
for {
for layout != "" {
prefix, std, suffix := nextStdChunk(layout)
b.WriteString(prefix)
if std == "" {
if prefix != "" {
b = append(b, prefix...)
}
if std == 0 {
break
}
layout = suffix
// Compute year, month, day if needed.
if year < 0 {
// Jan 01 02 2006
if a, z := std[0], std[len(std)-1]; a == 'J' || a == 'j' || z == '1' || z == '2' || z == '6' {
year, month, day = t.Date()
}
if year < 0 && std&stdNeedDate != 0 {
year, month, day, _ = absDate(abs, true)
}
// Compute hour, minute, second if needed.
if hour < 0 {
// 03 04 05 15 pm
if z := std[len(std)-1]; z == '3' || z == '4' || z == '5' || z == 'm' || z == 'M' {
hour, min, sec = t.Clock()
}
if hour < 0 && std&stdNeedClock != 0 {
hour, min, sec = absClock(abs)
}
var p string
switch std {
switch std & stdMask {
case stdYear:
p = zeroPad(year % 100)
y := year
if y < 0 {
y = -y
}
b = appendUint(b, uint(y%100), '0')
case stdLongYear:
// Pad year to at least 4 digits.
p = itoa(year)
y := year
switch {
case year <= -1000:
// ok
b = append(b, '-')
y = -y
case year <= -100:
p = p[:1] + "0" + p[1:]
b = append(b, "-0"...)
y = -y
case year <= -10:
p = p[:1] + "00" + p[1:]
b = append(b, "-00"...)
y = -y
case year < 0:
p = p[:1] + "000" + p[1:]
b = append(b, "-000"...)
y = -y
case year < 10:
p = "000" + p
b = append(b, "000"...)
case year < 100:
p = "00" + p
b = append(b, "00"...)
case year < 1000:
p = "0" + p
b = append(b, '0')
}
b = appendUint(b, uint(y), 0)
case stdMonth:
p = month.String()[:3]
b = append(b, month.String()[:3]...)
case stdLongMonth:
p = month.String()
m := month.String()
b = append(b, m...)
case stdNumMonth:
p = itoa(int(month))
b = appendUint(b, uint(month), 0)
case stdZeroMonth:
p = zeroPad(int(month))
b = appendUint(b, uint(month), '0')
case stdWeekDay:
p = t.Weekday().String()[:3]
b = append(b, absWeekday(abs).String()[:3]...)
case stdLongWeekDay:
p = t.Weekday().String()
s := absWeekday(abs).String()
b = append(b, s...)
case stdDay:
p = itoa(day)
b = appendUint(b, uint(day), 0)
case stdUnderDay:
p = pad(day, " ")
b = appendUint(b, uint(day), ' ')
case stdZeroDay:
p = zeroPad(day)
b = appendUint(b, uint(day), '0')
case stdHour:
p = zeroPad(hour)
b = appendUint(b, uint(hour), '0')
case stdHour12:
// Noon is 12PM, midnight is 12AM.
hr := hour % 12
if hr == 0 {
hr = 12
}
p = itoa(hr)
b = appendUint(b, uint(hr), 0)
case stdZeroHour12:
// Noon is 12PM, midnight is 12AM.
hr := hour % 12
if hr == 0 {
hr = 12
}
p = zeroPad(hr)
b = appendUint(b, uint(hr), '0')
case stdMinute:
p = itoa(min)
b = appendUint(b, uint(min), 0)
case stdZeroMinute:
p = zeroPad(min)
b = appendUint(b, uint(min), '0')
case stdSecond:
p = itoa(sec)
b = appendUint(b, uint(sec), 0)
case stdZeroSecond:
p = zeroPad(sec)
b = appendUint(b, uint(sec), '0')
case stdPM:
if hour >= 12 {
p = "PM"
b = append(b, "PM"...)
} else {
p = "AM"
b = append(b, "AM"...)
}
case stdpm:
if hour >= 12 {
p = "pm"
b = append(b, "pm"...)
} else {
p = "am"
b = append(b, "am"...)
}
case stdISO8601TZ, stdISO8601ColonTZ, stdNumTZ, stdNumColonTZ:
// Ugly special case. We cheat and take the "Z" variants
// to mean "the time zone as formatted for ISO 8601".
_, offset := t.Zone()
if offset == 0 && std[0] == 'Z' {
p = "Z"
if offset == 0 && (std == stdISO8601TZ || std == stdISO8601ColonTZ) {
b = append(b, 'Z')
break
}
zone := offset / 60 // convert to minutes
if zone < 0 {
p = "-"
b = append(b, '-')
zone = -zone
} else {
p = "+"
b = append(b, '+')
}
p += zeroPad(zone / 60)
b = appendUint(b, uint(zone/60), '0')
if std == stdISO8601ColonTZ || std == stdNumColonTZ {
p += ":"
b = append(b, ':')
}
p += zeroPad(zone % 60)
b = appendUint(b, uint(zone%60), '0')
case stdTZ:
name, offset := t.Zone()
if name != "" {
p = name
b = append(b, name...)
break
}
// No time zone known for this time, but we must print one.
// Use the -0700 format.
zone := offset / 60 // convert to minutes
if zone < 0 {
b = append(b, '-')
zone = -zone
} else {
// No time zone known for this time, but we must print one.
// Use the -0700 format.
zone := offset / 60 // convert to minutes
if zone < 0 {
p = "-"
zone = -zone
} else {
p = "+"
}
p += zeroPad(zone / 60)
p += zeroPad(zone % 60)
}
default:
if len(std) >= 2 && (std[0:2] == ".0" || std[0:2] == ".9") {
p = formatNano(t.Nanosecond(), len(std)-1, std[1] == '9')
b = append(b, '+')
}
b = appendUint(b, uint(zone/60), '0')
b = appendUint(b, uint(zone%60), '0')
case stdFracSecond0, stdFracSecond9:
b = formatNano(b, uint(t.Nanosecond()), std>>stdArgShift, std&stdMask == stdFracSecond9)
}
b.WriteString(p)
layout = suffix
}
return b.String()
return string(b)
}
var errBad = errors.New("bad value for field") // placeholder not passed to user
@ -638,11 +663,12 @@ func Parse(layout, value string) (Time, error) {
for {
var err error
prefix, std, suffix := nextStdChunk(layout)
stdstr := layout[len(prefix) : len(layout)-len(suffix)]
value, err = skip(value, prefix)
if err != nil {
return Time{}, &ParseError{alayout, avalue, prefix, value, ""}
}
if len(std) == 0 {
if std == 0 {
if len(value) != 0 {
return Time{}, &ParseError{alayout, avalue, "", value, ": extra text: " + value}
}
@ -650,7 +676,7 @@ func Parse(layout, value string) (Time, error) {
}
layout = suffix
var p string
switch std {
switch std & stdMask {
case stdYear:
if len(value) < 2 {
err = errBad
@ -716,7 +742,8 @@ func Parse(layout, value string) (Time, error) {
// fractional second in the format?
if len(value) >= 2 && value[0] == '.' && isDigit(value, 1) {
_, std, _ := nextStdChunk(layout)
if len(std) > 0 && std[0] == '.' && isDigit(std, 1) {
std &= stdMask
if std == stdFracSecond0 || std == stdFracSecond9 {
// Fractional second in the layout; proceed normally
break
}
@ -756,7 +783,7 @@ func Parse(layout, value string) (Time, error) {
err = errBad
}
case stdISO8601TZ, stdISO8601ColonTZ, stdNumTZ, stdNumShortTZ, stdNumColonTZ:
if std[0] == 'Z' && len(value) >= 1 && value[0] == 'Z' {
if (std == stdISO8601TZ || std == stdISO8601ColonTZ) && len(value) >= 1 && value[0] == 'Z' {
value = value[1:]
z = UTC
break
@ -824,21 +851,17 @@ func Parse(layout, value string) (Time, error) {
}
// It's a valid format.
zoneName = p
default:
if len(value) < len(std) {
err = errBad
break
}
if len(std) >= 2 && std[0:2] == ".0" {
nsec, rangeErrString, err = parseNanoseconds(value, len(std))
value = value[len(std):]
}
case stdFracSecond0, stdFracSecond9:
ndigit := std >> stdArgShift
nsec, rangeErrString, err = parseNanoseconds(value, 1+ndigit)
value = value[1+ndigit:]
}
if rangeErrString != "" {
return Time{}, &ParseError{alayout, avalue, std, value, ": " + rangeErrString + " out of range"}
return Time{}, &ParseError{alayout, avalue, stdstr, value, ": " + rangeErrString + " out of range"}
}
if err != nil {
return Time{}, &ParseError{alayout, avalue, std, value, ""}
return Time{}, &ParseError{alayout, avalue, stdstr, value, ""}
}
}
if pmSet && hour < 12 {

View File

@ -257,6 +257,30 @@ func (t Time) abs() uint64 {
return uint64(sec + (unixToInternal + internalToAbsolute))
}
// locabs is a combination of the Zone and abs methods,
// extracting both return values from a single zone lookup.
func (t Time) locabs() (name string, offset int, abs uint64) {
l := t.loc
if l == nil {
l = &utcLoc
}
// Avoid function call if we hit the local time cache.
sec := t.sec + internalToUnix
if l != &utcLoc {
if l.cacheZone != nil && l.cacheStart <= sec && sec < l.cacheEnd {
name = l.cacheZone.name
offset = l.cacheZone.offset
} else {
name, offset, _, _, _ = l.lookup(sec)
}
sec += int64(offset)
} else {
name = "UTC"
}
abs = uint64(sec + (unixToInternal + internalToAbsolute))
return
}
// Date returns the year, month, and day in which t occurs.
func (t Time) Date() (year int, month Month, day int) {
year, month, day, _ = t.date(true)
@ -283,8 +307,13 @@ func (t Time) Day() int {
// Weekday returns the day of the week specified by t.
func (t Time) Weekday() Weekday {
return absWeekday(t.abs())
}
// absWeekday is like Weekday but operates on an absolute time.
func absWeekday(abs uint64) Weekday {
// January 1 of the absolute year, like January 1 of 2001, was a Monday.
sec := (t.abs() + uint64(Monday)*secondsPerDay) % secondsPerWeek
sec := (abs + uint64(Monday)*secondsPerDay) % secondsPerWeek
return Weekday(int(sec) / secondsPerDay)
}
@ -349,7 +378,12 @@ func (t Time) ISOWeek() (year, week int) {
// Clock returns the hour, minute, and second within the day specified by t.
func (t Time) Clock() (hour, min, sec int) {
sec = int(t.abs() % secondsPerDay)
return absClock(t.abs())
}
// absClock is like clock but operates on an absolute time.
func absClock(abs uint64) (hour, min, sec int) {
sec = int(abs % secondsPerDay)
hour = sec / secondsPerHour
sec -= hour * secondsPerHour
min = sec / secondsPerMinute
@ -610,8 +644,13 @@ const (
// date computes the year and, only when full=true,
// the month and day in which t occurs.
func (t Time) date(full bool) (year int, month Month, day int, yday int) {
return absDate(t.abs(), full)
}
// absDate is like date but operates on an absolute time.
func absDate(abs uint64, full bool) (year int, month Month, day int, yday int) {
// Split into time and day.
d := t.abs() / secondsPerDay
d := abs / secondsPerDay
// Account for 400 year cycles.
n := d / daysPer400Years

View File

@ -935,9 +935,18 @@ func BenchmarkNow(b *testing.B) {
}
func BenchmarkFormat(b *testing.B) {
time := Unix(1265346057, 0)
t := Unix(1265346057, 0)
for i := 0; i < b.N; i++ {
time.Format("Mon Jan 2 15:04:05 2006")
t.Format("Mon Jan 2 15:04:05 2006")
}
}
func BenchmarkFormatNow(b *testing.B) {
// Like BenchmarkFormat, but easier, because the time zone
// lookup cache is optimized for the present.
t := Now()
for i := 0; i < b.N; i++ {
t.Format("Mon Jan 2 15:04:05 2006")
}
}

View File

@ -123,21 +123,23 @@ func (l *Location) lookup(sec int64) (name string, offset int, isDST bool, start
// Not using sort.Search to avoid dependencies.
tx := l.tx
end = 1<<63 - 1
for len(tx) > 1 {
m := len(tx) / 2
lo := 0
hi := len(tx)
for hi-lo > 1 {
m := lo + (hi-lo)/2
lim := tx[m].when
if sec < lim {
end = lim
tx = tx[0:m]
hi = m
} else {
tx = tx[m:]
lo = m
}
}
zone := &l.zone[tx[0].index]
zone := &l.zone[tx[lo].index]
name = zone.name
offset = zone.offset
isDST = zone.isDST
start = tx[0].when
start = tx[lo].when
// end = maintained during the search
return
}