HashSet kodu için beklenmeyen çalışma süreleri


28

Yani aslında, bu kodu vardı:

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));
    }
}

Bilgisayarımdaki döngüler için yuvalanmış çalıştırmak yaklaşık 4 s sürer ve neden bu kadar uzun sürdüğünü anlamıyorum. Dış döngü 100.000 kez çalışır, döngü için iç 1 kez çalışmalıdır (hashSet'in değeri hiçbir zaman -1 olmayacaktır) ve bir öğenin HashSet'ten kaldırılması O (1) olduğundan, yaklaşık 200.000 işlem olmalıdır. Saniyede genellikle 100.000.000 işlem varsa, kodum nasıl çalışır?

Ayrıca, satır hashSet.remove(i);yorumlanırsa kod yalnızca 16 ms sürer. Döngünün içi yorumlanırsa (ancak edilmezse hashSet.remove(i);), kod yalnızca 8 ms sürer.


4
Bulgularını onaylıyorum. Sebep hakkında spekülasyon yapabilirdim, ama umarım akıllı biri büyüleyici bir açıklama yayınlar.
khelwood

1
for valDöngü zaman alan bir şey gibi görünüyor . removeHala çok hızlı. Set değiştirildikten sonra yeni bir yineleyici kurmak bir tür havai kurulum ...?
khelwood

@apangin içinde iyi bir açıklama sağlanan stackoverflow.com/a/59522575/108326 niçin for valdöngü yavaştır. Ancak, döngüye hiç gerek olmadığını unutmayın. Sette -1 dışında herhangi bir değer olup olmadığını kontrol etmek istiyorsanız, kontrol etmek çok daha verimli olacaktır hashSet.size() > 1 || !hashSet.contains(-1).
markusk

Yanıtlar:


32

HashSetAlgoritmanın kuadratik karmaşıklığa dönüştüğü marjinal bir kullanım örneği oluşturdunuz .

İşte bu kadar uzun süren basitleştirilmiş döngü:

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

async-profiler neredeyse tüm zamanların java.util.HashMap$HashIterator()yapıcı içinde harcandığını gösterir :

    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);
        }
    }

Vurgulanan çizgi, karma tablosundaki boş olmayan ilk kovayı arayan doğrusal bir döngüdür.

Yana Integerönemsiz sahiptir hashCode(hashCode sayısının kendisine eşittir, yani), ardışık tamsayı çok karma tablosunda ard arda kovalar işgal çıkıyor: 0 numaralı birinci bölümün gider, 1 sayısı, ikinci kovanın vb gider

Şimdi ardışık sayıları 0'dan 99999'a çıkarın. En basit durumda (grup tek bir anahtar içerdiğinde), bir anahtarın kaldırılması grup dizisindeki karşılık gelen öğeyi geçersiz kılmak olarak uygulanır. Çıkarıldıktan sonra tablonun sıkıştırılmadığını veya yeniden şekillenmediğini unutmayın.

Bu nedenle, grup dizisinin başlangıcından ne kadar çok anahtar kaldırırsanız, HashIteratorboş olmayan ilk grubu bulmanız o kadar uzun sürer .

Anahtarları diğer uçtan çıkarmaya çalışın:

hashSet.remove(100_000 - i);

Algoritma çok daha hızlı olacak!


1
Ahh, bununla karşılaştım ama ilk birkaç çalışmadan sonra bunu reddettim ve bunun bazı JIT optimizasyonu olabileceğini düşündüm ve JITWatch aracılığıyla analiz yapmaya geçtim. Önce async-profiler çalıştırmış olmalıydı. Lanet olsun!
Adwait Kumar

1
Oldukça ilginç. Eğer döngü içinde aşağıdaki gibi bir şey yaparsanız, iç haritanın boyutunu azaltarak hızlandırır: if (i % 800 == 0) { hashSet = new HashSet<>(hashSet); }.
Gri - SO
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.