2009-11-20 15 views
5

Cuando una función C++ acepta un argumento std::vector, el patrón habitual es pasar por const de referencia, tales como:paso eficiente de std :: vector

int sum2(const std::vector<int> &v) 
{ 
    int s = 0; 
    for(size_t i = 0; i < v.size(); i++) s += fn(v[i]); 
    return s; 
} 

Creo que los resultados de este código en doble desreferencia cuando se accede a los elementos del vector, ya que la CPU debe primero desreferenciar v para leer el puntero al primer elemento, cuyo puntero necesita desreferenciarse nuevamente para leer el primer elemento. Esperaría que sería más eficiente pasar una copia superficial del objeto vector en la pila. Dicha copia superficial encapsularía un puntero al primer elemento y el tamaño, con el puntero haciendo referencia a la misma área de memoria que el vector original.

int sum2(vector_ref<int> v) 
{ 
    int s = 0; 
    for(size_t i = 0; i < v.size(); i++) s += fn(v[i]); 
    return s; 
} 

rendimiento similar, pero mucho menos de conveniencia podrían lograrse haciendo pasar un par iterador de acceso aleatorio. Mi pregunta es: ¿qué tiene defectos con esta idea? Espero que haya alguna buena razón para que las personas inteligentes acepten pagar el costo de rendimiento de la referencia de vector, o que hagan frente a la inconveniencia de los iteradores.

Editar: Con base en los comentarios a continuación, ten en cuenta la situación si simplemente cambiar el nombre de la clase sugerido vector_ref a rebanada o gama. La intención es usar pares de iteradores de acceso aleatorio con una sintaxis más natural.

+5

¿Ha comparado realmente el código máquina generado en los dos casos? ¿O midió el rendimiento? –

+0

¿Ha comprobado que efectivamente es así en el montaje? Tampoco creo que la convención de los pares de iteradores provenga de consideraciones de rendimiento, es una decisión de diseño (y de hecho impulsar está favoreciendo un concepto mucho más conveniente de objetos de gama de iteradores, aunque no he estudiado cómo funcionan exactamente). – UncleBens

+0

No creo que tus comentarios realmente tengan sentido. Una referencia es un alias de un objeto. Como tal, no es más costoso acceder a una referencia a un objeto que a acceder al objeto original. Esta diatriba ligeramente inflamatoria es especulación de su parte, si hubiera hecho dos minutos de debida diligencia, no habría necesitado esta publicación. –

Respuesta

10

Creo que este código da como resultado el doble desreferenciar cuando se accede a los elementos del vector

No necesariamente. Los compiladores son bastante inteligentes y deberían ser capaces de eliminate common subexpressions.. Pueden ver que el operador [] no cambia el 'puntero al primer elemento', por lo que no es necesario que la CPU lo recargue de la memoria para cada ciclo de iteración.

+1

Excelente punto. –

+0

OK, por lo que los compiladores teóricamente pueden disminuir el número de dobles derecesiones a 1 por cada llamada de este método. ¿No sería un 0 asegurado mejor? – shojtsy

+2

Calcule sus costos en términos de acceso a la memoria, no la cantidad de desreferencias. En su 'copia superficial' propuesta estaría haciendo el acceso a la memoria en la persona que llama en lugar del destinatario, por lo que el resultado neto sería (en el mejor de los casos) el mismo. De hecho, si el compilador no alinea la función, probablemente sería peor, ya que le está pasando un argumento más grande. – int3

1

Pase por valor a menos que esté seguro de que pasar por referencia mejora el rendimiento.

Cuando pasa por valor, puede producirse una elisión de copia que dará como resultado un rendimiento similar, si no mejor.

de Dave escribió sobre él aquí:

http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

+1

Yo votaría fuertemente en contra de eso. Esto no es solo una cuestión de optimización del rendimiento. Si un método no altera el contenido del vector, siempre discutiría para pasar un vector const &. Dado que la elisión de copia depende del contexto en el que se invoca su método, aún así creo que la referencia correcta es la elección correcta en la mayoría de los casos. – Sebastian

+1

Si no altera el vector, pasar por valor también es perfecto. +1 de mi parte – jalf

+1

Interesante. Tendré que leer eso más de cerca. Sin embargo, a pesar del título, parece que se centra en los retornos, no en el paso de parámetros, así que no estoy seguro de que se aplique a una situación como esta. –

4

¿Qué tiene defecto con esta idea?

Simple: Optimización prematura. Alternativas: acepte un vector<int> const& y use iteradores o pase iteradores directamente a la función.

+0

¿Es una optimización prematura cuando el código es más simple que la versión ingenua? ¿Por qué utilizar iteradores cuando podemos crear una sintaxis más natural con el mismo rendimiento que los iteradores? – shojtsy

+0

Porque podría haber pasado el iterador original, por valor o por referencia de referencia, y confiar en el compilador para optimizar su código. En muchos casos, el compilador puede eliminar el doble indirecto. Y en los casos en que no puede hacer eso, la diferencia en el rendimiento casi seguro no importará *. – jalf

+1

Un par de iteradores * es * ya la "copia superficial que desea". Además, es una optimización prematura porque es probable que estés buscando en el lugar equivocado para mejorar el rendimiento. Mida primero, luego tome medidas. – sellibitze

1

No hay doble desreferenciación porque el compilador probablemente pasará el puntero real al vector como argumento y no a un puntero a un puntero. Simplemente puede probar esto y consultar la vista en el desmontaje de su IDE de lo que realmente está pasando detrás de las escenas:

void Method(std::vector<int> const& vec) { 
int i = vec.back(); 
} 


void SomeOtherMethod() { 
    std::vector<int> vec; 
    vec.push_back(1); 
    Method(vec); 
} 

¿Qué pasa aquí? El vector está asignado en la pila.La primera vuelta de empuje se traduce a:

push  eax // this is the constant one that has been stored in eax 
lea   ecx,[ebp-24h] // ecx is the pointer to vec on the stack 
call  std::vector<int,std::allocator<int> >::push_back 

Ahora que llamamos Método(), pasando el vector const &:

lea   ecx,[ebp-24h] 
push  ecx 
call  Method (8274DC0h) 

Como era de esperar, el puntero al vector se pasa como referencias no son más que de forma permanente punteros desreferenciados Ahora dentro Método(), el vector se accede de nuevo:

mov   ecx,dword ptr [ebp+8] 
call  std::vector<int,std::allocator<int> >::back (8276100h) 

El puntero vector se toma directamente de la pila y se escribe en ecx.

+2

Para hacer eso, el compilador tendría que cambiar la firma de la función, lo que parece poco probable. Cambiar la firma rompería las llamadas de las funciones. Es posible si tienes generación de código de tiempo de enlace avanzada, pero no estoy seguro de si hay una implementación por ahí que sea tan avanzada. –

+0

Lo he comprobado. No hay doble desreferenciación. – Sebastian

+2

El vector contiene un puntero a los datos reales, por lo que pasar un puntero al vector está pasando un puntero al puntero. La segunda desreferencia ocurre en la llamada a back(). Si pasó un iterador en su lugar, entonces (en la mayoría de las implementaciones estaría pasando un puntero directamente a los datos contenidos en el vector, eliminando así un nivel de indirección. –

2

Tiene razón en que hay una indirecta adicional aquí. Es concebible (aunque sería sorprendente) si el compilador (con la ayuda de la generación de código de tiempo de enlace) lo optimizara.

Lo que ha propuesto se denomina a veces slicing, y se usa extensamente en algunas situaciones. Aunque, en general, no estoy seguro de que valga la pena los peligros. Debes tener mucho cuidado al invalidar tu porción (o la de otra persona).

Tenga en cuenta que si se ha utilizado iteradores para el bucle en lugar de indexación, entonces lo que DEREF la referencia solamente un par de veces (para llamar begin() y end()) en lugar de n veces (para indexar en el vector).

int sum(const vector<int> &v) 
{ 
    int s = 0; 
    for (auto it = v.begin(); it != v.end(); ++it) { 
     s += fn(*it); 
    } 
    return s; 
} 

(estoy suponiendo que el optimizador izar las end() llamadas fuera del circuito. Usted puede hacerlo de forma explícita para estar seguro.)

Pasar un par de iteradores en lugar del propio recipiente parece como el lenguaje STL. Eso le daría más generalidad, ya que el tipo de contenedor puede variar, pero también lo puede hacer la cantidad de referencias necesarias.

9

¿Qué pasa con su idea es que ya tiene dos perfectamente buenas soluciones:

  • pase el vector como es, ya sea por el valor (en la que el compilador menudo eliminará la copia), o por (const) referencia, y confíe en el compilador para eliminar el doble direccionamiento indirecto, o
  • Pase un par de iteradores.

Por supuesto, puede argumentar que el par de iteradores es "sintaxis menos natural", pero no estoy de acuerdo. Es perfectamente natural para cualquiera que esté acostumbrado a STL. Es eficiente y le proporciona exactamente lo que necesita para trabajar con el rango, utilizando algoritmos std o sus propias funciones.

Los pares de iteradores son una expresión común de C++, y un programador de C++ que lea su código los comprenderá sin problemas, mientras que se sorprenderán con sus envoltorios de vectores elaborados en casa.

Si eres realmente paranoico sobre el rendimiento, pasa el par de iteradores. Si la sintaxis realmente te molesta, pasa el vector y confía en el compilador.

Cuestiones relacionadas