Bene, per mettere a tacere questa cosa, ho creato un'app di prova per eseguire un paio di scenari e ottenere alcune visualizzazioni dei risultati. Ecco come vengono eseguiti i test:
- Sono state provate raccolte di diverse dimensioni: cento, mille e centomila voci.
- Le chiavi utilizzate sono istanze di una classe identificate in modo univoco da un ID. Ogni test utilizza chiavi univoche, con numeri interi incrementali come ID. Il
equalsmetodo utilizza solo l'ID, quindi nessuna mappatura dei tasti ne sovrascrive un altro.
- Le chiavi ottengono un codice hash costituito dal resto del modulo del loro ID rispetto a un numero preimpostato. Chiameremo quel numero limite di hash . Questo mi ha permesso di controllare il numero di collisioni di hash previste. Ad esempio, se la dimensione della nostra raccolta è 100, avremo chiavi con ID compresi tra 0 e 99. Se il limite hash è 100, ogni chiave avrà un codice hash univoco. Se il limite hash è 50, la chiave 0 avrà lo stesso codice hash della chiave 50, 1 avrà lo stesso codice hash di 51 ecc. In altre parole, il numero previsto di collisioni hash per chiave è la dimensione della raccolta divisa per l'hash limite.
- Per ogni combinazione di dimensione della raccolta e limite hash, ho eseguito il test utilizzando mappe hash inizializzate con impostazioni diverse. Queste impostazioni sono il fattore di carico e una capacità iniziale espressa come fattore dell'impostazione di raccolta. Ad esempio, un test con una dimensione di raccolta di 100 e un fattore di capacità iniziale di 1,25 inizializzerà una mappa hash con una capacità iniziale di 125.
- Il valore per ogni chiave è semplicemente nuovo
Object.
- Ogni risultato del test è incapsulato in un'istanza di una classe Result. Alla fine di tutti i test, i risultati vengono ordinati dalla peggiore prestazione complessiva alla migliore.
- Il tempo medio per put e get è calcolato per 10 put / get.
- Tutte le combinazioni di test vengono eseguite una volta per eliminare l'influenza della compilazione JIT. Successivamente, vengono eseguiti i test per i risultati effettivi.
Ecco la classe:
package hashmaptest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
public class HashMapTest {
private static final List<Result> results = new ArrayList<Result>();
public static void main(String[] args) throws IOException {
final int[][] sampleSizesAndHashLimits = new int[][] {
{100, 50, 90, 100},
{1000, 500, 900, 990, 1000},
{100000, 10000, 90000, 99000, 100000}
};
final double[] initialCapacityFactors = new double[] {0.5, 0.75, 1.0, 1.25, 1.5, 2.0};
final float[] loadFactors = new float[] {0.5f, 0.75f, 1.0f, 1.25f};
for(int[] sizeAndLimits : sampleSizesAndHashLimits) {
int size = sizeAndLimits[0];
for(int i = 1; i < sizeAndLimits.length; ++i) {
int limit = sizeAndLimits[i];
for(double initCapacityFactor : initialCapacityFactors) {
for(float loadFactor : loadFactors) {
runTest(limit, size, initCapacityFactor, loadFactor);
}
}
}
}
results.clear();
for(int[] sizeAndLimits : sampleSizesAndHashLimits) {
int size = sizeAndLimits[0];
for(int i = 1; i < sizeAndLimits.length; ++i) {
int limit = sizeAndLimits[i];
for(double initCapacityFactor : initialCapacityFactors) {
for(float loadFactor : loadFactors) {
runTest(limit, size, initCapacityFactor, loadFactor);
}
}
}
}
Collections.sort(results);
for(final Result result : results) {
result.printSummary();
}
}
private static void runTest(final int hashLimit, final int sampleSize,
final double initCapacityFactor, final float loadFactor) {
final int initialCapacity = (int)(sampleSize * initCapacityFactor);
System.out.println("Running test for a sample collection of size " + sampleSize
+ ", an initial capacity of " + initialCapacity + ", a load factor of "
+ loadFactor + " and keys with a hash code limited to " + hashLimit);
System.out.println("====================");
double hashOverload = (((double)sampleSize/hashLimit) - 1.0) * 100.0;
System.out.println("Hash code overload: " + hashOverload + "%");
final List<Key> keys = generateSamples(hashLimit, sampleSize);
final List<Object> values = generateValues(sampleSize);
final HashMap<Key, Object> map = new HashMap<Key, Object>(initialCapacity, loadFactor);
final long startPut = System.nanoTime();
for(int i = 0; i < sampleSize; ++i) {
map.put(keys.get(i), values.get(i));
}
final long endPut = System.nanoTime();
final long putTime = endPut - startPut;
final long averagePutTime = putTime/(sampleSize/10);
System.out.println("Time to map all keys to their values: " + putTime + " ns");
System.out.println("Average put time per 10 entries: " + averagePutTime + " ns");
final long startGet = System.nanoTime();
for(int i = 0; i < sampleSize; ++i) {
map.get(keys.get(i));
}
final long endGet = System.nanoTime();
final long getTime = endGet - startGet;
final long averageGetTime = getTime/(sampleSize/10);
System.out.println("Time to get the value for every key: " + getTime + " ns");
System.out.println("Average get time per 10 entries: " + averageGetTime + " ns");
System.out.println("");
final Result result =
new Result(sampleSize, initialCapacity, loadFactor, hashOverload, averagePutTime, averageGetTime, hashLimit);
results.add(result);
System.gc();
try {
Thread.sleep(200);
} catch(final InterruptedException e) {}
}
private static List<Key> generateSamples(final int hashLimit, final int sampleSize) {
final ArrayList<Key> result = new ArrayList<Key>(sampleSize);
for(int i = 0; i < sampleSize; ++i) {
result.add(new Key(i, hashLimit));
}
return result;
}
private static List<Object> generateValues(final int sampleSize) {
final ArrayList<Object> result = new ArrayList<Object>(sampleSize);
for(int i = 0; i < sampleSize; ++i) {
result.add(new Object());
}
return result;
}
private static class Key {
private final int hashCode;
private final int id;
Key(final int id, final int hashLimit) {
this.id = id;
this.hashCode = id % hashLimit;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(final Object o) {
return ((Key)o).id == this.id;
}
}
static class Result implements Comparable<Result> {
final int sampleSize;
final int initialCapacity;
final float loadFactor;
final double hashOverloadPercentage;
final long averagePutTime;
final long averageGetTime;
final int hashLimit;
Result(final int sampleSize, final int initialCapacity, final float loadFactor,
final double hashOverloadPercentage, final long averagePutTime,
final long averageGetTime, final int hashLimit) {
this.sampleSize = sampleSize;
this.initialCapacity = initialCapacity;
this.loadFactor = loadFactor;
this.hashOverloadPercentage = hashOverloadPercentage;
this.averagePutTime = averagePutTime;
this.averageGetTime = averageGetTime;
this.hashLimit = hashLimit;
}
@Override
public int compareTo(final Result o) {
final long putDiff = o.averagePutTime - this.averagePutTime;
final long getDiff = o.averageGetTime - this.averageGetTime;
return (int)(putDiff + getDiff);
}
void printSummary() {
System.out.println("" + averagePutTime + " ns per 10 puts, "
+ averageGetTime + " ns per 10 gets, for a load factor of "
+ loadFactor + ", initial capacity of " + initialCapacity
+ " for " + sampleSize + " mappings and " + hashOverloadPercentage
+ "% hash code overload.");
}
}
}
L'esecuzione di questo potrebbe richiedere del tempo. I risultati vengono stampati su standard out. Potresti notare che ho commentato una riga. Quella linea chiama un visualizzatore che genera rappresentazioni visive dei risultati in file png. La classe per questo è data di seguito. Se desideri eseguirlo, rimuovi il commento dalla riga appropriata nel codice sopra. Attenzione: la classe visualizer presuppone che tu stia eseguendo su Windows e creerà cartelle e file in C: \ temp. Quando si esegue su un'altra piattaforma, regolare questo.
package hashmaptest;
import hashmaptest.HashMapTest.Result;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.imageio.ImageIO;
public class ResultVisualizer {
private static final Map<Integer, Map<Integer, Set<Result>>> sampleSizeToHashLimit =
new HashMap<Integer, Map<Integer, Set<Result>>>();
private static final DecimalFormat df = new DecimalFormat("0.00");
static void visualizeResults(final List<Result> results) throws IOException {
final File tempFolder = new File("C:\\temp");
final File baseFolder = makeFolder(tempFolder, "hashmap_tests");
long bestPutTime = -1L;
long worstPutTime = 0L;
long bestGetTime = -1L;
long worstGetTime = 0L;
for(final Result result : results) {
final Integer sampleSize = result.sampleSize;
final Integer hashLimit = result.hashLimit;
final long putTime = result.averagePutTime;
final long getTime = result.averageGetTime;
if(bestPutTime == -1L || putTime < bestPutTime)
bestPutTime = putTime;
if(bestGetTime <= -1.0f || getTime < bestGetTime)
bestGetTime = getTime;
if(putTime > worstPutTime)
worstPutTime = putTime;
if(getTime > worstGetTime)
worstGetTime = getTime;
Map<Integer, Set<Result>> hashLimitToResults =
sampleSizeToHashLimit.get(sampleSize);
if(hashLimitToResults == null) {
hashLimitToResults = new HashMap<Integer, Set<Result>>();
sampleSizeToHashLimit.put(sampleSize, hashLimitToResults);
}
Set<Result> resultSet = hashLimitToResults.get(hashLimit);
if(resultSet == null) {
resultSet = new HashSet<Result>();
hashLimitToResults.put(hashLimit, resultSet);
}
resultSet.add(result);
}
System.out.println("Best average put time: " + bestPutTime + " ns");
System.out.println("Best average get time: " + bestGetTime + " ns");
System.out.println("Worst average put time: " + worstPutTime + " ns");
System.out.println("Worst average get time: " + worstGetTime + " ns");
for(final Integer sampleSize : sampleSizeToHashLimit.keySet()) {
final File sizeFolder = makeFolder(baseFolder, "sample_size_" + sampleSize);
final Map<Integer, Set<Result>> hashLimitToResults =
sampleSizeToHashLimit.get(sampleSize);
for(final Integer hashLimit : hashLimitToResults.keySet()) {
final File limitFolder = makeFolder(sizeFolder, "hash_limit_" + hashLimit);
final Set<Result> resultSet = hashLimitToResults.get(hashLimit);
final Set<Float> loadFactorSet = new HashSet<Float>();
final Set<Integer> initialCapacitySet = new HashSet<Integer>();
for(final Result result : resultSet) {
loadFactorSet.add(result.loadFactor);
initialCapacitySet.add(result.initialCapacity);
}
final List<Float> loadFactors = new ArrayList<Float>(loadFactorSet);
final List<Integer> initialCapacities = new ArrayList<Integer>(initialCapacitySet);
Collections.sort(loadFactors);
Collections.sort(initialCapacities);
final BufferedImage putImage =
renderMap(resultSet, loadFactors, initialCapacities, worstPutTime, bestPutTime, false);
final BufferedImage getImage =
renderMap(resultSet, loadFactors, initialCapacities, worstGetTime, bestGetTime, true);
final String putFileName = "size_" + sampleSize + "_hlimit_" + hashLimit + "_puts.png";
final String getFileName = "size_" + sampleSize + "_hlimit_" + hashLimit + "_gets.png";
writeImage(putImage, limitFolder, putFileName);
writeImage(getImage, limitFolder, getFileName);
}
}
}
private static File makeFolder(final File parent, final String folder) throws IOException {
final File child = new File(parent, folder);
if(!child.exists())
child.mkdir();
return child;
}
private static BufferedImage renderMap(final Set<Result> results, final List<Float> loadFactors,
final List<Integer> initialCapacities, final float worst, final float best,
final boolean get) {
final Color[][] map = new Color[initialCapacities.size()][loadFactors.size()];
for(final Result result : results) {
final int x = initialCapacities.indexOf(result.initialCapacity);
final int y = loadFactors.indexOf(result.loadFactor);
final float time = get ? result.averageGetTime : result.averagePutTime;
final float score = (time - best)/(worst - best);
final Color c = new Color(score, 1.0f - score, 0.0f);
map[x][y] = c;
}
final int imageWidth = initialCapacities.size() * 40 + 50;
final int imageHeight = loadFactors.size() * 40 + 50;
final BufferedImage image =
new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_3BYTE_BGR);
final Graphics2D g = image.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, imageWidth, imageHeight);
for(int x = 0; x < map.length; ++x) {
for(int y = 0; y < map[x].length; ++y) {
g.setColor(map[x][y]);
g.fillRect(50 + x*40, imageHeight - 50 - (y+1)*40, 40, 40);
g.setColor(Color.BLACK);
g.drawLine(25, imageHeight - 50 - (y+1)*40, 50, imageHeight - 50 - (y+1)*40);
final Float loadFactor = loadFactors.get(y);
g.drawString(df.format(loadFactor), 10, imageHeight - 65 - (y)*40);
}
g.setColor(Color.BLACK);
g.drawLine(50 + (x+1)*40, imageHeight - 50, 50 + (x+1)*40, imageHeight - 15);
final int initialCapacity = initialCapacities.get(x);
g.drawString(((initialCapacity%1000 == 0) ? "" + (initialCapacity/1000) + "K" : "" + initialCapacity), 15 + (x+1)*40, imageHeight - 25);
}
g.drawLine(25, imageHeight - 50, imageWidth, imageHeight - 50);
g.drawLine(50, 0, 50, imageHeight - 25);
g.dispose();
return image;
}
private static void writeImage(final BufferedImage image, final File folder,
final String filename) throws IOException {
final File imageFile = new File(folder, filename);
ImageIO.write(image, "png", imageFile);
}
}
L'output visualizzato è il seguente:
- I test vengono divisi prima per dimensione della raccolta, quindi per limite hash.
- Per ogni test, c'è un'immagine di output riguardante il tempo di put medio (per 10 put) e il tempo di get medio (per 10 get). Le immagini sono "mappe termiche" bidimensionali che mostrano un colore per combinazione di capacità iniziale e fattore di carico.
- I colori nelle immagini si basano sul tempo medio su una scala normalizzata dal miglior risultato al peggiore, che va dal verde saturo al rosso saturo. In altre parole, il tempo migliore sarà completamente verde, mentre il tempo peggiore sarà completamente rosso. Due diverse misurazioni del tempo non dovrebbero mai avere lo stesso colore.
- Le mappe dei colori vengono calcolate separatamente per put e gets, ma comprendono tutti i test per le rispettive categorie.
- Le visualizzazioni mostrano la capacità iniziale sull'asse xe il fattore di carico sull'asse y.
Senza ulteriori indugi, diamo un'occhiata ai risultati. Inizierò con i risultati per i put.
Metti i risultati
Dimensione della raccolta: 100. Limite hash: 50. Ciò significa che ogni codice hash deve essere ripetuto due volte e ogni altra chiave entra in collisione nella mappa hash.

Bene, questo non inizia molto bene. Vediamo che c'è un grande hotspot per una capacità iniziale del 25% superiore alla dimensione della raccolta, con un fattore di carico di 1. L'angolo inferiore sinistro non funziona troppo bene.
Dimensioni della raccolta: 100. Limite hash: 90. Una chiave su dieci ha un codice hash duplicato.

Questo è uno scenario leggermente più realistico, non avendo una funzione hash perfetta ma ancora un sovraccarico del 10%. L'hotspot è sparito, ma la combinazione di una bassa capacità iniziale con un basso fattore di carico ovviamente non funziona.
Dimensioni della raccolta: 100. Limite hash: 100. Ciascuna chiave come codice hash univoco. Non sono previste collisioni se ci sono abbastanza secchi.

Una capacità iniziale di 100 con un fattore di carico di 1 sembra soddisfacente. Sorprendentemente, una capacità iniziale più elevata con un fattore di carico inferiore non è necessariamente buona.
Dimensioni della raccolta: 1000. Limite hash: 500. Qui sta diventando più serio, con 1000 voci. Proprio come nel primo test, c'è un sovraccarico di hash di 2 a 1.

L'angolo inferiore sinistro continua a non funzionare bene. Ma sembra esserci una simmetria tra la combinazione di conteggio iniziale inferiore / fattore di carico alto e conteggio iniziale più alto / fattore di carico basso.
Dimensioni della raccolta: 1000. Limite hash: 900. Ciò significa che un codice hash su dieci verrà ripetuto due volte. Scenario ragionevole per quanto riguarda le collisioni.

C'è qualcosa di molto divertente in corso con l'improbabile combinazione di una capacità iniziale troppo bassa con un fattore di carico superiore a 1, il che è piuttosto controintuitivo. Altrimenti, ancora abbastanza simmetrico.
Dimensioni della raccolta: 1000. Limite hash: 990. Alcune collisioni, ma solo poche. Piuttosto realistico sotto questo aspetto.

Abbiamo una bella simmetria qui. L'angolo inferiore sinistro non è ancora ottimale, ma le combo capacità di 1000 init / fattore di carico 1.0 contro capacità di 1250 init / fattore di carico 0,75 sono allo stesso livello.
Dimensioni della raccolta: 1000. Limite hash: 1000. Nessun codice hash duplicato, ma ora con una dimensione del campione di 1000.

Non c'è molto da dire qui. La combinazione di una capacità iniziale più elevata con un fattore di carico di 0,75 sembra superare leggermente la combinazione di una capacità iniziale di 1000 con un fattore di carico di 1.
Dimensioni della collezione: 100_000. Limite hash: 10_000. Va bene, ora si fa sul serio, con una dimensione del campione di centomila e 100 duplicati di codice hash per chiave.

Yikes! Penso che abbiamo trovato il nostro spettro più basso. Una capacità di inizializzazione esattamente della dimensione della collezione con un fattore di carico di 1 sta andando molto bene qui, ma a parte questo è in tutto il negozio.
Dimensioni della collezione: 100_000. Limite hash: 90_000. Un po 'più realistico rispetto al test precedente, qui abbiamo un sovraccarico del 10% nei codici hash.

L'angolo inferiore sinistro è ancora indesiderabile. Capacità iniziali più elevate funzionano meglio.
Dimensioni della collezione: 100_000. Limite hash: 99_000. Ottimo scenario, questo. Una vasta raccolta con un sovraccarico di codice hash dell'1%.

L'utilizzo della dimensione esatta della raccolta come capacità di inizializzazione con un fattore di carico di 1 vince qui! Tuttavia, capacità di inizializzazione leggermente maggiori funzionano abbastanza bene.
Dimensioni della collezione: 100_000. Limite hash: 100_000. Quello grande. La più grande collezione con una perfetta funzione hash.

Alcune cose sorprendenti qui. Una capacità iniziale con il 50% di spazio aggiuntivo con un fattore di carico di 1 vince.
Va bene, è tutto per le put. Ora, controlleremo i risultati. Ricorda, le mappe sottostanti sono tutte relative ai tempi di recupero migliori / peggiori, i tempi di put non vengono più presi in considerazione.
Ottieni risultati
Dimensioni della raccolta: 100. Limite hash: 50. Ciò significa che ogni codice hash dovrebbe essere ripetuto due volte e ci si aspettava che ogni altra chiave entrasse in collisione nella mappa hash.

Eh ... cosa?
Dimensioni della raccolta: 100. Limite hash: 90. Una chiave su dieci ha un codice hash duplicato.

Whoa Nelly! Questo è lo scenario più probabile da correlare alla domanda del richiedente, e apparentemente una capacità iniziale di 100 con un fattore di carico di 1 è una delle cose peggiori qui! Giuro di non aver simulato questo.
Dimensioni della raccolta: 100. Limite hash: 100. Ciascuna chiave come codice hash univoco. Non sono previste collisioni.

Questo sembra un po 'più tranquillo. Per lo più gli stessi risultati su tutta la linea.
Dimensioni della raccolta: 1000. Limite hash: 500. Proprio come nel primo test, c'è un sovraccarico hash di 2 a 1, ma ora con molte più voci.

Sembra che qualsiasi impostazione produrrà un risultato decente qui.
Dimensioni della raccolta: 1000. Limite hash: 900. Ciò significa che un codice hash su dieci verrà ripetuto due volte. Scenario ragionevole per quanto riguarda le collisioni.

E proprio come con i put per questa configurazione, otteniamo un'anomalia in un punto strano.
Dimensioni della raccolta: 1000. Limite hash: 990. Alcune collisioni, ma solo poche. Piuttosto realistico sotto questo aspetto.

Prestazioni decenti ovunque, fatta eccezione per la combinazione di un'elevata capacità iniziale con un basso fattore di carico. Mi aspetto questo per i put, poiché potrebbero essere previsti due ridimensionamenti della mappa hash. Ma perché succede?
Dimensioni della raccolta: 1000. Limite hash: 1000. Nessun codice hash duplicato, ma ora con una dimensione del campione di 1000.

Una visualizzazione assolutamente non spettacolare. Sembra funzionare qualunque cosa accada.
Dimensioni della collezione: 100_000. Limite hash: 10_000. Entrando di nuovo nei 100K, con un sacco di codice hash sovrapposto.

Non sembra carino, anche se i punti negativi sono molto localizzati. Le prestazioni qui sembrano dipendere in gran parte da una certa sinergia tra le impostazioni.
Dimensioni della collezione: 100_000. Limite hash: 90_000. Un po 'più realistico rispetto al test precedente, qui abbiamo un sovraccarico del 10% nei codici hash.

Molta varianza, anche se strizzando gli occhi puoi vedere una freccia che punta verso l'angolo in alto a destra.
Dimensioni della collezione: 100_000. Limite hash: 99_000. Ottimo scenario, questo. Una vasta raccolta con un sovraccarico di codice hash dell'1%.

Molto caotico. È difficile trovare molta struttura qui.
Dimensioni della collezione: 100_000. Limite hash: 100_000. Quello grande. La più grande collezione con una perfetta funzione hash.

Qualcun altro pensa che questa stia iniziando a somigliare alla grafica Atari? Ciò sembra favorire una capacità iniziale esattamente della dimensione della raccolta, -25% o + 50%.
Va bene, è ora di trarre conclusioni ...
- Per quanto riguarda i tempi di put: ti consigliamo di evitare capacità iniziali inferiori al numero previsto di voci sulla mappa. Se un numero esatto è noto in anticipo, quel numero o qualcosa di leggermente superiore sembra funzionare meglio. Fattori di carico elevati possono compensare capacità iniziali inferiori a causa di precedenti ridimensionamenti delle mappe hash. Per capacità iniziali più elevate, non sembrano avere molta importanza.
- Per quanto riguarda i tempi di recupero: i risultati sono leggermente caotici qui. Non c'è molto da concludere. Sembra fare molto affidamento su rapporti sottili tra la sovrapposizione del codice hash, la capacità iniziale e il fattore di carico, con alcune configurazioni presumibilmente cattive che funzionano bene e buone configurazioni che si comportano in modo pessimo.
- Apparentemente sono pieno di schifezze quando si tratta di supposizioni sulle prestazioni di Java. La verità è che, a meno che tu non stia adattando perfettamente le tue impostazioni all'implementazione di
HashMap, i risultati saranno ovunque. Se c'è una cosa da togliere da questo, è che la dimensione iniziale predefinita di 16 è un po 'stupida per qualsiasi cosa tranne le mappe più piccole, quindi usa un costruttore che imposta la dimensione iniziale se hai qualche idea su quale ordine di dimensione sarà.
- Stiamo misurando in nanosecondi qui. Il miglior tempo medio per 10 put è stato di 1179 ns e il peggior 5105 ns sulla mia macchina. Il miglior tempo medio per 10 risultati è stato di 547 ns e il peggiore 3484 ns. Potrebbe essere una differenza di fattore 6, ma stiamo parlando di meno di un millisecondo. Su collezioni molto più grandi di quanto aveva in mente il poster originale.
Bene, questo è tutto. Spero che il mio codice non abbia una supervisione orrenda che invalida tutto ciò che ho pubblicato qui. È stato divertente e ho imparato che alla fine potresti fare affidamento su Java per fare il suo lavoro piuttosto che aspettarti molta differenza da piccole ottimizzazioni. Questo non vuol dire che alcune cose non debbano essere evitate, ma stiamo parlando principalmente della costruzione di lunghe stringhe in cicli for, utilizzando le strutture dati sbagliate e creando un algoritmo O (n ^ 3).