2009-08-21 12 views
7

Estoy implementando algunos tipos matemáticos y quiero optimizar los operadores para minimizar la cantidad de memoria creada, destruida y copiada. Para demostrar, te mostraré parte de mi implementación de Quaternion.retorno por valor funciones en línea

class Quaternion 
{ 
public: 
    double w,x,y,z; 

    ... 

    Quaternion operator+(const Quaternion &other) const; 
} 

Quiero saber cómo las dos implementaciones siguientes difieren entre sí. Tengo una implementación + = que opera en el lugar donde no se crea memoria, pero algunas operaciones de nivel superior que utilizan cuaterniones es útil usar + y no + =.

__forceinline Quaternion Quaternion::operator+(const Quaternion &other) const 
{ 
    return Quaternion(w+other.w,x+other.x,y+other.y,z+other.z); 
} 

y

__forceinline Quaternion Quaternion::operator+(const Quaternion &other) const 
{ 
    Quaternion q(w+other.w,x+other.x,y+other.y,z+other.z); 
    return q; 
} 

Mi C++ es completamente autodidacta por lo que cuando se trata de algunas optimizaciones, estoy seguro de qué hacer, porque no sé exactamente cómo el compilador se encarga de estas cosas. Además, ¿cómo se traduce esta mecánica a implementaciones no en línea?

Cualquier otra crítica de mi código es bienvenida.

+2

No sé de dónde viene __forceinline, pero ciertamente no es estándar C++ –

+0

es específico del compilador. Simplemente obliga al compilador a hacerlo en línea. – Mark

+0

No hay nada que podamos decir sobre esto sin saber cuál es tu compilador, ya que todo depende del compilador. –

Respuesta

10

Su primer ejemplo permite que el compilador utilice potencialmente algo llamado "Optimización del valor de retorno" (RVO).

El segundo ejemplo permite que el compilador utilice potencialmente algo llamado "Optimización de valor de devolución con nombre" (NRVO). Estas 2 optimizaciones están claramente relacionadas.

Algunos detalles de la implementación de NRVO de Microsoft se pueden encontrar aquí:

Tenga en cuenta que el artículo indica que el apoyo NRVO comenzó con VS 2005 (MSVC 8,0). No especifica si se aplica lo mismo a RVO o no, pero creo que MSVC utilizó optimizaciones de RVO antes de la versión 8.0.

This article about Move Constructors by Andrei Alexandrescu tiene buena información sobre cómo funciona RVO (y cuándo y por qué los compiladores pueden no usarlo).

Incluyendo este bit:

usted estará decepcionado al saber que cada compilador, y con frecuencia cada versión del compilador, tiene sus propias reglas para detectar y aplicar RVO.Algunos aplican RVO solo a funciones que devuelven temporales no nombrados (la forma más simple de RVO). Los más sofisticados también aplican RVO cuando hay un resultado nombrado que devuelve la función (el llamado RVO con nombre, o NRVO).

Básicamente, al escribir código, puede contar con que RVO se aplique de forma portátil a su código, dependiendo de cómo escriba exactamente el código (bajo una definición muy fluida de "exactamente"), la fase de la luna y el tamaño de tus zapatos

El artículo fue escrito en 2003 y los compiladores deberían mejorarse mucho hasta ahora; con suerte, la fase de la luna es menos importante que cuando el compilador puede usar RVO/NRVO (tal vez es hasta el día de la semana). Como se señaló anteriormente, parece que MS no implementó NRVO hasta 2005. Tal vez fue entonces cuando alguien que trabajaba en el compilador de Microsoft obtuvo un nuevo par de zapatos más cómodos, de un tamaño un poco más grande que antes.

Sus ejemplos son lo suficientemente simples como para esperar que ambos generen un código equivalente con versiones de compilador más recientes.

+2

Con los compiladores actuales, ambos fragmentos tienen casi las mismas posibilidades de tener la optimización en su lugar. Hoy en día, lo que es realmente difícil para los compiladores si aplica RVO cuando el código tiene más de una declaración de retorno (es decir, con diferentes objetos) como en ese caso, es más difícil para el compilador determinar cuál de las diferentes instancias debería construirse el espacio de memoria de retorno. –

+1

También hay algunos otros pequeños cambios que pueden ayudar al compilador a optimizar y nunca incurrirá en un costo adicional como usar versiones de funciones gratuitas del operador + que toman el primer argumento por valor.En este caso, el compilador puede elide la construcción/construcción de copia si la función se llama con un temporal. –

6

Entre las dos implementaciones que presentó, realmente no hay diferencia. Cualquier compilador que haga cualquier tipo de optimización optimizará su variable local.

En cuanto al operador + =, es probable que se requiera un debate un poco más complicado sobre si desea o no que sus Quaternions sean objetos inmutables ... Siempre me gustaría crear objetos como este como objetos inmutables. (pero, de nuevo, también soy un codificador administrado)

+1

Y siempre me inclino a hacerlos mutables, sobre la base de que en C++, los tipos numéricos definidos por el usuario deben (en la medida de lo posible) imitar int. En, por ejemplo, Java es una historia diferente, todas tus variables son referencias y los tipos inmutables son más apropiados. –

2

Si estas dos implementaciones no generan exactamente el mismo código de ensamblado cuando se activa la optimización, debe considerar el uso de un compilador diferente. :) Y no creo que importe si la función está en línea o no.

Por cierto, tenga en cuenta que __forceinline es muy no portátil. Simplemente usaría el antiguo estándar simple inline y dejaría que el compilador decidiera.

+0

Well atm No tengo que preocuparme por la portabilidad porque soy el único que maneja este código. Probablemente debería usar una macro, pero no estoy demasiado preocupado por eso ahora. – Mark

+3

Primero, escribir un código portátil siempre que sea posible es generalmente una buena costumbre. En segundo lugar, incluso si nadie más mira su código, es posible que desee utilizarlo en una plataforma diferente o con un compilador diferente algún día. Obligar a una función a estar en línea, cuando la mayoría de los compiladores hacen un buen trabajo por sí mismos no parece ser una buena razón para usar una función no estándar. Solo mis 2 centavos. :) – Dima

+0

Estoy completamente de acuerdo con sus dos centavos, era demasiado perezoso para hacer eso allí. Lo cambiaré ahora si te hace feliz =] – Mark

2

El consenso actual es que debe implementar primero todos sus operadores = que no creen objetos nuevos. Dependiendo de si la seguridad de la excepción es un problema (en su caso, probablemente no lo es) o un objetivo, la definición de operador = puede ser diferente. Después de eso, ¿implementar operador? como una función libre en términos del operador? = usando semántica pass-by-value.

// thread safety is not a problem 
class Q 
{ 
    double w,x,y,z; 
public: 
    // constructors, other operators, other methods... omitted 
    Q& operator+=(Q const & rhs) { 
     w += rhs.w; 
     x += rhs.x; 
     y += rhs.y; 
     z += rhs.z; 
     return *this; 
    } 
}; 
Q operator+(Q lhs, Q const & rhs) { 
    lhs += rhs; 
    return lhs; 
} 

Esto tiene las siguientes ventajas:

  • Sólo una implementación de la lógica. Si la clase cambia solo necesita volver a implementar el operador? = Y el operador? se adaptará automáticamente
  • El operador de función libre es simétrico con respecto a las conversiones de compilador implícitas
  • ¿Es la implementación más eficiente del operador? usted puede encontrar con respecto a las copias

¿Eficiencia del operador?

¿Cuándo llamas al operador? en dos elementos, se debe crear y devolver un tercer objeto. Usando el enfoque anterior, la copia se realiza en la llamada al método. Tal como está, el compilador puede elidear la copia cuando está pasando un objeto temporal. Tenga en cuenta que esto debe leerse como 'el compilador sabe que puede elide la copia', no como 'el compilador será elide la copia'. El kilometraje variará con los diferentes compiladores, e incluso el mismo compilador puede arrojar resultados diferentes en diferentes ejecuciones de compilación (debido a diferentes parámetros o recursos disponibles para el optimizador).

En el siguiente código, un temporal se creará con la suma de a y b, y que temporalmente se deben pasar de nuevo a operator+ junto con c para crear un segundo temporal con el resultado final:

Q a, b, c; 
// initialize values 
Q d = a + b + c; 

Si operator+ tiene una semántica de paso por valor, el compilador puede elide la copia de valor por paso (el compilador sabe que el temporal será destruido inmediatamente después de la segunda llamada operator+, y no necesita crear una copia diferente para pasar)

Incluso si el operator? pudiera implementarse como una función de línea (Q operator+(Q lhs, Q const & rhs) { return lhs+=rhs; }) en el código, no debería ser así. La razón es que el compilador no puede saber si la referencia devuelta por operator?= es de hecho una referencia al mismo objeto o no. Al hacer que la declaración return explícitamente tome el objeto lhs, el compilador sabe que la copia devuelta puede ser eliminada.

simetría con respecto a los tipos de

si hay una conversión implícita de tipo T para escribir Q, y tiene dos instancias t y q, respectivamente, de cada tipo, entonces se esperaría (t+q) y (q+t) tanto estar invocable Si implementa operator+ como una función miembro dentro de Q, el compilador no podrá convertir el objeto t en un objeto temporal Q y luego llamará al (Q(t)+q) ya que no puede realizar conversiones de tipo en el lado izquierdo para llamar a una función miembro. Por lo tanto, con una función miembro, la implementación t+q no se compilará.

Tenga en cuenta que esto también es cierto para los operadores que no son simétricos en términos aritméticos, estamos hablando de tipos. Si puede sustraer un T desde un Q promocionando el T a un , entonces no hay razón para no poder sustraer un Q de un T con otra promoción automática.

Cuestiones relacionadas