Uno de los problemas de pimpl es la penalización de rendimiento de usarlo (asignación de memoria adicional, miembros de datos no contiguos, indirecciones adicionales, etc.). Me gustaría proponer una variación en el lenguaje de pimpl que evitará estas penalizaciones de rendimiento a expensas de no obtener todos los beneficios de pimpl. La idea es dejar a todos los miembros de datos privados en la clase misma y mover solo los métodos privados a la clase pimpl. El beneficio en comparación con el pimpl básico es que la memoria permanece contigua (sin indirección adicional). Los beneficios en comparación con no usar pimpl son:
- Oculta las funciones privadas.
- Puede estructurarlo para que todas estas funciones tengan un enlace interno y permitan que el compilador lo optimice de manera más agresiva.
Entonces, mi idea es hacer que el pimpl herede de la clase en sí (suena un poco loco, lo sé, pero tengan paciencia conmigo). Se vería algo así:
En el archivo Ah:
class A
{
A();
void DoSomething();
protected: //All private stuff have to be protected now
int mData1;
int mData2;
//Not even a mention of a PImpl in the header file :)
};
En el archivo A.cpp:
#define PCALL (static_cast<PImpl*>(this))
namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
static_assert(sizeof(PImpl) == sizeof(A),
"Adding data members to PImpl - not allowed!");
void DoSomething1();
void DoSomething2();
//No data members, just functions!
};
void PImpl::DoSomething1()
{
mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
DoSomething2();
}
void PImpl::DoSomething2()
{
mData2 = baz();
}
}
A::A(){}
void A::DoSomething()
{
mData2 = foo();
PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}
Hasta donde veo, no hay absolutamente ninguna penalización de rendimiento al usar este vs no pimpl y algunas posibles ganancias de rendimiento y una interfaz de archivo de encabezado más limpia. Una desventaja que esto tiene frente al pimpl estándar es que no puede ocultar los miembros de datos, por lo que los cambios en esos miembros de datos aún desencadenarán una recopilación de todo lo que depende del archivo de encabezado. Pero a mi modo de ver, es obtener ese beneficio o el beneficio de rendimiento de tener a los miembros contiguos en la memoria (o hacer este truco- "Por qué el intento n. ° 3 es deplorable"). Otra advertencia es que si A es una clase con plantilla, la sintaxis se vuelve molesta (ya sabes, no puedes usar mData1 directamente, debes hacer esto-> mData1, y debes comenzar a usar el nombre de tipo y quizás las palabras clave de plantilla para los tipos dependientes y tipos con plantilla, etc.). Sin embargo, otra advertencia es que ya no puede usar private en la clase original, solo miembros protegidos, por lo que no puede restringir el acceso desde ninguna clase heredada, no solo el pimpl. Lo intenté pero no pude solucionar este problema. Por ejemplo, intenté convertir el pimpl en una clase de plantilla de amigo con la esperanza de que la declaración de amigo sea lo suficientemente amplia como para permitirme definir la clase de pimpl real en un espacio de nombres anónimo, pero eso simplemente no funciona. Si alguien tiene alguna idea de cómo mantener la privacidad de los miembros de datos y aún permitir que una clase de pimpl heredada definida en un espacio de nombres anónimo acceda a ellos, ¡realmente me gustaría verlo! Eso eliminaría mi reserva principal de usar esto.
Sin embargo, creo que estas advertencias son aceptables para los beneficios de lo que propongo.
Intenté buscar en línea alguna referencia a este modismo de "pimpl solo de función" pero no pude encontrar nada. Estoy realmente interesado en lo que la gente piensa sobre esto. ¿Hay otros problemas con esto o razones por las que no debería usar esto?
ACTUALIZAR:
He encontrado esta propuesta que más o menos trata de lograr exactamente lo que soy, pero lo hace cambiando el estándar. Estoy completamente de acuerdo con esa propuesta y espero que se convierta en el estándar (no sé nada de ese proceso, así que no tengo idea de la probabilidad de que eso suceda). Prefiero tener esto posible a través de un mecanismo de lenguaje incorporado. La propuesta también explica los beneficios de lo que estoy tratando de lograr mucho mejor que yo. Tampoco tiene el problema de romper la encapsulación como lo ha hecho mi sugerencia (privado -> protegido). Aún así, hasta que esa propuesta se convierta en el estándar (si eso sucede), creo que mi sugerencia hace posible obtener esos beneficios, con las advertencias que mencioné.
ACTUALIZACIÓN2:
Una de las respuestas menciona LTO como una posible alternativa para obtener algunos de los beneficios (optimizaciones más agresivas, supongo). No estoy realmente seguro de qué sucede exactamente en varios pases de optimización del compilador, pero tengo un poco de experiencia con el código resultante (uso gcc). Simplemente poner los métodos privados en la clase original obligará a aquellos a tener un enlace externo.
Podría estar equivocado aquí, pero la forma en que lo interpreto es que el optimizador de tiempo de compilación no puede eliminar la función incluso si todas sus instancias de llamada están completamente integradas dentro de esa TU. Por alguna razón, incluso LTO se niega a deshacerse de la definición de la función, incluso si parece que todas las instancias de llamada en todo el binario vinculado están en línea. Encontré algunas referencias que indican que es porque el vinculador no sabe si de alguna manera llamarás a la función usando punteros de función (aunque no entiendo por qué el vinculador no puede darse cuenta de que la dirección de ese método nunca se toma )
Este no es el caso si utiliza mi sugerencia y coloca esos métodos privados en un pimpl dentro de un espacio de nombres anónimo. Si se alinean, las funciones NO aparecerán en (con -O3, que incluye -finline-functions) el archivo de objeto.
Según tengo entendido, el optimizador, al decidir si se debe incorporar o no una función, tiene en cuenta su impacto en el tamaño del código. Entonces, usando mi sugerencia, estoy haciendo un poco "más barato" para que el optimizador incorpore esos métodos privados.
PCALL
es un comportamiento indefinido. No puede convertir unA
aPImpl
y usarlo a menos que el objeto subyacente sea realmente de tipoPImpl
. Sin embargo, a menos que me equivoque, los usuarios solo crearán objetos de tipoA
.Respuestas:
Los puntos de venta del patrón Pimpl son:
Para este efecto, el Pimpl clásico consta de tres partes:
Una interfaz para el objeto de implementación, que debe ser pública, y utilizar métodos virtuales para la interfaz:
Se requiere que esta interfaz sea estable.
Un objeto de interfaz que representa la implementación privada. No tiene que usar métodos virtuales. El único miembro permitido es un puntero a la implementación:
El archivo de encabezado de esta clase debe ser estable.
Al menos una implementación
El Pimpl luego nos compra una gran estabilidad para una clase de biblioteca, a costa de una asignación de almacenamiento dinámico y un envío virtual adicional.
¿Cómo se compara tu solución?
Entonces, para cada objetivo del patrón Pimpl, no logra cumplir este objetivo. Por lo tanto, no es razonable llamar a su patrón una variación del Pimpl, es mucho más una clase ordinaria. En realidad, es peor que una clase ordinaria porque sus variables miembro son privadas. Y debido a ese reparto que es un punto flagrante de fragilidad.
Tenga en cuenta que el patrón Pimpl no siempre es óptimo: hay una compensación entre la estabilidad y el polimorfismo, por un lado, y la compacidad de la memoria, por el otro. Es semánticamente imposible que un lenguaje tenga ambos (sin la compilación JIT). Entonces, si está micro optimizando la compactación de la memoria, claramente el Pimpl no es una solución adecuada para su caso de uso. Probablemente también dejará de usar la mitad de la biblioteca estándar, ya que estas horribles clases de cadenas y vectores implican asignaciones de memoria dinámica ;-)
fuente
Para mí, las ventajas no superan las desventajas.
Ventajas :
Puede acelerar la compilación, ya que guarda una reconstrucción si solo han cambiado las firmas de métodos privados. Pero la reconstrucción es necesaria si las firmas de métodos públicos o protegidos o los miembros de datos privados han cambiado, y es raro que tenga que cambiar las firmas de métodos privados sin tocar ninguna de estas otras opciones.
Puede permitir optimizaciones de compilador más agresivas, pero LTO debería permitir muchas de las mismas optimizaciones (al menos, creo que puede hacerlo; no soy un gurú de la optimización de compiladores), además de algunas más, y puede hacerse estándar y automático.
Desventajas
Mencionó un par de desventajas: la imposibilidad de usar privado y las complejidades con las plantillas. Sin embargo, para mí, la mayor desventaja es que es simplemente incómodo: un estilo de programación poco convencional, con saltos de estilo Pimpl no bastante estándar entre la interfaz y la implementación, que no será familiar para los futuros mantenedores o nuevos miembros del equipo, y que puede estar mal respaldado por herramientas (ver, por ejemplo, este error GDB ).
Las preocupaciones estándar sobre la optimización se aplican aquí: ¿Ha medido que las optimizaciones dan una mejora significativa a su rendimiento? ¿Mejoraría su rendimiento al hacer esto o al tomarse el tiempo necesario para mantenerlo e invertirlo en perfiles de puntos de acceso, mejora de algoritmos, etc.? Personalmente, prefiero elegir un estilo de programación claro y directo, suponiendo que libere tiempo para hacer optimizaciones específicas. Pero esa es mi perspectiva para los tipos de código en los que trabajo: para su dominio problemático, las compensaciones pueden ser diferentes.
Nota al margen : permisos privados con pimpl solo de método
Preguntó acerca de cómo permitir miembros privados con su sugerencia de pimpl de solo método. Desafortunadamente, considero que el pimpl solo por método es una especie de hack, pero si ha decidido que las ventajas superan a las desventajas, entonces también podría aceptar el hack.
Ah
A.cpp:
fuente
Puede usar
std::aligned_storage
para declarar almacenamiento para su pimpl en la clase de interfaz.En la implementación, puede construir su clase Pimpl en el lugar
_storage
:fuente
No, no puede implementarlo sin una penalización de rendimiento. PIMPL es, por su propia naturaleza, una penalización de rendimiento, ya que está aplicando una indirecta en tiempo de ejecución.
Por supuesto, esto depende exactamente de lo que desea indirectamente. Parte de la información simplemente no es utilizada por el consumidor, como exactamente lo que pretende poner en sus 64 bytes de 4 bytes alineados. Pero otra información es, como el hecho de que desea 64 bytes de 4 bytes alineados para su objeto.
Los PIMPL genéricos sin penalizaciones de rendimiento no existen y nunca existirán. Es la misma información que le niega a su usuario que desea utilizar para optimizar. Si se lo das, tu IMPL no se abstrae; si se lo niegas, no pueden optimizar. No puedes tenerlo en ambos sentidos.
fuente
Con el debido respeto y no con la intención de matar esta emoción, no veo ningún beneficio práctico que sirva desde una perspectiva de tiempo de compilación. Muchos de los beneficios
pimpls
se obtendrán al ocultar detalles de tipo definidos por el usuario. Por ejemplo:... en tal caso, el costo más alto de la compilación proviene del hecho de que, para definir
Foo
, debemos conocer los requisitos de tamaño / alineación deBar
(lo que significa que debemos exigir recursivamente la definición deBar
).Si no está ocultando los miembros de datos, se pierde uno de los beneficios más significativos desde una perspectiva de tiempo de compilación. También hay un código de aspecto potencialmente peligroso allí, pero el encabezado no se aclara y el archivo fuente se vuelve más pesado con más funciones de reenvío, por lo que es probable que aumente, en lugar de disminuir, los tiempos de compilación en general.
Encabezados más ligeros es la clave
Para obtener una disminución en los tiempos de compilación, desea poder mostrar una técnica que dé como resultado un encabezado dramáticamente más liviano (generalmente al permitirle no recurrir recursivamente a
#include
otros encabezados porque oculta detalles que ya no requieren ciertasstruct/class
definiciones). Ahí es donde genuinopimpls
puede tener un efecto significativo, rompiendo cadenas en cascada de inclusiones de encabezado y produciendo encabezados mucho más independientes con todos los detalles privados ocultos.Maneras más seguras
Si de todos modos desea hacer algo como esto, también será mucho más simple usar un
friend
definido en su archivo de origen en lugar de uno que herede la misma clase que en realidad no crea con trucos de puntero para invocar métodos un objeto desinstalado, o simplemente use funciones independientes con enlace interno dentro del archivo fuente que reciben los parámetros apropiados para hacer el trabajo necesario (cualquiera de estos al menos podría permitirle ocultar algunos métodos privados del encabezado para un ahorro muy trivial en tiempos de compilación y un poco de margen de maniobra para evitar la compilación en cascada).Asignador fijo
Si desea el tipo de pimpl más barato, el truco principal es utilizar un asignador fijo. Especialmente cuando se agregan pimpls a granel, la mayor causa de muerte es la pérdida de localidad espacial y las fallas de página obligatorias adicionales al acceder al pimpl por primera vez. Al preasignar agrupaciones de memoria que agrupan la memoria en los pimpls que se asignan y devuelven la memoria a la agrupación en lugar de liberarla en la desasignación, el costo de una gran cantidad de instancias de pimpl disminuye drásticamente. Sin embargo, aún no es gratuito desde el punto de vista del rendimiento, pero es mucho más barato y mucho más amigable con la caché / página.
fuente