while 循环和递归所花费的时间
Time taken by a while loop and recursion
我不是在问我应该使用递归还是迭代,或者它们之间哪个更快。我试图了解迭代和递归所花费的时间,并且我在两者所花费的时间中提出了一个有趣的模式,即文件顶部的内容比另一个花费的时间更多。
例如:如果我在开始时编写 for 循环,它比递归花费更多时间,反之亦然。这两个过程所花费的时间之间的差异非常大,大约是 30 到 40 倍。
我的问题是:-
- 循环和递归的顺序重要吗?
- 有打印相关的东西吗?
- 这种行为的可能原因是什么?
以下是我在同一个文件中的代码,我使用的语言是 scala?
def count(x: Int): Unit = {
if (x <= 1000) {
print(s"$x ")
count(x + 1)
}
}
val t3 = System.currentTimeMillis()
count(1)
val t4 = System.currentTimeMillis()
println(s"\ntime taken by the recursion look = ${t4 - t3} mili second")
var c = 1
val t1 = System.currentTimeMillis()
while(c <= 1000)
{
print(s"$c ")
c+=1
}
val t2 = System.currentTimeMillis()
println(s"\ntime taken by the while loop = ${t2 - t1} mili second")
在这种情况下,递归和while循环所花费的时间分别为986ms、20ms。
当我切换循环和递归的位置时,即先循环再递归,递归和while循环所花费的时间分别为1.69秒和28毫秒。
编辑 1:
如果递归代码在顶部,我可以看到与 bufferWriter 相同的行为。但递归在循环下方时不是这种情况。当递归低于循环时,它花费的时间几乎相同,相差 2 到 3 毫秒。
Scala 不会编译成机器代码,而是编译成 "Java Virtual Machine"(JVM) 的字节码,然后在本机处理器上解释该代码。 JVM使用多种机制优化运行频繁的代码,最终将频繁调用的函数("hotspots")转换为纯机器代码。
这意味着测试函数的第一个 运行 并不能很好地衡量最终性能。您需要 "warm up" JIT 编译器 运行 在尝试测量所用时间之前多次使用测试代码。
此外,如评论中所述,进行任何类型的 I/O 都会使计时变得非常不可靠,因为 I/O 存在阻塞的危险。如果可能,写一个不做任何阻塞的测试用例。
如果您想在不依赖任何分析工具的情况下说服自己 tailrec 优化有效,您可以尝试以下方法:
- 使用更多的迭代方式
- 放弃前几次迭代,让 JIT 有时间醒来并进行热点优化
- 丢弃所有不可预测的副作用,例如打印到标准输出
- 放弃两种方法中相同的所有昂贵操作(格式化数字等)
- 多轮测量
- 随机化每轮的重复次数
- 在每一轮中随机化变体的顺序,以避免任何"catastrophic resonance"与垃圾收集器的循环
- 最好不要运行计算机上的任何其他东西
大致如下:
def compare(
xs: Array[(String, () => Unit)],
maxRepsPerBlock: Int = 10000,
totalRounds: Int = 100000,
warmupRounds: Int = 1000
): Unit = {
val n = xs.size
val times: Array[Long] = Array.ofDim[Long](n)
val rng = new util.Random
val indices = (0 until n).toList
var totalReps: Long = 0
for (round <- 1 to totalRounds) {
val order = rng.shuffle(indices)
val reps = rng.nextInt(maxRepsPerBlock / 2) + maxRepsPerBlock / 2
for (i <- order) {
var r = 0
while (r < reps) {
r += 1
val start = System.currentTimeMillis
(xs(i)._2)()
val end = System.currentTimeMillis
if (round > warmupRounds) {
times(i) += (end - start)
}
}
}
if (round > warmupRounds) {
totalReps += reps
}
}
for (i <- 0 until n) {
println(f"${xs(i)._1}%20s : ${times(i) / totalReps.toDouble}")
}
}
def gaussSumWhile(n: Int): Long = {
var acc: Long = 0
var i = 0
while (i <= n) {
acc += i
i += 1
}
acc
}
@annotation.tailrec
def gaussSumRec(n: Int, acc: Long = 0, i: Int = 0): Long = {
if (i <= n) gaussSumRec(n, acc + i, i + 1)
else acc
}
compare(Array(
("while", { () => gaussSumWhile(1000) }),
("@tailrec", { () => gaussSumRec(1000) })
))
这是它打印的内容:
while : 6.737733046257334E-5
@tailrec : 6.70325653896487E-5
即使是上面的简单提示也足以创建一个基准,表明 while 循环和尾递归函数 大致 花费相同的时间。
我不是在问我应该使用递归还是迭代,或者它们之间哪个更快。我试图了解迭代和递归所花费的时间,并且我在两者所花费的时间中提出了一个有趣的模式,即文件顶部的内容比另一个花费的时间更多。
例如:如果我在开始时编写 for 循环,它比递归花费更多时间,反之亦然。这两个过程所花费的时间之间的差异非常大,大约是 30 到 40 倍。
我的问题是:-
- 循环和递归的顺序重要吗?
- 有打印相关的东西吗?
- 这种行为的可能原因是什么?
以下是我在同一个文件中的代码,我使用的语言是 scala?
def count(x: Int): Unit = {
if (x <= 1000) {
print(s"$x ")
count(x + 1)
}
}
val t3 = System.currentTimeMillis()
count(1)
val t4 = System.currentTimeMillis()
println(s"\ntime taken by the recursion look = ${t4 - t3} mili second")
var c = 1
val t1 = System.currentTimeMillis()
while(c <= 1000)
{
print(s"$c ")
c+=1
}
val t2 = System.currentTimeMillis()
println(s"\ntime taken by the while loop = ${t2 - t1} mili second")
在这种情况下,递归和while循环所花费的时间分别为986ms、20ms。
当我切换循环和递归的位置时,即先循环再递归,递归和while循环所花费的时间分别为1.69秒和28毫秒。
编辑 1: 如果递归代码在顶部,我可以看到与 bufferWriter 相同的行为。但递归在循环下方时不是这种情况。当递归低于循环时,它花费的时间几乎相同,相差 2 到 3 毫秒。
Scala 不会编译成机器代码,而是编译成 "Java Virtual Machine"(JVM) 的字节码,然后在本机处理器上解释该代码。 JVM使用多种机制优化运行频繁的代码,最终将频繁调用的函数("hotspots")转换为纯机器代码。
这意味着测试函数的第一个 运行 并不能很好地衡量最终性能。您需要 "warm up" JIT 编译器 运行 在尝试测量所用时间之前多次使用测试代码。
此外,如评论中所述,进行任何类型的 I/O 都会使计时变得非常不可靠,因为 I/O 存在阻塞的危险。如果可能,写一个不做任何阻塞的测试用例。
如果您想在不依赖任何分析工具的情况下说服自己 tailrec 优化有效,您可以尝试以下方法:
- 使用更多的迭代方式
- 放弃前几次迭代,让 JIT 有时间醒来并进行热点优化
- 丢弃所有不可预测的副作用,例如打印到标准输出
- 放弃两种方法中相同的所有昂贵操作(格式化数字等)
- 多轮测量
- 随机化每轮的重复次数
- 在每一轮中随机化变体的顺序,以避免任何"catastrophic resonance"与垃圾收集器的循环
- 最好不要运行计算机上的任何其他东西
大致如下:
def compare(
xs: Array[(String, () => Unit)],
maxRepsPerBlock: Int = 10000,
totalRounds: Int = 100000,
warmupRounds: Int = 1000
): Unit = {
val n = xs.size
val times: Array[Long] = Array.ofDim[Long](n)
val rng = new util.Random
val indices = (0 until n).toList
var totalReps: Long = 0
for (round <- 1 to totalRounds) {
val order = rng.shuffle(indices)
val reps = rng.nextInt(maxRepsPerBlock / 2) + maxRepsPerBlock / 2
for (i <- order) {
var r = 0
while (r < reps) {
r += 1
val start = System.currentTimeMillis
(xs(i)._2)()
val end = System.currentTimeMillis
if (round > warmupRounds) {
times(i) += (end - start)
}
}
}
if (round > warmupRounds) {
totalReps += reps
}
}
for (i <- 0 until n) {
println(f"${xs(i)._1}%20s : ${times(i) / totalReps.toDouble}")
}
}
def gaussSumWhile(n: Int): Long = {
var acc: Long = 0
var i = 0
while (i <= n) {
acc += i
i += 1
}
acc
}
@annotation.tailrec
def gaussSumRec(n: Int, acc: Long = 0, i: Int = 0): Long = {
if (i <= n) gaussSumRec(n, acc + i, i + 1)
else acc
}
compare(Array(
("while", { () => gaussSumWhile(1000) }),
("@tailrec", { () => gaussSumRec(1000) })
))
这是它打印的内容:
while : 6.737733046257334E-5
@tailrec : 6.70325653896487E-5
即使是上面的简单提示也足以创建一个基准,表明 while 循环和尾递归函数 大致 花费相同的时间。