una todas parametros llamar las funciones funcion estructura ejemplos dev con como codigos c++ performance this language-lawyer member-functions

todas - ¿Cada función miembro de c++ toma `this` como una entrada implícitamente?



funciones en c++ ejemplos (4)

Cuando creamos una función miembro para una clase en c ++, tiene un argumento adicional implícito que es un puntero al objeto que llama, denominado this .

¿Es esto cierto para cualquier función, incluso si no usa this puntero? Por ejemplo, dada la clase.

class foo { private: int bar; public: int get_one() { return 1; // Not using `this` } int get_bar() { return this->bar; // Using `this` } }

¿Las dos funciones ( get_one y get_bar ) tomarían this como un parámetro implícito, aunque solo una de ellas lo use realmente?
Parece un poco inútil hacerlo.

Nota : entiendo que lo correcto sería hacer get_one() estático, y que la respuesta puede depender de la implementación, pero tengo curiosidad.


¿Las dos funciones (get_one y get_bar) tomarían esto como un parámetro implícito aunque solo lo use onle get_bar?

Sí (a menos que el compilador lo optimice, lo que aún no significa que pueda llamar a la función sin un objeto válido).

Parece un poco inútil hacerlo

Entonces, ¿por qué es un miembro si no utiliza ningún dato del miembro? A veces, el enfoque correcto es convertirlo en una función gratuita en el mismo espacio de nombres.


... clase en c ++, tal como lo entiendo, tiene un argumento adicional implícito que es un puntero al objeto llamante

Es importante tener en cuenta que C ++ comenzó como C con objetos.

Para eso, el puntero de this no es uno que está implícitamente presente dentro de una función miembro, sino que la función miembro, cuando se compila, necesita una forma de saber a qué se refiere esto; por lo tanto, la noción de un puntero implícito al objeto llamante que se pasa.

Para decirlo de otra manera, tomemos tu clase de C ++ y hagamos una versión C:

C ++

class foo { private: int bar; public: int get_one() { return 1; } int get_bar() { return this->bar; } int get_foo(int i) { return this->bar + i; } }; int main(int argc, char** argv) { foo f; printf("%d/n", f.get_one()); printf("%d/n", f.get_bar()); printf("%d/n", f.get_foo(10)); return 0; }

do

typedef struct foo { int bar; } foo; int foo_get_one(foo *this) { return 1; } int foo_get_bar(foo *this) { return this->bar; } int foo_get_foo(int i, foo *this) { return this->bar + i; } int main(int argc, char** argv) { foo f; printf("%d/n", foo_get_one(&f)); printf("%d/n", foo_get_bar(&f)); printf("%d/n", foo_get_foo(10, &f)); return 0; }

Cuando el programa C ++ se compila y ensambla, this puntero se "agrega" a la función mutilada para "saber" qué objeto está llamando la función miembro.

Entonces foo::get_one podría ser "destrozado" al equivalente en C de foo_get_one(foo *this) , foo::get_bar podría ser foo_get_bar(foo *this) a foo_get_bar(foo *this) y foo::get_foo(int) podría ser foo_get_foo(int, foo *this) , etc.

¿Las dos funciones ( get_one y get_bar ) tomarían esto como un parámetro implícito aunque solo un get_bar use? Parece un poco inútil hacerlo.

Esta es una función del compilador y si no se realizaron optimizaciones en absoluto, las heurísticas aún podrían eliminar this puntero en una función mutilada donde no es necesario llamar a un objeto (para guardar la pila), pero eso depende en gran medida del código y de cómo Se está compilando y en qué sistema.

Más específicamente, si la función fuera tan simple como foo::get_one (simplemente devolviendo un 1 ), es probable que el compilador simplemente ponga la constante 1 en lugar de la llamada a object->get_one() , eliminando la necesidad de cualquier Referencias / punteros.

Espero que pueda ayudar.


Si no usas this , entonces no puedes decir si está disponible. Así que literalmente no hay distinción. Esto es como preguntar si un árbol que cae en un bosque despoblado emite un sonido. Es literalmente una pregunta sin sentido.

Te puedo decir esto: si quieres usar this en una función miembro, puedes hacerlo. Esa opción siempre está disponible para usted.


Semánticamente, this puntero siempre está disponible en una función miembro, como lo señaló otro usuario. Es decir, podría luego cambiar la función para usarla sin problema (y, en particular, sin la necesidad de volver a compilar el código de llamada en otras unidades de traducción) o, en el caso de una función virtual , se podría usar una versión anulada en una subclase. this incluso si la implementación base no lo hizo.

Entonces, la pregunta interesante que queda es qué impacto impone el rendimiento , si lo hay. Puede haber un costo para la persona que llama y / o la persona que recibe la llamada y el costo puede ser diferente cuando está en línea y no en línea. Examinamos todas las permutaciones a continuación:

En línea

En el caso en línea , el compilador puede ver tanto el sitio de la llamada como la implementación de la función 1 , por lo que presumiblemente no necesita seguir ninguna convención de llamada en particular y, por lo tanto, el costo del puntero oculto debería desaparecer. Tenga en cuenta también que, en este caso, no hay una distinción real entre el código del "destinatario" y el código "llamado", ya que se combinan en forma óptima en el sitio de la llamada.

Usemos el siguiente código de prueba:

#include <stdio.h> class foo { private: int bar; public: int get_one_member() { return 1; // Not using `this` } }; int get_one_global() { return 2; } int main(int argc, char **) { foo f = foo(); if(argc) { puts("a"); return f.get_one_member(); } else { puts("b"); return get_one_global(); } }

Tenga en cuenta que las dos llamadas de llamada están ahí para hacer que las ramas sean un poco más diferentes; de lo contrario, los compiladores son lo suficientemente inteligentes como para usar solo un conjunto / movimiento condicional, por lo que ni siquiera pueden separar los cuerpos en línea de las dos funciones.

Todo gcc , icc y clang integran las dos llamadas y generan un código que es equivalente tanto para la función miembro como para la función no miembro, sin ningún rastro de this puntero en el caso del miembro. Veamos el código de clang , ya que es el más limpio:

main: push rax test edi,edi je 400556 <main+0x16> # this is the member case mov edi,0x4005f4 call 400400 <puts@plt> mov eax,0x1 pop rcx ret # this is the non-member case mov edi,0x4005f6 call 400400 <puts@plt> mov eax,0x2 pop rcx ret

Ambas rutas generan exactamente la misma serie de 4 instrucciones previas a la última ret : dos instrucciones para la llamada a las llamadas, una sola instrucción para mov el valor de retorno de 1 o 2 a eax , y un pop rcx para limpiar la pila 2 . Por lo tanto, la llamada real tomó exactamente una instrucción en cualquier caso, y no hubo ninguna manipulación de puntero o pase en absoluto.

Fuera de línea

En los costos fuera de línea, el soporte de this indicador realmente tendrá algunos costos reales pero generalmente pequeños, al menos en el lado de la persona que llama.

Usamos un programa de prueba similar, pero con las funciones miembro declaradas fuera de línea y con la inclusión de esas funciones desactivadas 3 :

class foo { private: int bar; public: int __attribute__ ((noinline)) get_one_member(); }; int foo::get_one_member() { return 1; // Not using `this` } int __attribute__ ((noinline)) get_one_global() { return 2; } int main(int argc, char **) { foo f = foo(); return argc ? f.get_one_member() :get_one_global(); }

Este código de prueba es algo más simple que el anterior porque no necesita la llamada a las llamadas para distinguir las dos ramas.

Llamar al sitio

Veamos el conjunto que generates gcc 4 para main (es decir, en los sitios de llamadas para las funciones):

main: test edi,edi jne 400409 <main+0x9> # the global branch jmp 400530 <get_one_global()> # the member branch lea rdi,[rsp-0x18] jmp 400520 <foo::get_one_member()> nop WORD PTR cs:[rax+rax*1+0x0] nop DWORD PTR [rax]

En este caso, ambas llamadas de función se realizan realmente mediante jmp : este es un tipo de optimización de llamada de cola, ya que son las últimas funciones llamadas en main, por lo que el ret para la función llamada realmente regresa al llamador de main , pero aquí el llamador de La función miembro paga un precio extra:

lea rdi,[rsp-0x18]

Eso es cargar el puntero de this en la pila en rdi que recibe el primer argumento que es this para las funciones de miembro de C ++. Así que hay un costo extra (pequeño).

Cuerpo de funciones

Ahora, mientras que el sitio de llamada paga un cierto costo para pasar un puntero (sin usar), en este caso al menos, los cuerpos de funciones reales son igualmente eficientes:

foo::get_one_member(): mov eax,0x1 ret get_one_global(): mov eax,0x2 ret

Ambos están compuestos de un solo mov y un ret . Así que la función en sí misma puede simplemente ignorar this valor, ya que no se utiliza.

Esto plantea la cuestión de si esto es cierto en general: ¿el cuerpo de la función de una función miembro que no usa this siempre se compilará tan eficientemente como una función no miembro equivalente?

La respuesta corta es no , al menos para la mayoría de las ABI modernas que pasan argumentos en los registros. El puntero de this toma ocupa un registro de parámetros en la convención de llamada, por lo que alcanzará el número máximo de argumentos de registro pasado un parámetro antes al compilar una función miembro.

Tomemos, por ejemplo, esta función que simplemente suma sus seis parámetros int juntos:

int add6(int a, int b, int c, int d, int e, int f) { return a + b + c + d + e + f; }

Cuando se compila como una función miembro en una plataforma x86-64 utilizando el SysV ABI , tendrá que pasar el registro en la pila para la función miembro, lo que resultará en un código como este :

foo::add6_member(int, int, int, int, int, int): add esi,edx mov eax,DWORD PTR [rsp+0x8] add ecx,esi add ecx,r8d add ecx,r9d add eax,ecx ret

Tenga en cuenta la lectura de la pila eax,DWORD PTR [rsp+0x8] que generalmente agregará algunos ciclos de latencia 5 y una instrucción en gcc 6 en comparación con la versión no miembro, que no tiene lectura de memoria:

add6_nonmember(int, int, int, int, int, int): add edi,esi add edx,edi add ecx,edx add ecx,r8d lea eax,[rcx+r9*1] ret

Por lo general, no tendrá seis o más argumentos para una función (especialmente los muy cortos, sensibles al rendimiento), pero al menos esto demuestra que incluso en el lado de la generación de código del interlocutor, this puntero oculto no siempre es gratuito.

Tenga en cuenta también que si bien los ejemplos utilizaron el código x86-64 y el SysV ABI, los mismos principios básicos se aplicarían a cualquier ABI que pase algunos argumentos en los registros.

1 Tenga en cuenta que esta optimización solo se aplica fácilmente a funciones efectivamente no virtuales, ya que solo entonces el compilador puede conocer la implementación de la función real.

2 Supongo que para eso está: deshace el push rax en la parte superior del método para que rsp tenga el valor correcto en el retorno, pero no sé por qué el par push/pop debe estar allí en primer lugar . Otros compiladores utilizan diferentes estrategias, como add rsp, 8 y sub rsp,8 .

3 En la práctica, realmente no se va a deshabilitar la inline así, pero la falla en la línea podría ocurrir solo porque los métodos están en diferentes unidades de compilación. Debido a la forma en que funciona Godbolt, no puedo hacer eso exactamente, por lo que deshabilitar el ingreso en línea tiene el mismo efecto.

4 Por extraño que parezca, no pude hacer que el clang deje de incluir ninguna de las funciones, ya sea con el atributo noinline o con -fno-inline .

5 De hecho, a menudo algunos ciclos superan la latencia habitual de L1 de 4 ciclos en Intel, debido al reenvío de la tienda del valor escrito recientemente.

6 En principio, al menos en x86, la penalización de una sola instrucción puede eliminarse mediante el uso de un add con un operando de origen de memoria, en lugar de un mov de la memoria con un add registro posterior y, de hecho, clang y clang hacen exactamente eso. Sin embargo, no creo que un enfoque domine: el enfoque de gcc con un movimiento separado es más capaz de mover la carga de la ruta crítica: iniciarla temprano y luego usarla solo en la última instrucción, mientras que el enfoque icc agrega 1 ciclo a La ruta crítica que involucra el enfoque de mov y clang parece la peor de todas: unir todos los agregados en una larga cadena de dependencia en eax que termina con la lectura de la memoria.