Ottimizza le ricerche non banali nel vettore di puntatori di oggetti che condividono la classe base

1

Ho un array che contiene oltre 150.000 puntatori di oggetti di oltre 300 classi diverse, ma tutti ereditano dalla stessa classe base. Ovviamente questo è molto inefficiente quando dobbiamo cercare un oggetto.

Ho pensato di suddividere questo array per-object-type , che ha ottenuto prestazioni migliori, ma non tanto quanto avrei sperato:

std::unordered_map<std::type_index, std::vector<A*>> instances;

Inoltre, i criteri di ricerca sono spesso basati sul nome, ma possono anche essere solo dei parametri modello variadici, complicando ulteriormente il problema. Quindi non sono sicuro di come dovrei ordinare questo array. Ecco una versione semplificata di una delle funzioni di ricerca:

template <class TYPE, class... ARGS> A* search(ARGS... args) {
    auto object_template = TYPE(args...);
    for (auto const& object : objects) {
        auto typed_object = as<TYPE>(object);
        if (typed_object && *typed_object == object_template)
            return object;
    }
    return nullptr;
}

C'è qualche schema di programmazione che può essere utilizzato per risolvere questo tipo di problema di prestazioni di ricerca? Qualcuno ha già affrontato questo problema e ha una buona soluzione?

Grazie!

    
posta Deathicon 12.09.2017 - 15:40
fonte

4 risposte

2

Mi sembra che tu abbia già un sacco di codice già implementato e desideri ottimizzare la tua ricerca.

Inoltre, mi sembra che la tua ricerca non possa essere interamente rifattorizzata (cambiando i criteri del filtro).

Poiché hai già provato un miglioramento della ricerca solo all'interno del set del tipo specifico del tuo oggetto, aggiungerei l'approccio parallelo alla ricerca.

Fondamentalmente, nella tua ricerca (indipendentemente dal fatto che sia fatta da parametri variadici o per nome):

  • Interrogazione per un oggetto di tipo T;
  • Dall'array A1 originale (con oltre 300 tipi diversi), seleziona solo il sottoinsieme contenente oggetti di tipo T;
  • Ora hai un altro array A2;
  • Separa N pezzi della stessa dimensione dell'array A2 e crea un thread che eseguirà una ricerca lineare in ognuno di questi blocchi;
  • attendi che tutti gli N thread siano completati;
  • uno dei thread potrebbe trovare l'oggetto interrogato.

Pertanto, il tempo complessivo verrà ottimizzato in base al numero di thread definiti.

    
risposta data 12.09.2017 - 18:45
fonte
1

Le prestazioni hanno due parti:

  • Fai meno lavoro possibile.
  • Che lavoro fai, fai il più rapidamente possibile.

Qui la seconda parte può essere indirizzata abbastanza rapidamente: non utilizzare dynamic_cast , vedremo modi per evitarlo in un minuto. Non costruire lo stesso oggetto più e più volte. Per lo meno, vogliamo qualcosa di simile:

template <class TYPE>
A* search(TYPE const& expected) {
  static_assert( TYPE is subclass of A );
  for (TYPE* obj : magically_get_eligible_objects<TYPE>()) {
    if (obj && (*obj == expected))
      return obj;
  }
  return nullptr;
}

In C ++, i modelli e il polimorfismo non si adattano affatto bene. Non ci sarà una soluzione elegante e sicura per il tipo a magically_get_eligible_objects() . Ma se scriviamo il codice corretto possiamo prendere scorciatoie e violare il sistema di tipo C ++ in qualche modo in sicurezza. Per il resto di questa risposta, solo i TIPI di foglie sono rilevanti. Anche il tipo A* potrebbe essere void* .

Poiché conosci sempre il TIPO per una query, puoi suddividere gli oggetti per tipo. Non è necessario memorizzarli in una singola struttura dati. Ciò contribuirà a ridurre lo spazio di ricerca se la popolazione di oggetti è ben distribuita su più tipi: se al massimo il 30% degli oggetti ha lo stesso tipo, anche il tempo peggiore per una ricerca è sceso al 30% (più sovraccarico).

Per questo partizionamento puoi usare map<type_index, vector<A*>> o multimap<type_index, A*> nella condizione in cui usi type_index(typeid(obj)) di un oggetto come chiave per inserire, eliminare e cercare oggetti o intervalli di oggetti. Quando si ottengono iteratori o raccolte su un numero di oggetti che sono nominalmente di tipo A ma si conosce il loro TIPO effettivo, allora è possibile * tossire * reinterpret_cast delle raccolte. Dal momento che stai memorizzando solo i puntatori, questo dovrebbe essere sicuro nella pratica. Per i singoli puntatori puoi utilizzare static_cast in sicurezza.

Nota: se tutti gli oggetti hanno una costante name o altri dati che sono conosciuti da tutte ricerche, allora la chiave può essere un tuple<type_index, NameType> invece che dovrebbe portare ad un partizionamento sufficientemente buono . Continuerò a pensare che non sia così.

Probabilmente, potresti usare una struttura dati tuple<vector<TYPE1*>, vector<TYPE2*>, ..., vector<TYPEn*>> invece e cercare le partizioni per tipo. Questo evita alcuni cast ma è (a parte quella sicurezza di tipo e potenzialmente un po 'di performance di runtime) non fondamentalmente superiore a map<type_index, vector<void*>> . Questo rende le operazioni polimorfiche (come l'inserimento di un elemento in cui il TYPE non è noto) molto più difficile.

All'interno di una partizione, gli oggetti sarebbero ancora non ordinati, richiedendo la scansione lineare. Se i TYPE specifici hanno proprietà più forti, avremmo bisogno di introdurre un tipo di partizione personalizzato che possa essere specializzato per i singoli TIPI.

Avremmo bisogno di una partizione di base astratta per la compilazione del contenitore della partizione, qualcosa del tipo:

class BasePartition {
public:
  virtual void insert(void*) = 0;
  virtual void* search_by_name(NameType const&) = 0;
  virtual void* search_by_expected(void*) = 0;
};

Quindi una classe template che fa automaticamente riferimento a un vettore non isolato, ma potrebbe essere specializzata per strutture dati migliori:

template<class TYPE>
class Partition : BasePartition {
  vector<TYPE*> objects;
public:
  void insert(void* obj) override {
    objects.push_back(static_cast<TYPE*>(obj));
  }

  void* search_by_name(NameType const& name) override { ... }

  void* search_by_expected(void* untyped_expected) override {
    // may prefer dynamic_cast for more defensive code
    TYPE* untyped_expected = static_cast<TYPE*>(untyped_expected);
    ...
  }
};

Potremmo quindi avere un contenitore map<type_index, unique_ptr<BasePartition>> partitions . Dato un BasePartition* partition per il TYPE corretto, la funzione search() potrebbe quindi essere semplificata in

// partition casts internally
return static_cast<TYPE*>(partition->search_by_expected(&expected));

Possiamo quindi specializzare il modello Partition per i tipi in cui è possibile una ricerca più efficiente. Per esempio. se è possibile ordinare un tipo, potremmo utilizzare set anziché vector . Puoi anche conservare unordered_map s aggiuntivo per indicizzare gli oggetti con alcune proprietà, ad es. per nome. Se l'utilizzo della memoria aggiuntiva è un compromesso valido dipende dalla tua applicazione.

    
risposta data 12.09.2017 - 19:07
fonte
0

Solo un'idea pragmatica per l'indicizzazione / hashing migliorato:

  1. Crea un valore hash numerico ( uint ) non solo tenendo conto di TYPE (come hai già fatto nel tuo esempio), ma anche ARGS - queste sono le due proprietà che posso identificare per definire un gruppo di oggetti distinto dal tuo esempio .
  2. Memorizza i puntatori in un contenitore di hash multivalore ( std::unordered_multimap ), utilizzando il valore hash come chiave.
  3. Estrai tutti gli oggetti di un gruppo con std::unordered_multimap::equal_range() .

Con questo hai spostato parte dei calcoli necessari per confrontare gli oggetti nella creazione dell'hash e la ricerca di tutti i puntatori di uno specifico "tipo" / gruppo di oggetti dovrebbe essere il più veloce possibile.

    
risposta data 13.09.2017 - 14:08
fonte
0

Per me la tua funzione search ha troppe informazioni. Anche se ora lo hai parallelizzato ed è 6 volte più veloce, stai ancora facendo cose come cercare linearmente attraverso ogni singolo oggetto di un determinato tipo solo per vedere se due campi di stringa name corrispondono. Questo è ancora abbastanza grossolano anche se l'algoritmo di ricerca del tempo lineare è distribuito tra thread. È come il multithreading di un bubble sort quando avresti potuto usare quicksort.

I chiamanti ovviamente hanno più informazioni su ciò che stanno cercando quando chiamano la funzione search perché sia in grado di fare qualcosa di molto, molto più intelligente della ricerca lineare attraverso tutti gli oggetti dello stesso tipo che controllano l'uguaglianza. Altrimenti la funzione search non sarebbe in grado di restringere il risultato a un singolo oggetto.

Quindi per me vale la pena identificare il tipo di ricerche che fai più spesso e non usare questa funzione generalizzata search in quei percorsi critici. Se fai name confronti di stringhe spesso per trovare oggetti con un nome corrispondente, allora verrebbe probabilmente rimosso più tempo del necessario per costruire e aggiornare la struttura quando aggiungi / rimuovi oggetti se hai costruito un contenitore associativo come un binario tree (map), trie, o una tabella hash (unordered_map) che associa chiavi di stringa a puntatori di oggetti. Puoi quindi utilizzare una funzione search_by_name per recuperare oggetti con un nome corrispondente utilizzando questa struttura dati per accelerare le ricerche.

Ora hai migliorato l'algoritmo di ricerca da tempo lineare a tempo logaritmico o persino costante e le tue ricerche saranno potenzialmente molto più veloci con un singolo thread in tempo costante rispetto a anche multithread in tempo lineare oltre a essere in grado di scalare meglio e ora puoi parallelizzare le ricerche più economiche se vuoi ancora il multithread invece di dedicare tutto il tuo hardware a una ricerca lineare costosa solo per trovare un oggetto con un nome corrispondente, ad esempio

La tua funzione search con multithreading potrebbe ancora essere utile come ripiego per casi rari, ma per i casi comuni mi degeneralizzare un po 'e iniziare a costruire strutture associative appropriate e funzioni di ricerca corrispondenti per velocizzare enormemente queste ricerche per tipi di ricerche più comuni che esegui.

    
risposta data 30.11.2017 - 03:18
fonte

Leggi altre domande sui tag