Tempi di esecuzione imprevisti per il codice HashSet


28

Quindi originariamente avevo questo codice:

import java.util.*;

public class sandbox {
    public static void main(String[] args) {
        HashSet<Integer> hashSet = new HashSet<>();
        for (int i = 0; i < 100_000; i++) {
            hashSet.add(i);
        }

        long start = System.currentTimeMillis();

        for (int i = 0; i < 100_000; i++) {
            for (Integer val : hashSet) {
                if (val != -1) break;
            }

            hashSet.remove(i);
        }

        System.out.println("time: " + (System.currentTimeMillis() - start));
    }
}

Ci vogliono circa 4 secondi per eseguire i loop nidificati sul mio computer e non capisco perché ci sia voluto così tanto tempo. Il ciclo esterno viene eseguito 100.000 volte, il ciclo interno interno dovrebbe essere eseguito 1 volta (poiché qualsiasi valore di hashSet non sarà mai -1) e la rimozione di un elemento da un HashSet è O (1), quindi dovrebbero esserci circa 200.000 operazioni. Se in genere ci sono 100.000.000 di operazioni in un secondo, come mai il mio codice impiega 4 secondi per essere eseguito?

Inoltre, se la riga hashSet.remove(i);viene commentata, il codice richiede solo 16ms. Se il ciclo interno interno viene commentato (ma non hashSet.remove(i);), il codice richiede solo 8 ms.


4
Confermo i tuoi risultati. Potrei speculare sul motivo, ma spero che qualcuno intelligente scriverà una spiegazione affascinante.
khelwood

1
Sembra che il for valciclo sia la cosa che richiede tempo. Il removeè ancora molto veloce. Una sorta di overhead che imposta un nuovo iteratore dopo che il set è stato modificato ...?
khelwood,

@apangin ha fornito una buona spiegazione in stackoverflow.com/a/59522575/108326 sul perché il for valciclo è lento. Tuttavia, si noti che il ciclo non è affatto necessario. Se si desidera verificare se ci sono valori diversi da -1 nel set, sarebbe molto più efficiente verificare hashSet.size() > 1 || !hashSet.contains(-1).
Markusk

Risposte:


32

Hai creato un caso d'uso marginale di HashSet, in cui l'algoritmo degrada alla complessità quadratica.

Ecco il ciclo semplificato che richiede così tanto tempo:

for (int i = 0; i < 100_000; i++) {
    hashSet.iterator().next();
    hashSet.remove(i);
}

async-profiler mostra che quasi tutto il tempo viene speso all'interno del java.util.HashMap$HashIterator()costruttore:

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
--->        do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

La linea evidenziata è un ciclo lineare che cerca il primo bucket non vuoto nella tabella hash.

Dato che Integerha il banale hashCode(cioè hashCode è uguale al numero stesso), si scopre che numeri interi consecutivi occupano principalmente i bucket consecutivi nella tabella hash: il numero 0 va al primo bucket, il numero 1 va al secondo bucket, ecc.

Ora rimuovi i numeri consecutivi da 0 a 99999. Nel caso più semplice (quando il bucket contiene una chiave singola), la rimozione di una chiave viene implementata come annullamento dell'elemento corrispondente nell'array bucket. Si noti che la tabella non viene compattata o ridisegnata dopo la rimozione.

Quindi, più chiavi vengono rimosse dall'inizio dell'array bucket, più HashIteratora lungo è necessario trovare il primo bucket non vuoto.

Prova a rimuovere le chiavi dall'altra estremità:

hashSet.remove(100_000 - i);

L'algoritmo diventerà notevolmente più veloce!


1
Ahh, mi sono imbattuto in questo, ma l'ho scartato dopo le prime manche e ho pensato che potesse trattarsi di un'ottimizzazione JIT e sono passato all'analisi tramite JITWatch. Avrebbe dovuto eseguire prima async-profiler. Dannazione!
Attendi Kumar il

1
Molto interessante. Se fate qualcosa di simile a quanto segue nel ciclo, accelera in su riducendo la dimensione della mappa interna: if (i % 800 == 0) { hashSet = new HashSet<>(hashSet); }.
Grigio - COSÌ smetti di essere malvagio il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.