2012-09-01 22 views
8

Esta función Common Lisp, que simplemente calcula cuatro vértices de los bordes de un alambre de una pared con una aritmética de nivel de jardín de infantes extremadamente simple y algunas pruebas de 'caso' parece ser responsable de asignar dinámicamente 196608 bytes por cuadro renderizado; El perfilador de SBCL me dice que esta es mi función más problemática en lo que concierne a consing. Para dar una idea general de en qué estoy trabajando, se trata de un pequeño juego de mazmorra de calabozo en primera persona, y una mazmorra tiene exactamente 32x32 celdas, y cada celda tiene 4 paredes. 32 * 32 * 4 * x = 196608, y entonces x resulta ser 48, que resulta ser 4 * 12 (4 paredes * 12 flotadores por pared? Tal vez no).¿Eliminando el "misterio-consing" en esta función Common Lisp?

Ahora, puedo mitigar fácilmente este problema de rendimiento mediante el uso de listas de visualización de OpenGL en el modo de juego, y supongo que eso es lo que seguiré haciendo. Aún así, 1) Generalmente no optimizo prematuramente, y más importante aún 2) Todavía no me gusta dejar ciertos rasguños molestos como este sin rayar, y me pregunto qué más podría haber hecho. Mi función como está, es como sigue:

(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))))) 

Para un resumen de algunas de las cosas que he tratado de solucionar este problema:

  1. Haciendo un 'declare' a 'Optimizar' para la 'velocidad' en 3 y todo lo demás en 0 (ambos en esta función, así como la única función que lo llama). Curiosamente, el generador de perfiles sí informó esta función con un poco menos de ... pero aún así consed. Estoy apuntando a cero-consing. La aritmética no debe contra.

  2. Luego pensé 'valores' podría estar haciendo esto. Quizás, pensé, es internamente algo así como la función 'lista', que, sin duda, considera (el único propósito de la función 'lista' en el universo). ¿Qué hice para tratar de mitigar esto? Solo por el experimento, modifiqué el archivo para crear una única matriz global wall-vertex-buffer, dimensionada para caber en 12 elementos de tipo float, y modifiqué esta función para modificarla, y la función de llamada para leerla después llamando a esta función (por lo que constantemente estaría actualizando un solo conjunto de 12 flotadores guardados en el mismo lugar en la memoria, en lugar de asignar cualquier cosa). ¡Extrañamente, esto NO detuvo esta función de ser un cerdito! Entonces ... ¿estaba 'case' haciendo el consing? Me parece interesante que, mencionado anteriormente, ese número misterioso era 48. 48 = 4 * 12, tal vez esas 4 pruebas de casos multiplicadas por 12 flotantes por llamada de 'valores'. O bien, eso podría ser una coincidencia, con 48 bytes que significan algo más (ya que un flotador NO es de 1 byte, sospecho que es -es- algo más). Esto parece significativo, pero no puedo entender bien cuál debería ser mi próximo enfoque.

  3. Intenté reemplazar 'case' con un equivalente 'cond', simplemente agarrar pajitas en este punto, tampoco hice nada.

Entonces, ¿de dónde podría venir este "misterio consing" de esta función? ¿Cómo podrían los programadores de Lisp más experimentados abordar este engañoso gremlin de un problema?


(EDIT) para @FaheemMitha, es la función que utiliza la función de calcular de pared puntos; esa función problemático fue posteriormente inline con (declamar (en línea cálculo de pared puntos)) justo antes de la definición de calcular de pared puntos:

(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))))))))) 

nil)

Respuesta

9

El consed memoria es causada por la asignación de la flota. Cada llamada de función devuelve flotantes, en realidad 32 bit single-floats.Consing significa que algunos datos se asigna en el montón: cons cells, números, matrices, ...

A single-float es objeto de memoria de 32 bits. 4 bytes.

(+ 1.0 2.0) -> 3.0 

En caso 3.0 anterior es un nuevo baño de inmersión, posiblemente recién Consed.

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

Ahora, ¿qué hay arriba del cálculo anterior? La operación interna + devuelve un flotador 3.0. ¿Qué le sucede?

  • puede devolverse en un registro de procesador y utilizarse allí para la siguiente operación.
  • puede devolverse en la pila y utilizarse allí para la siguiente operación
  • en una operación más compleja, puede asignarse en el montón y devolverse como un puntero a un valor de almacenamiento dinámico. Este puede ser el caso si no hay suficientes registros para todos los valores devueltos o si el tamaño del marco de la pila no es lo suficientemente grande para todos los valores devueltos.

Ahora, ¿qué ocurre con estas carrozas después? ¿Están almacenados de alguna manera? En una lista? En una nueva matriz? En un nuevo structure? En un nuevo objeto CLOS?

Más arriba deja en claro que depende de la arquitectura del procesador y la estrategia del compilador. Un x86 no tiene muchos registros. La versión de 64 bits tiene más. Un procesador RISC puede tener aún más registros. Entonces, ¿qué tan grande es la pila y qué tan grandes son los marcos de pila típicos?

Para cálculos más complejos que implican varias funciones, un compilador de optimización podría optimizar qué valores permanecen en los registros y así reducir el consumo.

Arriba también deja en claro que, para Common Lisp, no existe una receta completamente general para hacer que las operaciones de flotación no sean condescendientes. La capacidad de reducir el consumo depende de algunas ideas generales y muchos compiladores/arquitectura específicos trucos.

Como está utilizando SBCL, es mejor pedir consejo en la lista de correo de SBCL y también informarles sobre el sistema operativo, la arquitectura (intel, arm, ...) y si se está ejecutando en modo de 32 bits o de 64 bits. También se necesita más código de contexto para obtener una mejor imagen de cómo reducir el consumo.

Parte de la información de fondo para la lectura:

+1

Gracias - Yo nunca habría sospechado que los números de punto flotante, de todas las cosas, era el culpable (Cogí lo suficientemente arenques rojos para abrir un restaurante de mariscos). Ahora, sabiendo qué buscar mejor, busqué el manual de SBCL y mencioné que declamar funciones float-pesadas "en línea" como esta produciría un mejor rendimiento. Eso pareció funcionar; ahora hay cero consing en esta función, así como en la función de llamada (que, supongo, ahora están fusionados internamente en la misma función, compilación posterior). – valq

+0

@valq: Hola. Me interesan los detalles de lo que hizo para detener la función consing. ¿Podría publicar ejemplos de su código de pre y post optimización, ya sea en la pregunta o como una respuesta separada? Gracias. –

+0

@FaheemMitha Más o menos después de hacer un (declaim (en línea calcular puntos de la pared)), recuerdo que vi mucho menos consing en el generador de perfiles entre él y la función que lo usa (lo usó). Publicaré el código en la función que lo usó en una respuesta separada. No sé si necesitará aún más código para meterse con él, ya que también hace referencia a un par de funciones y variables globales más allá de su alcance. – valq

1

¿Qué dice el compilador? Si optimiza la velocidad, debería quejarse en voz alta por no poder abrir la aritmética.

A continuación, ¿qué está pasando con la coacción? ¿Esto también está codificado?

Por último, recuerda que por lo general puede inspeccionar el código ensamblador genera una función con desensamblar()