2010-03-07 18 views
6

Si quiero asignar millones de objetos de una clase Foo, y quiero ahorrar memoria y tiempo, ¿cómo debo diseñar la clase Foo?¿Cómo diseñar una clase apropiada para millones de asignaciones?

Obviamente, Foo no debería contener demasiados datos de miembros.

Además, supongo que no debería usar funciones virtuales?

¿Y qué tan costoso es para Foo derivar de una clase base? ¿Y de varias clases base?

¿Hay otros consejos para hacer que millones de objetos Foo sean muy eficientes?

+0

Tener funciones virtuales solo agrega 4 bytes más al objeto (8 para 64 bits). Derivar de la clase base no cuesta nada. (Suponen una herencia única). – kennytm

+1

También debe considerar hacer una asignación más rápida usando un asignador personalizado. – yesraaj

+3

Lo que debe hacer depende de lo que esté haciendo. ¿Qué estás haciendo? – GManNickG

Respuesta

8

No creo que haya mucho que decir sobre el diseño de su clase para millones de asignaciones. Sí, existe el límite de memoria obvio, por lo que si tiene una cantidad fija de memoria, esto podría ser una preocupación real para usted, de lo contrario, siempre correrá el riesgo de quedarse sin memoria. Un puntero a la tabla virtual es solo eso, un puntero (4 u 8 bytes en la arquitectura de 32 o 64 bits), no estoy seguro de que este sea el caso en la herencia múltiple. Llamar a funciones virtuales tiene la sobrecarga de la búsqueda virtual (y falta de memoria caché adicional, si no la usó recientemente), pero solo para funciones virtuales, y es posible que nunca estén en línea.

Si hay una gran cantidad de valores repetidos, es posible que desee considerar tener una estructura de datos separada también (patrón flyweight). Y para mayor eficiencia, asegúrese de tener un constructor liviano (en línea) y operadores de asignación, especialmente si tiene la intención de usar vectores stl y similares.

Estas son todas las cosas bastante sencillo, por lo que ahora para mi verdadera sugerencia:

Lo que realmente va a matar a su gestión de memoria es si se obtiene la fragmentación, donde es posible que, de repente, tener un montón de memoria, pero todavía no tienes dónde poner tus objetos (no hay suficiente espacio contiguo). Si tiene una gran cantidad de asignaciones intercaladas, esto podría convertirse en un problema real, por lo que es posible que desee considerar la asignación de grandes bloques de objetos, mantenerlos en un grupo y volver a utilizarlos. O use un asignador personalizado (new-operator), donde asigna previamente un bloque de memoria que es un múltiplo del tamaño de su objeto y lo usa para sus objetos.

+3

Estoy de acuerdo. Flyweight + object-pool y reutilización de objetos. –

+0

De acuerdo. Además, si hay alguna manera posible de hacerlos inmutables, hacerlo permitiría la reutilización. Es posible salirse con la suya con miles de objetos a los que se hace referencia en millones de ubicaciones. Es difícil de saber sin tener más información sobre el dominio del problema. – kyoryu

3

En la medida en que se puede decir que un objeto es eficiente en absoluto, debería ser pequeño si se crean muchos, y las operaciones comunes en él deberían ser alineables si se realizan muchas llamadas. En términos de memoria, las funciones virtuales generalmente cuestan 4 u 8 bytes (el tamaño de un puntero) por objeto para el primero, y luego se liberan. Como han dicho otros, Flyweight es una forma de hacer objetos más pequeños, si contienen datos duplicados que se pueden compartir. Si tus millones de objetos no contienen datos duplicados, olvídate de eso.

Más correctamente es el código que es eficiente o ineficiente. Las llamadas virtuales probablemente le cuesten un poco de sobrecarga de llamadas, pero depende del código de llamada, no de la clase, cuántas veces se llama a cada función miembro. En cualquier caso, en línea es donde están las ganancias de gran velocidad, y las funciones virtuales son solo una forma de obstruir un sitio de llamadas en particular que se beneficia de la línea. Si su diseño se simplifica al tener 27 funciones miembro virtuales, cada una de las cuales se llama una vez al mes, pero asegurando que las 2 funciones que se llaman millones de veces por segundo pueden ser invocadas por la persona que llama, entonces no hay necesidad de evitar las funciones virtuales .

Las clases base cuestan casi lo mismo que un objeto miembro del mismo tipo. Con la herencia múltiple, static_cast podría dejar de ser una opción no operativa, ya que normalmente es solo de herencia, pero probablemente no valga la pena preocuparse por eso. La herencia virtual y dynamic_cast pueden ser interesantes en términos de la cantidad de trabajo realizado en tiempo de ejecución, pero solo en una escala similar a las funciones de miembros virtuales.

Dicho todo esto, lo principal que desea hacer es obtener un código que se ejecute lo antes posible que imite razonablemente la creación del objeto y las características de llamada que tendrá su código final. Entonces sabrá qué tipo de rendimiento está viendo: si su operación crítica aparece más o menos lo suficientemente rápido con las funciones virtuales, no tiene sentido crear un esquema de diseño contorsionado solo para evitarlas. Si su generador de perfiles le dice que se está gastando todo el tiempo creando objetos, no usándolos una vez que los tiene, entonces debe buscar las funciones de asignación, no de miembro.

El motivo para obtener un presupuesto anticipado es simplemente que no quiere perder tiempo diseñando algo que definitivamente no funcionará. Entonces, los puntos de referencia básicos ("¿puedo llamar al new mil veces por segundo? ¿Un millón de veces por segundo? ¿Tan rápido como varios hilos al mismo tiempo?") Pueden alimentar el proceso de diseño, pero por supuesto no se puede optimizar adecuadamente hasta que tener una versión plausible de tu código real.

4

Lo principal es minimizar el uso de new y delete manteniendo los objetos usados ​​en una piscina y reutilizándolos. No te preocupes por las funciones virtuales. La sobrecarga de llamarlos suele ser trivial en relación con cualquier otra cosa que esté sucediendo en su programa.

+0

Y el costo de la memoria de las funciones virtuales es mínimo: un solo puntero por objeto (a la variable virtual). Debería poder eliminar esto al no tener funciones virtuales. – kyoryu

+0

@kyoryu: Correcto. Y no podría decir cuál es la cantidad de objetos activos en estado estacionario, por lo que no podría decir si el tamaño es incluso un problema. –

3

Solo para aclarar un punto sobre asignadores personalizados.

Con el valor predeterminado new, es probable que obtenga bastante sobrecarga, es decir, memoria adicional asignada encima del sizeof(Foo). Lo que realmente quiere que suceda es distribuir esta sobrecarga sobre muchos Foo s.

La idea es realizar una llamada a new para asignar un solo bloque de bytes suficientemente grande para contener de 100 o 1000 (o más?) De los contiguos Foo s, y luego asignar un solo Foo s de eso.

Si mantiene un grupo de Foo s preasignados, es posible que aún sufra una sobrecarga de memoria en cada instancia, incluso si es más rápido.

En La pl2e C++, BS habla de bytes 'en mi máquina', por lo que tendrá que hacer los experimentos a ti mismo: ¿Cuánta memoria se toma realmente hasta asignar un único Foo con new?

Busque los Asignadores de grupos (p. Ej. Boost) o los Asignadores de objetos pequeños (p. Ej. Loki).

+0

Buen punto, pero no necesita mirar más allá del estándar. 'std :: deque ' ya asignará varios bloques contiguos de objetos T. El proveedor de compiladores optimiza la cantidad de T por bloque, y es probable que comprenda su plataforma. – MSalters

1

En cuanto a la asignación de sus objetos de clase, eche un vistazo a la biblioteca boost Pool, que puede ser más eficiente para muchas asignaciones de objetos pequeños que regular new/delete a través del asignador del sistema. También puede mantenerlos más cerca en la memoria. El fast_pool_allocator es muy útil como una implementación de asignador de C++ que puede colocar fácilmente en su aplicación para medir el beneficio. Sugiero usar asignaturas de todos modos, ya que hará que sea más fácil comparar diferentes esquemas de asignación.

Otra cosa a tener en cuenta es cuándo van a crearse los objetos, por ejemplo, si sabe de antemano cuántos va a necesitar (y por lo tanto sería útil un sistema de agrupación/reutilización según lo descrito por otros)), o si solo sabe que habrá O (1m) de ellos necesarios en diferentes puntos. Crear números grandes de una sola vez debería ser mucho más rápido que asignar muchos de ellos individualmente. De mi propio trabajo, a menudo he encontrado que la asignación repetida de memoria para muchos objetos pequeños se muestra como un gran cuello de botella en la creación de perfiles.

Te sugiero que organices una aplicación de prueba que simule el número de asignaciones que necesitas y compara las diferentes estrategias. Es posible que necesite combinar más de uno.

1

Otros han mencionado el patrón de Flyweight, pero ya que etiquetó esto para C++, miraría en Boost.Flyweight. Su diseño se adapta a tus necesidades y si necesitas reinventar la rueda, siempre puedes consultar su fuente para obtener más detalles.

1

Por lo que vale ... también puede beneficiarse de usar otro idioma.

Sé que el patrón Flyweight es bastante furor, pero aquí también podría beneficiarse si no asigna esos millones de objetos.

Si le parece extraño, piense en el objeto String en Python. Como en muchos idiomas recientes, String son inmutables en Python. Por supuesto, el objeto que manipulas puede cambiar, pero el verdadero String no: tu identificador simplemente se reubica.

Por supuesto, Python tiene recolección de basura automática que lo hace mucho más fácil, sin embargo, podría funcionar para usted también. Este es un boceto:

class FooImpl; 

class Foo 
{ 
public: 
    explicit Foo(int i): mImpl(FooImpl::Build(i)) {} 

    int foo() const { return mImpl->foo(); } 

    void foo(int i) { mImpl = mImpl->foo(i); } 

private: 
    const FooImpl* mImpl; 
}; // class Foo 

class FooImpl 
{ 
public: 
    static const FooImpl* Build(int i) 
    { 
    typedef std::unordered_set<FooImpl> foos_type; 
    FooImpl tmp(i); 
    foos_type::iterator it = gFooImplCollection.insert(tmp); 
    return &(*it); 
    } 

    int foo() const { return mFoo; } 

    const FooImpl* foo(int i) const 
    { 
    return Build(i); 
    } 

    // Useful thingy 
    bool operator==(const FooImpl& rhs) const { return mFoo == rhs.mFoo; } 
    size_t hash() const { return mFoo; } 

private: 
    explicit FooImpl(int i): mFoo(i) {} 

    int mFoo; 
}; 

std::unordered_set<FooImpl> gFooImplCollection; 

Por supuesto, esto es muy áspera , sólo para darle una idea. Si la cantidad potencial de elementos diferentes es importante, necesita recolección de basura.

Recolección de basura siendo otro tema, prefiero dejarles con la idea de una clase Immutable núcleo (sólo expone const métodos) y un mango mutable (que simplemente cambia la clase principal al que apunta cuando se le preguntó a cambios).

Y ahora que ha tomado el tiempo para leer, Boost lo tiene: Boost.Flyweight :)

Nota:

Parece importante precisa, porque Foo se supone que es asignado (en la pila) millones de veces, su tamaño debe permanecer lo más cerca posible de un puntero. Esto se logra usando el couting de referencia Intrusive (espero que eso sea lo que hace Boost). Además, no es necesario tener virtual métodos en Foo, virtual en FooImpl y Build de hecho puede llamar a AbstractFactory detrás de las escenas.

Así, desde Foo:

  • no tiene ninguna clase base
  • no tiene ningún métodos virtuales
  • sólo tiene un atributo (un puntero)

Su tamaño efectivo será el tamaño del puntero ... que es el mejor que se puede esperar si no desea almacenar un identificador de un costo de búsqueda de incurrir en cada llamada :)

Cuestiones relacionadas