如何“展开”递归函数?

18 投票
3 回答
7424 浏览
提问于 2025-04-16 19:17

我正在用C++写一个路径追踪器,想把最耗资源的代码尝试用CUDA或OpenCL来实现(我还不确定选哪个)。

我听说我的显卡版本的CUDA不支持递归,而我的路径追踪器 heavily依赖这个功能。

我已经用Python和C++写了代码,这里我贴一些简化的Python代码,方便大家看:

def Trace(ray):
  hit = what_object_is_hit(ray)

  if not hit:
    return Color(0, 0, 0)

  newRay = hit.bouceChildRayOffSurface(ray)

  return hit.diffuse * (Trace(newRay) + hit.emittance)

我尝试手动展开这个函数,发现确实有一定的规律(ddiffuse的意思,eemittance的意思):

Level 1:  d1 * e1 

Level 2:  d1 * d2 * e2
        + e1

Level 3:  d1 * d2 * d3 * e3
        + d1 * d2 * e2
        + e1

Level 4:  d1 * d2 * d3 * d4 * e4
        + d1 * d2 * d3 * e3
        + d1 * d2 * e2
        + e1

不过我可能错了……

我想问的是,怎么把这段代码用while循环来实现呢?

我在想用这样的格式:

total = Color(0, 0, 0)
n = 1

while n < 10:   # Maximum recursion depth
  result = magical_function()

  if not result:  break

  total += result
  n += 1

我之前从来没有处理过展开递归函数的任务,所以任何帮助都会非常感谢。谢谢!

3 个回答

1

一般来说,你总是可以用一个来表示递归。

举个例子:

stack.push(Color(0,0,0), ray, 0) // color, ray, level#
while (!stack.empty()):
   current = stack.pop()
   if (current.level == 10): break
   // compute hit, and newray from current.ray
   stack.push(hit.diffuse*(current.color + hit.emittance), newray, current.level+1)
return current

简单来说,递归的工作原理是把函数的参数放到栈里,然后用新的参数再次调用这个函数。你只需要用栈来模拟这个过程就可以了。

3

你真幸运。你的代码使用了尾递归,也就是在函数的最后一步使用递归。编译器通常可以帮你处理这个,但在这里你需要手动来做:

total = Color(0, 0, 0)
mult = 1
n = 1

while n < 10:   # Maximum recursion depth
  # Here was code to look for intersections

  if not hit: break

  total += mult * hit.diffuse * hit.emittance
  mult *= hit.diffuse
  n += 1

return total
21

在递归函数中,每次进行递归调用时,调用者的状态会被保存到一个栈里,等到递归调用完成后再恢复这个状态。要把递归函数转换成迭代函数,你需要把暂停的函数状态变成一个明确的数据结构。当然,你可以在软件中自己创建一个栈,但通常有一些技巧可以让你的代码更高效。

这个回答会逐步讲解这个例子的转换步骤。你可以把相同的方法应用到其他循环中。

尾递归转换

让我们再看看你的代码:

def Trace(ray):
  # Here was code to look for intersections

  if not hit:
      return Color(0, 0, 0)

  return hit.diffuse * (Trace(ray) + hit.emittance)

一般来说,递归调用必须返回到调用函数,这样调用者才能完成它正在做的事情。在这个例子中,调用者通过进行加法和乘法来“完成”它的工作。这会产生一个像 d1 * (d2 * (d3 * (... + e3) + e2) + e1))这样的计算。我们可以利用加法的分配律和乘法、加法的结合律,把这个计算转换成 [d1 * e1] + [(d1 * d2) * e2] + [(d1 * d2 * d3) * e3] + ... 。注意,这个序列中的第一个项只涉及到第一轮,第二个项只涉及到第一和第二轮,依此类推。这告诉我们可以实时计算这个序列。此外,这个序列包含了 (d1, d1*d2, d1*d2*d3, ...),我们也可以实时计算。把这些放回代码中:

def Trace(diffuse, emittance, ray):
  # Here was code to look for intersections

  if not hit: return emittance                            # The complete value has been computed

  new_diffuse = diffuse * hit.diffuse                     # (...) * dN
  new_emittance = emittance + new_diffuse * hit.emittance # (...) + [(d1 * ... * dN) + eN]
  return Trace(new_diffuse, new_emittance, ray)

尾递归消除

在新的循环中,调用者在被调用者完成后没有其他工作要做;它只是返回被调用者的结果。调用者没有工作要完成,所以它不需要保存任何状态!我们可以覆盖旧的参数,然后回到函数的开头(虽然这不是有效的Python代码,但它说明了这个观点):

def Trace(diffuse, emittance, ray):
  beginning:
  # Here was code to look for intersections

  if not hit: return emittance                            # The complete value has been computed

  new_diffuse = diffuse * hit.diffuse                     # (...) * dN
  new_emittance = emittance + new_diffuse * hit.emittance # (...) + [(d1 * ... * dN) + eN]
  (diffuse, emittance) = (new_diffuse, new_emittance)
  goto beginning

最后,我们把递归函数转换成了一个等效的循环。剩下的就是用Python语法来表达它。

def Trace(diffuse, emittance, ray):
  while True:
    # Here was code to look for intersections

    if not hit: break

    diffuse = diffuse * hit.diffuse                 # (...) * dN
    emittance = emittance + diffuse * hit.emittance # (...) + [(d1 * ... * dN) + eN]

  return emittance

撰写回答