2010-05-27 7 views
14

He oído hablar de código de infladas en el contexto de las plantillas de C++. Sé que no es el caso con los compiladores modernos de C++. Pero, quiero construir un ejemplo y convencerme a mí mismo.Plantillas C++: convencer a sí mismo contra el código de hinchazón

digamos que tenemos una clase

template< typename T, size_t N > 
class Array { 
    public: 
    T * data(); 
    private: 
    T elems_[ N ]; 
}; 

template< typename T, size_t N > 
T * Array<T>::data() { 
    return elems_; 
} 

Además, digamos que contiene types.h

typedef Array< int, 100 > MyArray; 

x.cpp contiene

MyArray ArrayX; 

y y.cpp contiene

MyArray ArrayY; 

Ahora, ¿cómo puedo verificar que el espacio de código para MyArray::data() es igual para ambos ArrayX y ArrayY?

¿Qué más debería saber y verificar de este (u otro ejemplo similar)? Si hay consejos específicos de g ++, también estoy interesado en eso.

PD: En cuanto a la hinchazón, me preocupa incluso el más mínimo de hinchazones, ya que vengo del contexto incrustado.


Adición: ¿Cambia la situación de todos modos, si las clases de plantilla se crean instancias de forma explícita?

Respuesta

12

Hace una pregunta incorrecta: cualquier "hinchazón" en su ejemplo no tiene nada que ver con las plantillas. (la respuesta a su pregunta, por cierto, es tomar la dirección de la función miembro en ambos módulos y verá que son los mismos)

Lo que realmente desea preguntar es, para cada instanciación de plantilla, el ejecutable resultante crece linealmente? La respuesta es no, el enlazador/optimizador hará magia.

compilar un exe que crea un tipo:

Array< int, 100 > MyArray; 

Nota el tamaño EXE resultante. Ahora hacerlo de nuevo:

Array< int, 100 > MyArray; 
Array< int, 99 > MyArray; 

Y así sucesivamente, durante unos 30 versiones diferentes, trazando los tamaños resultantes exe. Si las plantillas fueran tan horribles como la gente piensa, el tamaño del archivo crecerá en una cantidad fija para cada instancia de plantilla única.

+8

"tome la dirección de la función miembro en ambos módulos y verá que son las mismas" - esto invoca el compilador Schroedingers. Al observar algo, puedes alterarlo. En particular, tomar la dirección de algo restringe lo que un compilador puede hacer con él. P.ej. no puede poner un int en un registro si necesita tener un int * en él. – MSalters

+0

Lo que se puede hacer (en teoría) a través de la información de depuración, pero no me preguntes cómo. – BCS

+3

@MSalters: algo sin sentido, ya que C++ se define en torno a un comportamiento observable. Si no observas un programa en C++, el compilador podría simplemente emitir un solo 'nop'. – jalf

-1

El código generado será exactamente el mismo, ya que el código en ambos archivos es exactamente el mismo. Puede desarmar el código para verificarlo si lo desea.

+1

¿Pero habrá una copia o dos? – BCS

+0

@BCS probablemente solo haya una instancia en el código generado, porque son las mismas – Ghita

10

En este caso específico, encontrará que g ++ tenderá a alinear el acceso si tiene algún tipo de optimización activada. Es cierto que hay un poco de inflamación del código, aunque es discutible si la sobrecarga de la llamada sería menor.

Sin embargo, una forma fácil de verificar lo que se compila es con la herramienta nm.Si puedo compilar el código con un simple main() ejercer ArrayX::data() y ArrayY::data() y luego compilarlo con -O0 para desactivar procesos en línea, puedo correr nm -C para ver los símbolos en el ejecutable:

% nm -C test 
0804a040 B ArrayX 
0804a1e0 B ArrayY 
08049f08 d _DYNAMIC 
08049ff4 d _GLOBAL_OFFSET_TABLE_ 
0804858c R _IO_stdin_used 
     w _Jv_RegisterClasses 
080484c4 W Array<int, 100u>::data() 
08049ef8 d __CTOR_END__ 
08049ef4 d __CTOR_LIST__ 
08049f00 D __DTOR_END__ 
... 

verá que la El símbolo Array<int, 100u>::data() solo aparece una vez en el ejecutable final aunque el archivo de objeto para cada una de las dos unidades de traducción contenga su propia copia. (La herramienta nm también funciona en los archivos de objetos. Se puede utilizar para comprobar que x.o y y.o cada uno tiene una copia del Array<int, 100u>::data().)

Si nm no proporciona suficiente detalle, también puede echar un vistazo a la objdump herramienta. Se parece mucho a nm, pero con los símbolos de depuración activados, incluso puede mostrarle cosas como un desensamblaje del ejecutable de salida con líneas de origen entremezcladas.

+1

+ !: Esta es una buena forma de ver el problema. Comprobé que el ejecutable final tiene una copia * única de Array Arun

7

Las plantillas no tienen nada que ver con eso.

consideran este pequeño programa:

ah:

class a { 
    int foo() { return 42; } 
}; 

b.cpp:

#include "a.h" 

void b() { 
    a my_a; 
    my_a.foo(); 
} 

c.cpp:

#include "a.h" 

void c() { 
    a my_a; 
    my_a.foo(); 
} 

no hay plantillas, pero tienes exactamente el mismo problema La misma función se define en múltiples unidades de traducción. Y la regla es la misma: solo se permite una definición en el programa final; de lo contrario, el compilador no podría determinar a cuál llamar, y de lo contrario dos punteros a funciones que apuntan a la misma función pueden apuntar a diferentes direcciones.

El "problema" con la hinchazón del código de plantilla es algo diferente: es si crea muchas instancias diferentes de la misma plantilla. Por ejemplo, la utilización de su clase, este programa va a correr el riesgo de un cierto código hinchazón:

Array< int, 100 > i100; 
Array< int, 99 > i99; 
Array< long, 100 > l100; 
Array< long, 99> l99; 

i100.Data(); 
i99.Data(); 
l100.Data(); 
l99.Data(); 

Estrictamente hablando, se requiere que el compilador para crear 4 diferentes instancias de la función Data, uno para cada conjunto de parámetros de plantilla. En la práctica, algunos compiladores (pero no todos) tratan de fusionarlos nuevamente, siempre que el código generado sea idéntico. (En este caso, el conjunto generado para Array< int, 100 > y Array< long, 100 > sería idéntico en muchas plataformas, y la función no depende tampoco del tamaño de la matriz, por lo que las variantes 99 y 100 también deberían producir código idéntico, por lo que un compilador inteligente se fusionará las instancias vuelven juntas.

No hay ninguna magia para las plantillas. No misteriosamente "inflan" su código. Simplemente le dan una herramienta que le permite crear fácilmente un montón de tipos diferentes de la misma plantilla. use todos estos tipos, tiene que generar código para todos ellos. Como siempre con C++, usted paga por lo que usa. Si usa un Array<long, 100>, un Array<int, 100>, un Array<unsigned long, 100> y un Array<unsigned int, 100>, entonces obtiene cuatro clases diferentes , porque cuatro clases diferentes fueron lo que preguntado Si no solicita cuatro clases diferentes, no le costarán nada.

+0

Este análisis no es correcto. A diferencia de su ejemplo alternativo, una creación de instancias de plantilla genera métodos no en línea que terminarán en el archivo del objeto. Ver: http://gcc.gnu.org/onlinedocs/gcc/Template-Instantiation.html. La opción predeterminada es "3. No hacer nada. Pretender que G ++ implementa una gestión automática de instancias. El código escrito para el modelo Borland funcionará bien, pero cada unidad de traducción contendrá instancias de cada una de las plantillas que utiliza. En un programa grande, esto puede llevar a una cantidad inaceptable de duplicación de código ". –

+3

@Josh: la unidad de traducción contendrá instancias del mismo código (y también lo hará mi ejemplo sin plantilla), sí, pero el ejecutable final ciertamente no lo hará. Su enlace incluso dice esto explícitamente: "De alguna manera, el compilador y el enlazador deben asegurarse de que cada instancia de plantilla ocurra exactamente una vez en el ejecutable si es necesario, y de ninguna otra manera". Si el compilador genera varias instancias (como en el modelo de "no hacer nada"), el enlazador elimina todas, excepto una, al igual que con los símbolos que no son de plantilla. – jalf

+0

tiene razón, estaba confundiendo mis problemas de plantillas. Todavía puede terminar con las definiciones efectivamente redundantes en su ejecutable si, por ejemplo. Foo <1> :: Bar() y Foo <2> :: Bar() generan exactamente el mismo código. Muchos compiladores no colapsarán estos en una sola copia, incluso si el código emitido es idéntico. Pero tiene razón al decir que esto es una función de la cantidad de instancias diferentes que declara. –

3

Una prueba sería poner una variable estática en data(), incrementarla en cada llamada e informarla.

Si MiMatriz :: datos() está ocupando el mismo espacio de código, a continuación, debería ver que informan 1 y 2.

Si no es así, sólo debe ver 1.

me encontré con él, y obtuve 1 y 2, lo que indica que se estaba ejecutando desde el mismo conjunto de código. Para comprobar esto era cierto, he creado otra matriz con el parámetro de tamaño de 50, y se echaron 1. Código

completa (con un par de ajustes y correcciones) se ofrecen a continuación:

Array.hpp:

#ifndef ARRAY_HPP 
#define ARRAY_HPP 
#include <cstdlib> 
#include <iostream> 

using std::size_t; 

template< typename T, size_t N > 
class Array { 
    public: 
    T * data(); 
    private: 
    T elems_[ N ]; 
}; 

template< typename T, size_t N > 
T * Array<T,N>::data() { 
    static int i = 0; 
    std::cout << ++i << std::endl; 
    return elems_; 
} 

#endif 

types.hpp:

#ifndef TYPES_HPP 
#define TYPES_HPP 

#include "Array.hpp" 

typedef Array< int, 100 > MyArray; 
typedef Array< int, 50 > MyArray2; 

#endif 

x.cpp:

#include "types.hpp" 

void x() 
{ 
    MyArray arrayX; 
    arrayX.data(); 
} 

y.cpp:

#include "types.hpp" 

void y() 
{ 
    MyArray arrayY; 
    arrayY.data(); 
    MyArray2 arrayY2; 
    arrayY2.data(); 
} 

main.cpp:

void x(); 
void y(); 

int main() 
{ 
    x(); 
    y(); 
    return 0; 
} 
+0

+1: Esta es una construcción de experimento excelente. Verifiqué usando las mismas líneas y me convencí a mí mismo. – Arun

3

Aquí es una secuencia de comandos de utilidad poco que he estado usando para obtener una idea de sólo estas cuestiones. Le muestra no solo si un símbolo se define varias veces, sino también cuánto tamaño de código está tomando cada símbolo. He encontrado esto extremadamente valioso para auditar problemas de tamaño de código.

Por ejemplo, aquí es una muestra de la invocación:

$ ~/nmsize src/upb_table.o 
39.5%  488 upb::TableBase::DoInsert(upb::TableBase::Entry const&) 
57.9%  228 upb::TableBase::InsertBase(upb::TableBase::Entry const&) 
70.8%  159 upb::MurmurHash2(void const*, unsigned long, unsigned int) 
78.0%  89 upb::TableBase::GetEmptyBucket() const 
83.8%  72 vtable for upb::TableBase 
89.1%  65 upb::TableBase::TableBase(unsigned int) 
94.3%  65 upb::TableBase::TableBase(unsigned int) 
95.7%  17 typeinfo name for upb::TableBase 
97.0%  16 typeinfo for upb::TableBase 
98.0%  12 upb::TableBase::~TableBase() 
98.7%  9 upb::TableBase::Swap(upb::TableBase*) 
99.4%  8 upb::TableBase::~TableBase() 
100.0%  8 upb::TableBase::~TableBase() 
100.0%  0 
100.0%  0 __cxxabiv1::__class_type_info 
100.0%  0 
100.0% 1236 TOTAL 

En este caso he ejecutarlo en un solo archivo .o, pero también se puede ejecutar en un archivo .ao en un archivo ejecutable. Aquí puedo ver que los constructores y los destructores se emitieron dos o tres veces, que es el resultado de this bug.

Aquí está la secuencia de comandos:

#!/usr/bin/env ruby 

syms = [] 
total = 0 
IO.popen("nm --demangle -S #{ARGV.join(' ')}").each_line { |line| 
    addr, size, scope, name = line.split(' ', 4) 
    next unless addr and size and scope and name 
    name.chomp! 
    addr = addr.to_i(16) 
    size = size.to_i(16) 
    total += size 
    syms << [size, name] 
} 

syms.sort! { |a,b| b[0] <=> a[0] } 

cumulative = 0.0 
syms.each { |sym| 
    size = sym[0] 
    cumulative += size 
    printf "%5.1f%% %6s %s\n", cumulative/total * 100, size.to_s, sym[1] 
} 

printf "%5.1f%% %6s %s\n", 100, total, "TOTAL" 

Si ejecuta esto en sus propios archivos de .ao archivos ejecutables, debe ser capaz de convencerse de que sabe exactamente lo que está sucediendo con el tamaño de su código. Creo que las versiones recientes de gcc pueden eliminar instancias de plantillas redundantes o inútiles en tiempo de enlace, por lo que recomiendo analizar sus ejecutables reales.

4

Una mejor ilustración de código bloat con plantillas está utilizando una plantilla para generar código, no variables. El pánico típico se debe al código de generación del compilador para cada instancia de la plantilla (galería de símbolos). Esto es similar a la hinchazón del código debido a las funciones y métodos en línea. Sin embargo, los compiladores y enlazadores modernos pueden realizar magick para reducir el tamaño del código, dependiendo de la configuración de optimización.

Por ejemplo:

template <typename Any_Type> 
void Print_Hello(const Any_Type& v) 
{ 
    std::cout << "Hello, your value is:\n" 
       << v 
       << "\n"; 
    return; 
} 

El código anterior es mejor como una plantilla. El compilador generará el código según el tipo pasado a Print_Hello. El problema es que muy poco del código realmente depende de la variable. (que se puede reducir, factorizando los datos del código de const &).)

El temor es que el compilador generará el código para cada instancia utilizando el mismo tipo de variable, construyendo así el código repetitivo:

int main(void) 
{ 
    int a = 5; 
    int b = 6; 
    Print_Hello(a); // Instantiation #1 
    Print_Hello(b); // Instantiation #2 
    return 0; 
} 

El temor también podría extenderse cuando la plantilla (plantilla) se instancia en diferentes unidades de traducción.

Los compiladores y enlazadores modernos son inteligentes. Un compilador inteligente reconocería la llamada a la función de la plantilla y la convertiría en algún nombre único destrozado. El compilador solo usaría una instanciación para cada llamada. Similar a la sobrecarga de funciones.

Incluso si el compilador era descuidado y generaba varias instancias de la función (para el mismo tipo), el enlazador reconocería los duplicados y solo pondría una instancia en el ejecutable.

Cuando se usa de forma descuidada, una plantilla de función o método puede agregar código adicional. Los ejemplos son funciones grandes que solo difieren por tipo en algunas áreas. Tienen una alta proporción de código no tipeado para escribir código dependiente.

Una implementación del ejemplo anterior con menos engordar:

void Print_Prompt(void) 
{ 
    std::cout << "Hello, your value is:\n"; 
    return; 
} 

template <typename Any_Type> 
void Better_Print_Hello(const Any_Type& v) 
{ 
    Print_Prompt(); 
    std::cout << v << "\n"; 
    return; 
} 

La principal diferencia es que el código que no depende del tipo de variable ha sido un factor fuera en una nueva función. Esto puede no parecer útil para este pequeño ejemplo, pero ilustra el concepto. Y el concepto es refactorizar la función en piezas que dependen del tipo de variable y las que no. Las piezas que son dependientes se convierten en funciones de plantilla.

Cuestiones relacionadas