我有一个系统,它做一些模拟并输出大量带有时间戳的日志信息。
我很惊讶地看到DateTime.ToString()在循环中非常昂贵(它实际上被调用了很多次),所以我试图制作一个更快的版本来输出日期、时间和毫秒。
这可以做得更快吗(输出需要毫秒)?我没有尝试使用指针,因为我认为(可能是错误的)对于这么一小段代码,固定对象的开销会更高。
module DateTimeFormatter =
let inline private valueToDigit (value: int) : char =
char (value + int '0')
let inline private write2Characters (c: char[]) offset value =
c.[offset + 0] <- valueToDigit (value / 10)
c.[offset + 1] <- valueToDigit (value % 10)
let inline private write3Characters (c: char[]) offset value =
c.[offset + 0] <- valueToDigit (value / 100)
c.[offset + 1] <- valueToDigit ((value % 100) / 10)
c.[offset + 2] <- valueToDigit (value % 10)
let format (dateTime: DateTime) =
let c = Array.zeroCreate<char> 23
write2Characters c 0 (dateTime.Year / 100)
write2Characters c 2 (dateTime.Year % 100)
c.[4] <- '-'
write2Characters c 5 dateTime.Month
c.[7] <- '-'
write2Characters c 8 dateTime.Day
c.[10] <- ' '
write2Characters c 11 dateTime.Hour
c.[13] <- ':'
write2Characters c 14 dateTime.Minute
c.[16] <- ':'
write2Characters c 17 dateTime.Second
c.[19] <- '.'
write3Characters c 20 dateTime.Millisecond
new string(c)
下面是测试代码:
let a = DateTime.UtcNow
let iterations = 10_000_000
let sw = Stopwatch()
sw.Start()
for i = 0 to iterations do
a.ToString() |> ignore
sw.Stop()
printfn $"original no ms display {sw.ElapsedMilliseconds} ms"
sw.Reset()
sw.Start()
for i = 0 to iterations do
a.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) |> ignore
sw.Stop()
printfn $"original with ms display {sw.ElapsedMilliseconds} ms"
sw.Reset()
sw.Start()
for i = 0 to iterations do
a |> DateTimeFormatter.format |> ignore
sw.Stop()
printfn $"new with ms display {sw.ElapsedMilliseconds} ms"
和测试结果(MBP i7 2019):
original no ms display 2892 ms
original with ms display 4042 ms
new with ms display 1435 ms
我想改进的地方
-
避免重新创建字符数组
-
避免在字符数组中重新分配分隔符-它们永远不会改变
-
避免重新分配未更改的部分日期。如果你担心毫秒,我认为年、月、日、小时、分钟甚至秒都不会经常变化。
-
避免重新计算
int '0'
的值-它永远不会改变 -
避免额外的函数调用
let format = let mutable year = -1 let mutable month = -1 let mutable day = -1 let mutable hour = -1 let mutable minute = -1 let mutable second = -1 let array = "0000-00-00 00:00:00.000".ToCharArray() let zeroChar = int '0' fun (dateTime: DateTime) -> if dateTime.Year <> year then year <- dateTime.Year array.[0] <- char (zeroChar + year / 1000) array.[1] <- char (zeroChar + (year % 1000) / 100) array.[2] <- char (zeroChar + (year % 100) / 10) array.[3] <- char (zeroChar + (year % 10)) if dateTime.Month <> month then month <- dateTime.Month array.[5] <- char (zeroChar + month / 10) array.[6] <- char (zeroChar + month % 10) if dateTime.Day <> day then day <- dateTime.Day array.[8] <- char (zeroChar + day / 10) array.[9] <- char (zeroChar + day % 10) if dateTime.Hour <> hour then hour <- dateTime.Hour array.[11] <- char (zeroChar + hour / 10) array.[12] <- char (zeroChar + hour % 10) if dateTime.Minute <> minute then minute <- dateTime.Minute array.[14] <- char (zeroChar + minute / 10) array.[15] <- char (zeroChar + minute % 10) if dateTime.Second <> second then second <- dateTime.Second array.[17] <- char (zeroChar + second / 10) array.[18] <- char (zeroChar + second % 10) let ms = dateTime.Millisecond array.[20] <- char (zeroChar + ms / 100) array.[21] <- char (zeroChar + (ms % 100) / 10) array.[22] <- char (zeroChar + ms % 10) new string(array)
与您的测试用例一起运行它,与您的解决方案相比显示x2的性能,与原始解决方案相比显示x5的性能。
original no ms display 2354 ms
original with ms display 3545 ms
new with ms display 1221 ms
newest with ms display 691 ms
进一步的优化可以避免DateTime属性调用,并根据Ticks手动计算值。
重用数组和不重写常量字符已经在评论中提到。进一步考虑:
- 在这里使用
inline
关键字似乎并不影响"内联"的编译器优化。对应表达式;通过避免对数组索引 进行算术运算,可以更好地实现这一目标。 - 调用
System.DateTime
属性getter似乎是昂贵的 - 返回值是通过调用
System.String
类型的构造函数生成的,new string(c)
是c#调用它的方式
因此,看一下这个-额外的分割1似乎并没有减慢它:
let internal c = "0000-00-00T00:00:00.000".ToCharArray()
let internal (%&) x m = char(48 + (x / m) % 10)
let format (a : System.DateTime) =
let y, m, d, h, min, s, ms =
a.Year, a.Month, a.Day,
a.Hour, a.Minute, a.Second,
a.Millisecond
c.[ 0] <- y %& 1000
c.[ 1] <- y %& 100
c.[ 2] <- y %& 10
c.[ 3] <- y %& 1
c.[ 5] <- m %& 10
c.[ 6] <- m %& 1
c.[ 8] <- d %& 10
c.[ 9] <- d %& 1
c.[11] <- h %& 10
c.[12] <- h %& 1
c.[14] <- min %& 10
c.[15] <- min %& 1
c.[17] <- s %& 10
c.[18] <- s %& 1
c.[20] <- ms %& 100
c.[21] <- ms %& 10
c.[22] <- ms %& 1
System.String c
这将有望被编译成以下c#代码:
public static string format(DateTime a)
{
int year = a.Year;
...
c[0] = (char)(48 + year / 1000 % 10);
c[1] = (char)(48 + year / 100 % 10);
c[2] = (char)(48 + year / 10 % 10);
c[3] = (char)(48 + year / 1 % 10);
...
return new string(c);
}