7

这个 Common Lisp 函数通过极其简单的幼儿园级算法和一些“案例”测试简单地计算墙壁线框边缘的四个顶点,似乎负责为每个渲染帧动态分配 196608 字节;SBCL 的分析器告诉我,就 consing 而言,这是我最有问题的功能。为了大致了解我正在做的事情,这是一个小型的第一人称地牢爬行游戏,地牢是 32x32 的单元格,每个单元格有 4 面墙。32 * 32 * 4 * x = 196608,所以 x 结果是 48,而恰好是 4 * 12(4 个墙 * 12 个浮子/墙?也许不是)。

现在,我可以通过在游戏模式中使用 OpenGL 显示列表轻松缓解这个性能问题,我想这就是我将继续做的事情。尽管如此,1)我通常不会过早地优化,更重要的是 2)我仍然不喜欢让某些像这样的烦人的痒没有被划伤,我想知道我还能做些什么。我的功能原样如下:

(defun calculate-wall-points (x y wall)
  (declare (integer x y)
           (keyword wall))
  "Return the 4 vertices (12 floats) of a given dungeon cell wall"
  (let ((xf (coerce x 'float))
        (yf (coerce y 'float)))
    (case wall
      (:SOUTH
       (values xf yf 0.0
               (1+ xf) yf 0.0
               (1+ xf) yf 1.0
               xf yf 1.0))
      (:WEST
       (values xf yf 0.0
               xf yf 1.0
               xf (1+ yf) 1.0
               xf (1+ yf) 0.0))
      (:NORTH
       (values xf (1+ yf) 0.0
               xf (1+ yf) 1.0
               (1+ xf) (1+ yf) 1.0
               (1+ xf) (1+ yf) 0.0))
      (:EAST
       (values (1+ xf) (1+ yf) 0.0
               (1+ xf) (1+ yf) 1.0
               (1+ xf) yf 1.0
               (1+ xf) yf 0.0))

      (otherwise
       (error "Not a valid heading passed for wall in function calculate-wall-points: ~A" wall)))))

总结几件事我试图解决这个问题:

  1. 在 3 处为“速度”和其他所有内容在 0 处执行“声明”以“优化”(在此函数中以及调用它的唯一函数中)。奇怪的是,分析器确实报告了这个功能稍微少了一点……但它仍然是 consed。我的目标是零骗局。算术不应该是缺点。

  2. 然后我认为“价值观”可能会这样做。也许,我想,它在内部就像函数“list”,毫无疑问,conses(“list”函数在宇宙中的唯一用途)。我做了什么来试图减轻这种情况?只是为了实验,我修改了文件以制作单个墙顶点缓冲区全局数组,大小适合 12 个浮点类型的元素,并修改了这个函数来修改它,以及调用函数在调用这个函数后从它读取(所以它会不断更新一组 12 个浮点数,保持不变)放在内存中,而不是分配任何东西)。奇怪的是,这并没有阻止这个功能成为一个小猪!那么……是“案件”在做阴谋吗?我确实觉得有趣的是,前面提到过,那个神秘数字是 48. 48 = 4 * 12,也许这 4 个案例测试乘以每个“值”调用的 12 个浮点数。或者,这可能是巧合,48 个字节意味着别的东西(因为浮点数不是 1 个字节,我怀疑是 -is- 别的东西)。这似乎很重要,但我无法完全理解我的下一个方法应该是什么。

  3. 尝试用“cond”等价物替换“case”,此时只是抓住稻草,也没有做任何事情。

那么这个函数的“神秘阴谋”是从哪里来的呢?更有经验的 Lisp 程序员如何处理这个棘手的问题?


(编辑)对于@FaheemMitha,是使用计算墙点函数的函数;这个麻烦的函数后来被 (declaim (inline calculate-wall-points)) 在 calculate-wall-points 的定义之前内联:

(defun render-dungeon-room (dungeon-object x y)
  (declare (optimize (speed 3) (space 0) (debug 0)))
  (declare (type fixnum x y))
  (let ((cell (cell-at dungeon-object x y)))
    (unless (null cell)
      (dolist (wall-heading +basic-headings+)
    (unless (eq wall-heading (opposite-heading *active-player-heading*))
      (when (eql (get-wall-type cell wall-heading) :NORMAL)
        (multiple-value-bind (v1x v1y v1z v2x v2y v2z v3x v3y v3z v4x v4y v4z)
        (calculate-wall-points x y wall-heading)
          (declare (type float v1x v1y v1z v2x v2y v2z v3x v3y v3z v4x v4y v4z))

      (gl:with-primitive :quads
    (if (is-edit-mode)
        (case wall-heading
          (:NORTH
           (gl:color 0.4 0.4 0.4))
          (:WEST
           (gl:color 0.4 0.0 0.0))
          (:SOUTH
           (gl:color 0.0 0.0 0.4))
          (:EAST
           (gl:color 0.0 0.4 0.0)))
        (gl:color 0.1 0.1 0.1))
    (gl:vertex (the float v1x)
           (the float v1y)
           (the float v1z))
    (gl:vertex (the float v2x)
           (the float v2y)
           (the float v2z))
    (gl:vertex (the float v3x)
           (the float v3y)
           (the float v3z))
    (gl:vertex (the float v4x)
           (the float v4y)
           (the float v4z)))

      (gl:color 1.0 1.0 1.0)
      (gl:with-primitive :line-loop
    (gl:vertex (the float v1x)
           (the float v1y)
           (the float v1z))
    (gl:vertex (the float v2x)
           (the float v2y)
           (the float v2z))
    (gl:vertex (the float v3x)
           (the float v3y)
           (the float v3z))
    (gl:vertex (the float v4x)
           (the float v4y)
           (the float v4z)))))))))

零)

4

2 回答 2

8

consed内存是由分配浮点数引起的每个函数调用都返回浮点数,实际上是 32 位single-floatsConsing表示在堆上分配了一些数据:cons 单元格、数字、数组、...

Asingle-float是 32 位内存对象。4字节。

(+ 1.0 2.0)  ->  3.0

在上述情况下3.0是一个新的浮点数,可能是新的consed

(+ (+ 1.0 2.0) 4.0)  -> 7.0)

现在上面的计算是什么?内部+操作返回一个 float 3.0。它会发生什么?

  • 它可以在处理器寄存器中返回并在那里用于下一个操作。
  • 它可能会返回到堆栈中并在那里用于下一个操作
  • 在更复杂的操作中,它可以在堆中分配并作为指向堆值的指针返回。如果没有足够的寄存器用于所有返回值,或者堆栈帧的大小对于所有返回值都不够大,则可能会出现这种情况。

现在这些花车以后会发生什么?它们是否以某种方式存储?在列表中?在一个新的阵列中?在一个新的structure?在一个新的 CLOS 对象中?

上面清楚地表明它取决于处理器架构和编译器策略。x86 没有很多寄存器。64位版本有更多。RISC 处理器可能有更多的寄存器。那么堆栈有多大,典型的堆栈帧有多大?

对于涉及多个函数的更复杂的计算,优化编译器可能能够优化哪些值保留在寄存器中,从而减少浪费。

上面还清楚地表明,对于 Common Lisp,没有完全通用的方法来使浮点操作成为非 consing。减少 consing 的能力取决于一些一般的想法和许多编译器/架构特定的技巧

由于您使用的是 SBCL,因此最好在 SBCL 邮件列表上寻求建议,并告诉他们操作系统、架构(intel、arm、...)以及它是在 32 位还是 64 位模式下运行。还需要更多的上下文代码来更好地了解如何减少 consing。

一些阅读背景信息:

于 2012-09-01T18:01:42.870 回答
0

编译器说什么?如果您针对速度进行优化,它应该大声抱怨无法打开代码算术。

接下来,胁迫发生了什么?这也是开放编码的吗?

最后,请记住,您通常可以检查函数使用 disassemble() 生成的汇编代码

于 2012-09-01T18:04:04.570 回答