【问题标题】:Eliminating "mystery-consing" in this Common Lisp function?消除这个 Common Lisp 函数中的“神秘问题”?
【发布时间】:2012-08-27 02:53:15
【问题描述】:

这个 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 处为 'speed' 执行 'declare' 为 'optimize',在 0 处执行其他所有操作(都在此函数中,以及调用它的唯一函数)。奇怪的是,分析器确实报告了这个功能稍微少了一点……但它仍然是 consed。我的目标是零骗局。算术不应该是缺点。

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

  3. 尝试将 'case' 替换为 'cond' 等效项,此时只是抓住稻草,也没有做任何事情。

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


(EDIT) for @FaheemMitha,是使用计算墙点函数的函数;这个麻烦的函数后来在计算墙点的定义之前内联为 (declaim (inline 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)))))))))

无)

【问题讨论】:

    标签: performance common-lisp case


    【解决方案1】:

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

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

    (+ 1.0 2.0)  ->  3.0
    

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

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

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

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

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

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

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

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

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

    一些阅读背景资料:

    【讨论】:

    • 谢谢——我永远不会怀疑浮点数是罪魁祸首(我抓到了足够多的红鲱鱼来开一家海鲜餐厅)。现在知道要更好地寻找什么,我翻遍了 SBCL 的手册,它提到声明这样的“内联”浮点重函数会产生更好的性能。这似乎奏效了;现在在这个函数和调用函数中都发生了零错误(我想,现在在内部融合到同一个函数中,编译后)。
    • @valq:嗨。我对你为阻止函数 consing 所做的工作的细节感兴趣。您能否在问题中或作为单独的答案发布您的优化前后代码示例?谢谢。
    • @FaheemMitha 几乎在做了一个 (declaim (inline calculate-wall-points)) 之后,我记得我在探查器和使用的函数之间看到了非常低的 consing(使用)它。我会将代码发布到在单独答案中使用它的函数。我不知道您是否需要更多代码来处理它,因为它还引用了几个函数和超出其范围的全局变量。
    • @FaheemMitha 我还应该补充一点,我现在处理这样的“计算多个事物”的场景与我在 9 月份提出这个问题时所做的不同;我没有返回带有“值”的多值事物,而是使用这样的函数将输出放入预期已经存在的输出对象(如数组或结构)中,并在函数中尽可能多地声明类型以及在传递的输出对象的声明中——SBCL 似乎在优化完全声明类型的代码方面做得相对不错(尽管仍然不是 100% 完美)。
    • @valq 感谢您的指点。我实际上不需要测试可运行的代码——这对你要求太高了。我只是想更清楚地了解您使用的技术。令我惊讶的是,简单的内联对您产生了如此大的影响。尽管我从来没有过多关注过consing问题,但对我来说并没有明显的速度差异。也感谢有关分配给预先存在的对象的提示。当您说in the declaration of the passed output object 时,您的意思是在被调用函数之外创建对象时?
    【解决方案2】:

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

    接下来,强制发生了什么?这也是开源的吗?

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-08-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-24
      相关资源
      最近更新 更多