Archiviazione di oggetti le cui dimensioni dipendono dalla memoria in tempo di esecuzione in modo contiguo

5

Lo sfondo

Sto lavorando su un ECS in C ++ per divertimento e sto cercando di renderlo il più efficiente possibile. Una delle ottimizzazioni che sto cercando di implementare è quella di minimizzare i problemi di cache memorizzando i componenti in modo contiguo nella memoria.

Dato che voglio esporre l'interfaccia a Lua, non posso usare tutte le offerte di C ++ di bontà della compilazione. La parte difficile è che non conosco la dimensione degli oggetti al momento della compilazione. A quanto ho capito, non posso semplicemente usare std::vector<BaseComponent> e non provare affettare oggetto .

TL; DR - La domanda

Come faccio a memorizzare oggetti, le cui dimensioni sono conosciute solo in fase di esecuzione, in modo contiguo nella memoria? Le dimensioni degli oggetti in un contenitore sono identiche.

    
posta sjaustirni 18.04.2017 - 19:55
fonte

4 risposte

6

How do I store objects, whose sizes are known only at runtime, contiguously in the memory? The sizes of the objects in one container are identical.

  1. Assegna grandi blocchi di memoria.
  2. Utilizza il posizionamento new per inizializzare gli oggetti.
char* mem = new char[OBJECT_COUNT*sizeof(Object)];

// ...

// Keep track of where the next object can be constructed.
Object* obj = new (mem + offset) Object();

Se vuoi che la memoria allocata sia allineata in base ai requisiti di allineamento di Object , puoi usare:

struct alignas(Object) object_s
{
  char dummy[sizeof(Object)];
};

object_t* mem = new object_s[OBJECT_COUNT]);

// ...

// Keep track of where the next object can be constructed.
Object* obj = new (mem + offset) Object();
    
risposta data 18.04.2017 - 23:21
fonte
2

Se stai cercando un approccio standard, non posso aiutarti, ma questo non è particolarmente difficile da eseguire se tutti gli oggetti hanno le stesse dimensioni.

int maxNumberOfObjs = x;
int objectSize = y;
int count = 0;

void* ptr = malloc(maxNumberOfObjs * objectSize); //don't forget to free this or you will get a mem leak

//Add object
MyObj* myObjPtr = new (ptr + count * objectSize) MyObj();
++count;

//Iterate over your objects
int i = 0;
while (i < count)
{
    MyObj* obj = ptr + i * objectSize;
    ++i;
}

Da un po 'di tempo da quando ho fatto C ++, quindi potresti dover fare qualche modifica e cast, ma hai l'idea che spero

    
risposta data 18.04.2017 - 22:56
fonte
0

How do I store objects, whose sizes are known only at runtime, contiguously in the memory?

Questa è una domanda banale in C.

static void* mem_offset(void* ptr, int offset)
{
    return (char*)ptr + offset;
}

...
// Allocate the memory for 'num' objects given 'size' (could be
// a runtime lvalue)
void* mem = malloc(num*size);

// Access the component at the nth position.
void* component = mem_offset(mem, n*size);

// Do something with 'component', maybe passing it to Lua.

// Free the array of components.
free(mem);

Il fatto è che se hai solo la dimensione di un oggetto in fase di esecuzione, devi in gran parte abbandonare la ricchezza del sistema di tipi offerto in C ++ e anche C. Le cose si dividono in bit e byte in quella fase a meno che tu avere le informazioni in fase di compilazione in anticipo per conoscere il tipo di componente e il tipo e l'allineamento dei suoi campi di dati, a quel punto avresti già una volta la dimensione del componente ECS in fase di compilazione.

Quindi sei tornato a lavorare con i dati come se fossero solo matrici di byte se stai per leggerlo e / o modificarlo quando non sai nemmeno quale dimensione è in fase di compilazione. Se le informazioni sul tipo sono disponibili per Lua, è possibile archiviare questi bit e byte opachi in C ++, passare i dati a Lua, a quel punto Lua avrà le informazioni sul tipo necessarie per tradurre quei bit e quei byte indietro in qualcosa che assomiglia ad un tipo di dati effettivo (es: una tabella Lua o Lua userdata con tabella delle funzioni associata).

Naturalmente se hai informazioni parziali sul tipo in fase di compilazione, come se ereditasse una classe base o che i suoi primi campi avessero membri di un tipo specifico con un certo allineamento, allora puoi lavorare con ciò che è garantito essere conosciuto in anticipo al momento della compilazione attraverso il sistema di tipi. Come esempio C di basso livello:

struct KnownType
{
    int x;
    int y;
    void (*do_something)(struct UnknownType* obj);
};

struct UnknownType
{
    // Parts of the type known at compile-time.
    struct KnownType known_data;

    // Additional stuff stored below is known only at runtime.
    // This is effectively a variable-length struct.
    char trailing_data[];
};

// Access the nth component and modify its 'x' and 'y' fields
// which we know in advance at compile-time that the component
// will have, even if we don't know anything else about it.
struct UnknownType* unknown_data = mem_offset(mem, n*unknown_type_size);
unknown_data->known_data.x = 123;
unknown_data->known_data.y = 456;

// Do something (polymorphism):
unknown_data->known_data.do_something(unknown_data);

Quindi puoi almeno lavorare con i dati per il tipo sconosciuto come se fosse un tipo noto e lavorare con quei campi x e y , ma i dati finali sono qualcosa che non sarai in grado di accedere attraverso il sistema di tipi in C e C ++ (forse solo essere in grado di lavorare con esso in modo carino in Lua).

    
risposta data 29.11.2017 - 13:15
fonte
0

La soluzione più semplice che evita di scendere ai bit e ai byte dei raggi X e di perdere tutta la sicurezza del tipo nell'implementazione intermedia: non astrarre / mixare lo storage sul singolo componente, astrarre / mixare nel container livello. Invece di BaseComponent puoi usare BaseComponents (avviso plurale) per la tua astrazione. Ovviamente questo significa che quando si inserisce un componente concreto (magari attraverso una sorta di factory), è necessario downcast il relativo contenitore astratto (sebbene questo sia generalmente inevitabile con un ECS, specialmente se non è possibile utilizzare tutte le chicche in fase di compilazione , ma è centralizzato in un punto nell'implementazione ECS) con le informazioni sul tipo corrispondenti per inserire il sottotipo del componente concreto in base al valore.

Ciò è sfumato rispetto all'implementazione (posso aiutarlo se necessario), ma questa è la strategia fondamentale. Il modo più semplice per memorizzare tipi misti di elementi in cui gli elementi dello stesso sottotipo sono memorizzati in modo contiguo è astratto a livello del contenitore e il contenitore polimorfico è in realtà un contenitore di contenitori.

Un esempio molto semplice (frettoloso e sgradevole, ma minimalista), che non copre le campane e i fischi di associare componenti alle entità, o consentire la registrazione di componenti di runtime non codificati, ma mostra un modo fondamentale per mantenere contiguità nei sottotipi del componente:

struct FooComponent
{
    enum {id = 0};
    int x;
    int y;
};

struct BarComponent
{
    enum {id = 1};
    float z;
};

struct Components
{
    virtual ~Components() {}
};

template <class T>
struct ComponentsT: public Components
{
    // All components of a given type are stored contiguously,
    // by value.
    std::vector<T> comps;
};

struct Ecs
{
    Ecs()
    {
        // Insert concrete sequences/containers for Foo and Bar components.
        comps.emplace_back(new ComponentsT<FooComponent>);
        comps.emplace_back(new ComponentsT<BarComponent>);
    }

    template <class T>
    void insert(const T& comp)
    {
        // Fetch appropriate container based on associated type ID to T.
        typedef ComponentsT<T> SubComponents;
        SubComponents* sub_comps = dynamic_cast<SubComponents*>(comps[T::id].get());

        // Insert component of type T.
        sub_comps->comps.push_back(comp);
    }

    template <class T>
    T* get(int idx)
    {
        // Fetch appropriate container based on associated type ID to T.
        typedef ComponentsT<T> SubComponents;
        SubComponents* sub_comps = dynamic_cast<SubComponents*>(comps[T::id].get());

        // Return component of type T at specified index.
        return &sub_comps->comps[idx];
    }

    // A container of containers of different component types.
    std::vector<std::unique_ptr<Components>> comps;
};

In fase di runtime

Ora essere in grado di fare cose come registrare nuovi tipi di componenti al volo in fase di runtime diventa davvero coinvolgente con un'implementazione ECS in piena regola.

Ma la cosa fondamentale da ricordare è che hai bisogno di un'implementazione in fase di compilazione per costruire e distruggere un certo tipo, T . Quindi devi chiamare una funzione che ha quel tipo di informazioni per essere in grado di fare queste cose che richiedono tali informazioni di tipo.

Ma ovviamente se chiami tali funzioni da Lua, non hai informazioni di questo tipo. Quindi la strategia generale è che Lua usa, ad esempio, un ID di tipo (potrebbe essere solo un numero intero, o una stringa, o qualsiasi altra cosa galleggi la tua barca) per dire indirettamente all'ECS quali funzioni chiamare. E quando registri nuovi tipi di componenti, puoi fornire i metadati ECS su come costruire e distruggere quel particolare tipo di componente (es: puntatori di funzione o un'interfaccia virtuale per costruire e distruggere BarComponent ) e associarlo a questo tipo ID / chiave. I metadati potrebbero effettivamente memorizzare ComponentsT<BarComponent> come parte di esso insieme ai puntatori di funzione / funzioni virtuali per recuperare e costruire e distruggere e inserire% istanze diBarComponent.

Semplice esempio di registrazione runtime

Va bene, per noia ho incluso un esempio molto minimalista e frettoloso di come fare quanto detto sopra usando chiavi di stringa (non consiglio le chiavi di stringa normalmente in un ECS - le stringhe internate potrebbero essere un'alternativa decente, ma sono semplici).

#include <vector>
#include <map>
#include <string>
#include <memory>
#include <iostream>

struct FooComponent
{
    int x;
    int y;
};

struct BarComponent
{
    float z;
};

// Abstract container.
struct Components
{
    virtual ~Components() {}

    virtual void insert() = 0;
    virtual void erase(int idx) = 0;
    virtual int size() const = 0;
    virtual void* get(int idx) = 0;
};

// Container subtype.
template <class T>
struct ComponentsT: public Components
{
    virtual void insert() override
    {
        comps.push_back(T());
    }

    virtual void erase(int idx) override
    {
        comps[idx] = comps[comps.size() - 1];
        comps.pop_back();
    }

    virtual int size() const override
    {
        return static_cast<int>(comps.size());
    }

    virtual void* get(int idx) override
    {
        return &comps[idx];
    }

    // Stores all components of type T contiguously by value.
    std::vector<T> comps;
};

struct Ecs
{
    // Registers a new type of component and associates the type to
    // a string key.
    template <class T>
    void register_type(const std::string& key)
    {
        Components* comps_t = new ComponentsT<T>; 
        comps[key] = std::unique_ptr<Components>(comps_t);
    }

    // Fetch the container of components associated to string key
    // or nullptr if no such type is registered.
    Components* fetch_components(const std::string& key)
    {
        auto pos = comps.find(key);
        return pos != comps.end() ? pos->second.get(): nullptr;
    }

    // Associates a container of component subtypes to a string key.
    std::map<std::string, std::unique_ptr<Components>> comps;
};

int main()
{
    using namespace std;

    Ecs ecs;

    // Register FooComponents to a string key.
    ecs.register_type<FooComponent>("FooComponent");

    // Register BarComponents to a string key.
    ecs.register_type<BarComponent>("BarComponent");

    // Fetch contiguous FooComponents container by string key.
    Components* foo_comps = ecs.fetch_components("FooComponent");

    // Insert a FooComponent to scene.
    foo_comps->insert();

    // Retrieve a FooComponent and set its fields.
    FooComponent* foo = static_cast<FooComponent*>(foo_comps->get(0));
    foo->x = 123;
    foo->y = 456;

    // Fetch contiguous BarComponents container by string key.
    Components* bar_comps = ecs.fetch_components("BarComponent");

    // Insert some new BarComponents to scene.
    for (int j=0; j < 3; ++j)
        bar_comps->insert();

    // Output how many FooComponents there are in the scene.
    cout << foo_comps->size() << " FooComponents" << endl;

    // Output how many BarComponents there are in the scene.
    cout << bar_comps->size() << " BarComponents" << endl;
}
    
risposta data 13.12.2018 - 16:19
fonte

Leggi altre domande sui tag