Libreria di classi generiche - bug, descrizione, domande, caratteristiche d'uso e suggerimenti - pagina 19

 
Vasiliy Sokolov:
potete scrivere la vostra funzione di specializzazione per la classe, dopo tutto.
 
Combinatore:
Si può scrivere la propria specializzazione di una funzione per una classe, dopo tutto.

Non si può fare senza un'interfaccia.

 
Vasiliy Sokolov:

Non si può fare senza un'interfaccia.

Non si può fare cosa senza un'interfaccia? )
 
Combinatore:
Non puoi farlo senza un'interfaccia? )

Beh, pensateci. Comunque, non ingombrare il thread, per favore.

 
Vasiliy Sokolov:

Problema

Ovviamente, GetHashCode non può essere implementato correttamente nell'ambiente utente MQL5. Questo perché non tutti gli oggetti sono accessibili. E se l'implementazione funziona bene per i membri primitivi come double int, ecc., per gli oggetti complessi (classi, strutture e persino enumerazioni) l'hash è calcolato per nome. Se abbiamo migliaia di CObjects o anche ENUM_something_that, allora CHashMap e CHashSet degenereranno in LinkedList:

Non possiamo evitarlo perché tutto ciò che abbiamo a livello utente è il nome dell'oggetto:

Quindi, gli hash di tutti gli oggetti di tipo complesso sono uguali tra loro e questo codice di ricerca qui comporta un'enumerazione di array completa:

In effetti, questo è così critico che scavalca l'intero punto di usare Generic alla radice. Il punto è che nella maggior parte dei casi è necessario associare o memorizzare oggetti complessi: enumerazioni, strutture o classi. Se avete bisogno di operare con tipi semplici, potete fare con qualcosa di più semplice.

Affinché le collezioni generiche funzionino correttamente con gli oggetti delle classi, queste classi devono implementare l'interfaccia IEqualityComparable dove sono definiti i metodi Equals e HashCode. Significa che l'utente deve impostare i metodi di calcolo dei codici hash da solo, e questa è l'unica opzione finora, perché è impossibile implementare questi metodi automaticamente, come si fa in .Net, per esempio, per mezzo di MQL5.

 
Roman Konopelko:

Affinché le collezioni generiche funzionino correttamente con gli oggetti delle classi, queste classi devono implementare l'interfaccia IEqualityComparable, in cui sono definiti i metodi Equals e HashCode. Significa che l'utente deve impostare i metodi di calcolo dei codici hash e questa è l'unica opzione finora, perché è impossibile implementare questi metodi automaticamente, come è stato fatto in .Net, per esempio, per mezzo di MQL5.

Roman, hai dimenticato di dire che in MQL5 non ha interfacce. Questo articolo introdurrà il concetto di interfacce in MQL5.

p.s. Ma anche se le interfacce apparissero in MQL5, il problema delle strutture e delle enumerazioni rimarrebbe irrisolto.

 
Vasiliy Sokolov:

Roman, hai dimenticato di dire che in MQL5 non ha interfacce. Qualsiasi discussione sulle interfacce nel MQL5 di oggi è un'insinuazione maliziosa e demagogica.

p.s. Ma anche se fossero apparse le interfacce in MQL5, il problema con le strutture e le enumerazioni sarebbe rimasto irrisolto.

In MQL5 al momento non è possibile scrivere metodi template che funzionino per classi, strutture ed enumerazioni allo stesso tempo, a causa delle peculiarità del trasferimento dei dati.
 
Roman Konopelko:
Per ora in MQL5 non è possibile in linea di principio scrivere metodi template che funzionino per classi, strutture ed enumerazioni allo stesso tempo, a causa delle peculiarità del trasferimento dei dati.

Ecco di cosa sto parlando. Ma l'ambiente MQL5 sa tutto dei suoi oggetti! Ha puntatori a oggetti e conosce tutti gli identificatori di enum (EnumToString). Ecco perché abbiamo bisogno di GetHashCode come sistema e funzione onnivora.

Inoltre, finalmente permettere l'ereditarietà dell'interfaccia multipla. Riscrivete Generic per le interfacce normali e avrete uno sweet spot.

 

La situazione è ovvia: gli sviluppatori di MQ sono stati così spesso scottati dall'ereditarietà multipla in C++ che ora temono qualsiasi manifestazione di essa. Di conseguenza, si propone di evitare una schifezza (eredità multipla) usando un'altra schifezza: ridicole catene di eredità.

Dovete capire che le interfacce non hanno niente a che fare con l'ereditarietà. Un'interfaccia è una dichiarazione che una classe impegna per fornire una data funzionalità. Se due classi implementano la stessa funzionalità, non dovrebbero ereditare l'una dall'altra. Eredità = male.

 

Forum sul trading, sistemi di trading automatico e test di strategie di trading

Libreria di classi generiche - bug, descrizione, domande, particolarità d'uso e suggerimenti

Roman Konopelko, 2017.12.18 16:29

1) Il fattore di crescita del volume (capacità) non è uguale a 1,2, il metodo CPrimeGenerator::ExpandPrime è usato per calcolare il nuovo volume in CHashMap:

int CPrimeGenerator::ExpandPrime(const int old_size)
  {
   int new_size=2*old_size;
   if((uint)new_size>INT_MAX && INT_MAX>old_size)
      return INT_MAX;
   else
      return GetPrime(new_size);
  }

In questo metodo, la vecchia dimensione della collezione viene moltiplicata per due, poi per il valore risultante troviamo il più vicino al numero primo superiore e lo restituiamo come nuovo volume.

Sul valore iniziale della capacità - sono d'accordo, è molto piccolo.

Ma d'altra parte ci sono sempre i costruttori, dove si può specificare esplicitamente la capacità iniziale:

class CHashMap: public IMap<TKey,TValue>
  {
public:
                     CHashMap(const int capacity);
                     CHashMap(const int capacity,IEqualityComparer<TKey>*comparer);
  }

Quindi non vedo molto senso di cambiare qualcosa qui.


Sì, ho sbagliato, mi pento.
In realtà, il fattore di incremento del volume (capacità) per CHashMap è più di 2.
Grazie per aver sottolineato l'errore e mi scuso per aver perso tempo.



D'altra parte sono riuscito a prendere tempo per studiare l'implementazione di CPrimeGenerator.

//+------------------------------------------------------------------+
//| Fast generator of parime value.                                  |
//+------------------------------------------------------------------+
int CPrimeGenerator::GetPrime(const int min)
  {
//--- a typical resize algorithm would pick the smallest prime number in this array
//--- that is larger than twice the previous capacity. 
//--- get next prime value from table
   for(int i=0; i<ArraySize(s_primes); i++)
     {
      int prime=s_primes[i];
      if(prime>=min)
         return(prime);
     }
//--- outside of our predefined table
   for(int i=(min|1); i<INT_MAX;i+=2)
     {
      if(IsPrime(i) && ((i-1)%s_hash_prime!=0))
         return(i);
     }
   return(min);
  }


E ci sono alcuni suggerimenti, soprattutto per migliorare le prestazioni.


1. Eliminare i comportamenti ambigui:
Se passiamo "INT_MAX - 10" come parametro a CPrimeGenerator::ExpandPrime, il risultato "INT_MAX" sarà restituito.
Se passiamo "INT_MAX - 10" come parametro a CPrimeGenerator::GetPrime, verrà restituito lo stesso risultato: "INT_MAX - 10".

Inoltre, in entrambi i casi, il valore restituito non è un numero primo, il che inganna l'utente.



2. Quando si chiamaGetPrime per numeri maggiori di7199369, il risparmio di memoria diventa una priorità, ma questo non giustifica la relativa scarsa performance e i calcoli inutili.

Suggerimento:
- aggiungere un confronto del numero con l'ultimo valore dell'arrayCPrimeGenerator::s_primes[] e non eseguire un'inutile enumerazione di tutti i 72 elementi dell'array.
- Sostituisce la ricerca dinamica di un numero semplice (passa attraverso tutti i numeri in una riga) con un array di valori predefiniti comeCPrimeGenerator::s_primes[], ma con incremento lineare, non quadratico.
L'incremento dei valori sarà di circa 1 milione (una cifra simile all'incremento di s_primes sugli ultimi elementi dell'array).
Il numero di elementi è fino a 3000, i valori vanno da 8M a INT_MAX.
L'array sarà cercato attraverso una ricerca binaria upper bound, il numero di iterazioni richieste è 12.


3. Quando si chiamaGetPrime per numeri inferiori a7199369, il caso peggiore è una ricerca lineare di tutti i 72 valori dell'arrayCPrimeGenerator::s_primes[].

Il suggerimento è:
- ridurre il numero di elementi dell'array a 70. (cancellando i primi due, o il primo e l'ultimo):

const static int  CPrimeGenerator::s_primes[]=
  {
   3,7,11,17,23,29,37,47,59,71,89,107,131,163,197,239,293,353,431,521,631,761,919,
   1103,1327,1597,1931,2333,2801,3371,4049,4861,5839,7013,8419,10103,12143,14591,
   17519,21023,25229,30293,36353,43627,52361,62851,75431,90523,108631,130363,156437,
   187751,225307,270371,324449,389357,467237,560689,672827,807403,968897,1162687,1395263,
   1674319,2009191,2411033,2893249,3471899,4166287,4999559,5999471,7199369
  };

- se il valore di input è minore o uguale al 6° valore nella nuova matriceCPrimeGenerator::s_primes- allora itera linearmente attraverso i numeri (fino a 6 confronti).
- Altrimenti usa la ricerca binaria upper bound tra il 7° e il 70° valore dell'array (circa 6 iterazioni).

L'idea è di usare la ricerca lineare solo finché non c'è perdita di prestazioni rispetto alla ricerca binaria.
Il numero suggerito di elementi - 6 è usato come esempio, in realtà tutto dipende dall'implementazione concreta della ricerca binaria upper bound.
Il guadagno complessivo di prestazioni dovuto alla bassa intensità delle chiamate di una particolare funzione può non essere così vantaggioso da rendere necessario un lavoro per migliorare questa funzionalità.

Motivazione: