Sabit boyutlu bir HashMap için optimum kapasite ve yük faktörü nedir?


86

Belirli bir durum için en uygun kapasiteyi ve yük faktörünü bulmaya çalışıyorum. Sanırım işin özünü anladım, ama benden daha bilgili birinden bir onay için yine de minnettar olurum. :)

HashMap'imin örneğin 100 nesne içereceğini ve zamanın çoğunu 100 nesneye sahip olacağını biliyorsam, optimum değerlerin başlangıç ​​kapasitesi 100 ve yük faktörü 1 olduğunu tahmin ediyorum. Yoksa kapasite 101'e mi ihtiyacım var yoksa başka sorunlar var mı?

DÜZENLEME: Tamam, birkaç saat ayırdım ve bazı testler yaptım. Sonuçlar burada:

  • Merakla, kapasite, kapasite + 1, kapasite + 2, kapasite-1 ve hatta kapasite-10'un tümü tamamen aynı sonuçları verir. En az kapasite-1 ve kapasite-10'un daha kötü sonuçlar vermesini beklerdim.
  • Başlangıç ​​kapasitesini kullanmak (varsayılan değer olan 16'yı kullanmak yerine),% 30'a kadar daha hızlı, fark edilir bir put () iyileştirmesi sağlar.
  • 1 yük faktörünün kullanılması, az sayıda nesne için eşit performans ve daha fazla sayıda nesne için (> 100000) daha iyi performans sağlar. Ancak, bu, nesnelerin sayısı ile orantılı olarak iyileşmez; Sonuçları etkileyen ek bir faktör olduğundan şüpheleniyorum.
  • get () performansı, farklı sayıda nesne / kapasite için biraz farklıdır, ancak duruma göre biraz farklılık gösterse de, genellikle başlangıçtaki kapasite veya yük faktöründen etkilenmez.

EDIT2: Benim açımdan da bazı grafikler ekliyorum. HashMap'i başlatıp tam kapasiteye kadar doldurduğum durumlarda, yük faktörü 0,75 ve 1 arasındaki farkı gösteren bir örnek. Y ölçeğinde ms cinsinden zamandır (daha düşük olan daha iyidir) ve x ölçeği boyuttur (nesne sayısı). Boyut doğrusal olarak değiştiğinden, gerekli zaman da doğrusal olarak artar.

Bakalım elimde ne var. Aşağıdaki iki tablo, yük faktörlerindeki farkı göstermektedir. İlk grafik, HashMap kapasiteye kadar doldurulduğunda ne olduğunu gösterir; yük faktörü 0,75, yeniden boyutlandırma nedeniyle daha kötü performans gösterir. Bununla birlikte, sürekli olarak daha kötü değil ve her türlü çarpma ve atlama var - sanırım GC'nin bu konuda büyük bir rolü var. Yük faktörü 1.25, 1 ile aynı performansı gösterir, bu nedenle tabloya dahil edilmemiştir.

tamamen dolu

Bu grafik 0.75'in yeniden boyutlandırma nedeniyle daha kötü olduğunu kanıtlıyor; HashMap'i yarı kapasiteye kadar doldurursak, 0.75 daha kötü değil, sadece ... farklı (ve daha az bellek kullanmalı ve fark edilemeyecek kadar iyi yineleme performansına sahip olmalıdır).

yarı dolu

Göstermek istediğim bir şey daha var. Bu, üç yük faktörü ve farklı HashMap boyutları için performans elde etmektir. Yük faktörü 1 için bir artış haricinde küçük bir varyasyonla sürekli olarak sabit. Bunun ne olduğunu gerçekten bilmek istiyorum (muhtemelen GC, ama kim bilir).

başak yapmak

Ve işte ilgilenenler için kod:

import java.util.HashMap;
import java.util.Map;

public class HashMapTest {

  // capacity - numbers high as 10000000 require -mx1536m -ms1536m JVM parameters
  public static final int CAPACITY = 10000000;
  public static final int ITERATIONS = 10000;

  // set to false to print put performance, or to true to print get performance
  boolean doIterations = false;

  private Map<Integer, String> cache;

  public void fillCache(int capacity) {
    long t = System.currentTimeMillis();
    for (int i = 0; i <= capacity; i++)
      cache.put(i, "Value number " + i);

    if (!doIterations) {
      System.out.print(System.currentTimeMillis() - t);
      System.out.print("\t");
    }
  }

  public void iterate(int capacity) {
    long t = System.currentTimeMillis();

    for (int i = 0; i <= ITERATIONS; i++) {
      long x = Math.round(Math.random() * capacity);
      String result = cache.get((int) x);
    }

    if (doIterations) {
      System.out.print(System.currentTimeMillis() - t);
      System.out.print("\t");
    }
  }

  public void test(float loadFactor, int divider) {
    for (int i = 10000; i <= CAPACITY; i+= 10000) {
      cache = new HashMap<Integer, String>(i, loadFactor);
      fillCache(i / divider);
      if (doIterations)
        iterate(i / divider);
    }
    System.out.println();
  }

  public static void main(String[] args) {
    HashMapTest test = new HashMapTest();

    // fill to capacity
    test.test(0.75f, 1);
    test.test(1, 1);
    test.test(1.25f, 1);

    // fill to half capacity
    test.test(0.75f, 2);
    test.test(1, 2);
    test.test(1.25f, 2);
  }

}

1
Varsayılanları değiştirmenin bu durum için daha iyi performans (daha hızlı put () yürütme) sağlaması açısından ideal.
Domchi

2
@Peter GC = çöp toplama.
Domchi

2
Bu grafikler harika ... Onları oluşturmak / işlemek için ne kullandınız?
G_H

1
@G_H Hiç fantezi - yukarıdaki programın ve Excel'in çıktısı. :)
Domchi

2
Bir dahaki sefere çizgiler yerine noktalar kullanın. Karşılaştırmayı görsel olarak kolaylaştıracaktır.
Paul Draper

Yanıtlar:


74

Pekala, bunu rahatlatmak için birkaç senaryo çalıştırmak ve sonuçların bazı görselleştirmelerini almak için bir test uygulaması oluşturdum. Testler şu şekilde yapılır:

  • Bir dizi farklı koleksiyon boyutu denendi: yüz, bin ve yüz bin giriş.
  • Kullanılan anahtarlar, bir kimlik ile benzersiz şekilde tanımlanan bir sınıfın örnekleridir. Her test, kimlik olarak artan tam sayılarla benzersiz anahtarlar kullanır. equalsBir anahtar haritalama başka bir yazar, böylece yöntem sadece bir kimlik kullanır.
  • Anahtarlar, bazı ön ayar numaralarına karşı ID'lerinin modül kalanından oluşan bir karma kod alır. Bu numaraya hash limiti diyeceğiz . Bu, beklenen hash çarpışmalarının sayısını kontrol etmeme izin verdi. Örneğin, koleksiyon boyutumuz 100 ise, 0 ile 99 arasında değişen kimliklere sahip anahtarlarımız olacaktır. Karma sınırı 100 ise, her anahtarın benzersiz bir karma kodu olacaktır. Karma sınırı 50 ise, anahtar 0, anahtar 50 ile aynı karma koduna sahip olacaktır; 1, 51 ile aynı karma koduna sahip olacaktır. Diğer bir deyişle, anahtar başına beklenen karma çarpışma sayısı, koleksiyon boyutunun karma değerine bölünmesiyle hesaplanır. limit.
  • Koleksiyon boyutu ve hash limitinin her kombinasyonu için, testi farklı ayarlarla başlatılan hash haritalarını kullanarak çalıştırıyorum. Bu ayarlar yük faktörü ve toplama ayarının bir faktörü olarak ifade edilen bir başlangıç ​​kapasitesidir. Örneğin, toplama boyutu 100 ve başlangıç ​​kapasite faktörü 1,25 olan bir test, başlangıç ​​kapasitesi 125 olan bir karma haritayı başlatacaktır.
  • Her anahtarın değeri sadece yenidir Object.
  • Her test sonucu, bir Result sınıfının bir örneğinde kapsüllenir. Tüm testlerin sonunda sonuçlar en kötü genel performanstan en iyiye doğru sıralanır.
  • Ortalama koyma ve alma süresi, 10 koyma / alma başına hesaplanır.
  • JIT derleme etkisini ortadan kaldırmak için tüm test kombinasyonları bir kez çalıştırılır. Bundan sonra, gerçek sonuçlar için testler yapılır.

İşte sınıf:

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 {

        //First entry of each array is the sample collection size, subsequent entries
        //are the hash limits
        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};

        //Doing a warmup run to eliminate JIT influence
        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();

        //Now for the real thing...
        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();
        }

//      ResultVisualizer.visualizeResults(results);

    }

    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 + "%");

        //Generating our sample key collection.
        final List<Key> keys = generateSamples(hashLimit, sampleSize);

        //Generating our value collection
        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);

        //Haha, what kind of noob explicitly calls for garbage collection?
        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) {

            //Equals implies same hashCode if limit is the same
            //Same hashCode doesn't necessarily implies equals

            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.");

        }

    }

}

Bunu çalıştırmak biraz zaman alabilir. Sonuçlar standart olarak yazdırılır. Bir satır yorum yaptığımı fark edebilirsiniz. Bu satır, sonuçların görsel temsillerini png dosyalarına çıkaran bir görselleştirici çağırır. Bunun için sınıf aşağıda verilmiştir. Çalıştırmak istiyorsanız, yukarıdaki koddaki uygun satırın açıklamasını kaldırın. Dikkat edin: görselleştirici sınıfı Windows üzerinde çalıştığınızı varsayar ve C: \ temp'de klasörler ve dosyalar oluşturur. Başka bir platformda çalışırken bunu ayarlayın.

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) {

        //[x][y] => x is mapped to initial capacity, y is mapped to load factor
        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);

    }

}

Görselleştirilmiş çıktı aşağıdaki gibidir:

  • Testler önce toplama boyutuna, ardından hash limitine göre bölünür.
  • Her test için, ortalama koyma süresi (10 koyma başına) ve ortalama alma süresi (10 alma başına) ile ilgili bir çıktı görüntüsü vardır. Görüntüler, başlangıç ​​kapasitesi ve yük faktörü kombinasyonu başına bir renk gösteren iki boyutlu "ısı haritaları" dır.
  • Görüntülerdeki renkler, doymuş yeşilden doymuş kırmızıya değişen, en iyiden en kötü sonuca kadar normalleştirilmiş bir ölçekte ortalama süreye dayanmaktadır. Başka bir deyişle, en iyi zaman tamamen yeşil olurken, en kötü zaman tamamen kırmızı olacaktır. İki farklı zaman ölçümü asla aynı renge sahip olmamalıdır.
  • Renk haritaları, satışlar için ayrı ayrı hesaplanır, ancak ilgili kategoriler için tüm testleri kapsar.
  • Görselleştirmeler, x eksenleri üzerindeki başlangıç ​​kapasitesini ve y eksenindeki yük faktörünü gösterir.

Daha fazla uzatmadan sonuçlara bir göz atalım. Koyma sonuçlarıyla başlayacağım.

Sonuçları koy


Koleksiyon boyutu: 100. Karma sınırı: 50. Bu, her bir karma kodun iki kez gerçekleşmesi gerektiği ve karma haritada diğer her anahtarın çarpıştığı anlamına gelir.

size_100_hlimit_50_puts

Bu pek iyi başlamıyor. Koleksiyon boyutunun% 25 üzerinde ve yük faktörü 1 olan ilk kapasite için büyük bir sıcak nokta olduğunu görüyoruz. Sol alt köşe çok iyi performans göstermiyor.


Koleksiyon boyutu: 100. Karma sınırı: 90. On anahtardan biri, yinelenen bir karma koduna sahiptir.

size_100_hlimit_90_puts

Bu, biraz daha gerçekçi bir senaryodur, mükemmel bir hash fonksiyonuna sahip değil, ancak yine de% 10 aşırı yüke sahip. Hotspot gitti, ancak düşük bir başlangıç ​​kapasitesi ile düşük bir yük faktörü kombinasyonu açıkça işe yaramıyor.


Koleksiyon boyutu: 100. Karma sınırı: 100. Her anahtar kendi benzersiz karma kodu olarak. Yeterli kova varsa çarpışma beklenmez.

size_100_hlimit_100_puts

Yük faktörü 1 olan 100'lük bir başlangıç ​​kapasitesi iyi görünüyor. Şaşırtıcı bir şekilde, daha düşük bir yük faktörüne sahip daha yüksek bir başlangıç ​​kapasitesi mutlaka iyi değildir.


Koleksiyon boyutu: 1000. Hash limiti: 500. 1000 girişle burada daha ciddileşiyor. Tıpkı ilk testte olduğu gibi, 2'ye 1'lik bir hash aşırı yüklemesi var.

size_1000_hlimit_500_puts

Sol alt köşe hala iyi değil. Ancak, daha düşük başlangıç ​​sayımı / yüksek yük faktörü ve daha yüksek ilk sayım / düşük yük faktörü kombinasyonu arasında bir simetri var gibi görünüyor.


Koleksiyon boyutu: 1000. Karma sınırı: 900. Bu, on karma koddan birinin iki kez gerçekleşeceği anlamına gelir. Çarpışmalarla ilgili makul senaryo.

size_1000_hlimit_900_puts

1'in üzerinde bir yük faktörü ile çok düşük olan bir başlangıç ​​kapasitesinin beklenmedik kombinasyonunda çok komik bir şeyler oluyor ki bu oldukça sezgisel. Aksi takdirde, hala oldukça simetriktir.


Koleksiyon boyutu: 1000. Karma sınırı: 990. Bazı çarpışmalar, ancak yalnızca birkaçı. Bu açıdan oldukça gerçekçi.

size_1000_hlimit_990_puts

Burada güzel bir simetrimiz var. Sol alt köşe hala optimalin altında, ancak kombinasyonlar 1000 başlatma kapasitesi / 1.0 yük faktörüne karşı 1250 başlatma kapasitesi / 0.75 yük faktörü aynı seviyede.


Koleksiyon boyutu: 1000. Karma sınırı: 1000. Yinelenen karma kodlar yok, ancak şimdi 1000 örnek boyutuyla.

size_1000_hlimit_1000_puts

Burada söylenecek pek bir şey yok. Daha yüksek bir başlangıç ​​kapasitesinin 0,75'lik bir yük faktörüyle kombinasyonu, 1000 başlangıç ​​kapasitesinin 1'lik bir yük faktörü ile kombinasyonundan biraz daha iyi performans gösteriyor gibi görünüyor.


Koleksiyon boyutu: 100_000. Karma sınırı: 10_000. Pekala, anahtar başına yüz bin 100 karma kod kopyasından oluşan bir örnek boyutuyla şimdi ciddileşiyor.

size_100000_hlimit_10000_puts

Eyvah! Sanırım alt spektrumumuzu bulduk. Yükleme faktörü 1 olan toplama boyutunda bir başlangıç ​​kapasitesi gerçekten iyi işliyor, ancak bunun dışında tüm mağazada.


Koleksiyon boyutu: 100_000. Karma sınırı: 90_000. Önceki testten biraz daha gerçekçi, burada hash kodlarında% 10 aşırı yüklenme var.

size_100000_hlimit_90000_puts

Sol alt köşe hala istenmiyor. Daha yüksek başlangıç ​​kapasiteleri en iyi sonucu verir.


Koleksiyon boyutu: 100_000. Karma sınırı: 99_000. İyi senaryo, bu. % 1 karma kod aşırı yüklemesine sahip büyük bir koleksiyon.

size_100000_hlimit_99000_puts

Tam toplama boyutunu, yükleme faktörü 1 olan başlangıç ​​kapasitesi olarak kullanmak burada kazanır! Yine de biraz daha büyük başlatma kapasiteleri oldukça iyi çalışıyor.


Koleksiyon boyutu: 100_000. Karma sınırı: 100_000. Büyük olan. Mükemmel bir hash işlevine sahip en büyük koleksiyon.

size_100000_hlimit_100000_puts

Burada bazı şaşırtıcı şeyler var. 1 yük faktöründe% 50 ek odaya sahip bir ilk kapasite kazanır.


Pekala, koymalar için bu kadar. Şimdi, alırları kontrol edeceğiz. Unutmayın, aşağıdaki haritaların tümü en iyi / en kötü alma zamanlarına bağlıdır, koyma süreleri artık dikkate alınmaz.

Sonuç almak


Koleksiyon boyutu: 100. Karma sınırı: 50. Bu, her bir karma kodun iki kez gerçekleşmesi gerektiği ve diğer anahtarların karma haritada çarpışmasının beklendiği anlamına gelir.

size_100_hlimit_50_gets

Eh ... Ne?


Koleksiyon boyutu: 100. Karma sınırı: 90. On anahtardan biri, yinelenen bir karma koduna sahiptir.

size_100_hlimit_90_gets

Whoa Nelly! Bu, soruyu soranın sorusuyla ilişkilendirilebilecek en olası senaryodur ve görünüşe göre 1 yük faktörü ile 100'lük bir başlangıç ​​kapasitesi buradaki en kötü şeylerden biridir! Yemin ederim bunu kandırmadım.


Koleksiyon boyutu: 100. Karma sınırı: 100. Her anahtar kendi benzersiz karma kodu olarak. Çarpışma beklenmiyor.

size_100_hlimit_100_gets

Bu biraz daha huzurlu görünüyor. Genelde genel olarak aynı sonuçlar.


Koleksiyon boyutu: 1000. Hash limiti: 500. İlk testte olduğu gibi, 2'ye 1'lik bir hash aşırı yüklemesi var, ancak şimdi çok daha fazla giriş var.

size_1000_hlimit_500_gets

Görünüşe göre herhangi bir ayar burada iyi bir sonuç verecek.


Koleksiyon boyutu: 1000. Karma sınırı: 900. Bu, on karma koddan birinin iki kez gerçekleşeceği anlamına gelir. Çarpışmalarla ilgili makul senaryo.

size_1000_hlimit_900_gets

Ve tıpkı bu düzeneğin koymalarında olduğu gibi, garip bir noktada bir anormallik alıyoruz.


Koleksiyon boyutu: 1000. Karma sınırı: 990. Bazı çarpışmalar, ancak yalnızca birkaçı. Bu açıdan oldukça gerçekçi.

size_1000_hlimit_990_gets

Her yerde iyi performans, yüksek başlangıç ​​kapasitesi ile düşük yük faktörünün birleşiminden tasarruf edin. İki hash harita yeniden boyutlandırması beklenebileceğinden, bunu koymalar için bekliyorum. Ama neden paralı?


Koleksiyon boyutu: 1000. Karma sınırı: 1000. Yinelenen karma kodlar yok, ancak şimdi 1000 örnek boyutuyla.

size_1000_hlimit_1000_gets

Tamamen olağanüstü bir görselleştirme. Bu ne olursa olsun işe yarıyor gibi görünüyor.


Koleksiyon boyutu: 100_000. Karma sınırı: 10_000. Bir sürü karma kod çakışmasıyla tekrar 100K'ya giriyoruz.

size_100000_hlimit_10000_gets

Kötü noktalar çok lokalize olmasına rağmen hoş görünmüyor. Buradaki performans, büyük ölçüde ayarlar arasındaki belirli bir sinerjiye bağlı görünüyor.


Koleksiyon boyutu: 100_000. Karma sınırı: 90_000. Önceki testten biraz daha gerçekçi, burada hash kodlarında% 10 aşırı yüklenme var.

size_100000_hlimit_90000_gets

Pek çok varyans, ancak gözlerinizi kısarsanız sağ üst köşeyi işaret eden bir ok görebilirsiniz.


Koleksiyon boyutu: 100_000. Karma sınırı: 99_000. İyi senaryo, bu. % 1 karma kod aşırı yüklemesine sahip büyük bir koleksiyon.

size_100000_hlimit_99000_gets

Çok kaotik. Burada çok fazla yapı bulmak zor.


Koleksiyon boyutu: 100_000. Karma sınırı: 100_000. Büyük olan. Mükemmel bir hash işlevine sahip en büyük koleksiyon.

size_100000_hlimit_100000_gets

Bunun Atari grafiklerine benzemeye başladığını düşünen başka kimse var mı? Bu, tam olarak toplama boyutunun -% 25 veya +% 50'lik bir başlangıç ​​kapasitesini desteklemektedir.


Pekala, şimdi sonuç çıkarma zamanı ...

  • Yerleştirme süreleriyle ilgili olarak: beklenen harita girişi sayısından daha düşük başlangıç ​​kapasitelerinden kaçınmak isteyeceksiniz. Önceden kesin bir sayı biliniyorsa, bu sayı veya bunun biraz üzerinde bir şey en iyi sonucu verir. Yüksek yük faktörleri, önceki karma harita yeniden boyutlandırmaları nedeniyle daha düşük başlangıç ​​kapasitelerini dengeleyebilir. Daha yüksek başlangıç ​​kapasiteleri için, o kadar önemli görünmüyorlar.
  • Get times ile ilgili olarak: sonuçlar burada biraz kaotik. Sonuçlandırılacak pek bir şey yok. Karma kod çakışması, başlangıç ​​kapasitesi ve yük faktörü arasındaki ince oranlara çok güveniyor gibi görünüyor, bazı sözde kötü kurulumlar iyi performans gösteriyor ve iyi kurulumlar çok kötü çalışıyor.
  • Java performansıyla ilgili varsayımlar söz konusu olduğunda, görünüşe göre saçma sapan şeylerim var. Gerçek şu ki, ayarlarınızı uygulamasına mükemmel bir şekilde ayarlamadığınız sürece HashMap, sonuçlar her yerde olacaktır. Bundan çıkarılacak bir şey varsa, o da 16 olan varsayılan başlangıç ​​boyutunun en küçük haritalardan başka her şey için biraz aptalca olmasıdır, bu nedenle hangi boyut sırası hakkında herhangi bir fikriniz varsa başlangıç ​​boyutunu ayarlayan bir kurucu kullanın. Olacak.
  • Burada nanosaniye cinsinden ölçüyoruz. Makinemde 10 vuruş başına en iyi ortalama süre 1179 ns ve en kötü 5105 ns idi. 10 çekim başına en iyi ortalama süre 547 ns ve en kötü 3484 ns idi. Bu 6 faktör farkı olabilir, ancak bir milisaniyeden daha az konuşuyoruz. Orijinal posterin aklından çok daha büyük koleksiyonlar üzerine.

İşte bu. Umarım kodumun, burada yayınladığım her şeyi geçersiz kılan korkunç bir denetimi yoktur. Bu eğlenceliydi ve sonunda küçük optimizasyonlardan çok fazla farklılık beklemektense işini yapmak için Java'ya güvenebileceğinizi öğrendim. Bu, bazı şeylerden kaçınılmaması gerektiği anlamına gelmez, ancak o zaman çoğunlukla döngüler için uzun Dizeler oluşturmaktan, yanlış veri yapılarını kullanmaktan ve O (n ^ 3) algoritmaları yapmaktan bahsediyoruz.


1
Çaba için teşekkürler, harika görünüyor! Tembel olmamak için sonuçlarıma güzel grafikler de ekledim. Benim testlerim sizinkinden biraz daha kaba kuvvet ama daha büyük haritalar kullanırken farklılıkların daha belirgin olduğunu buldum. Küçük haritalarla, ne yaparsanız yapın, kaçıramazsınız. Performans, JVM optimizasyonları ve GC nedeniyle kaotik olma eğilimindedir ve bazı küçük veri kümeleriniz için bu kaostan güçlü sonuçların alındığına dair bir teorim var.
Domchi

Performansı elde etme üzerine bir yorum daha. Kaotik görünüyor, ancak çok dar bir aralıkta çok değiştiğini buldum, ama genel olarak, sabit ve cehennem kadar sıkıcı. 100/90 tarihinde yaptığın gibi ara sıra garip artışlar yaşadım. Açıklayamam ama pratikte muhtemelen farkedilemez.
Domchi

G_H, lütfen cevabıma bir göz atın, bunun çok eski bir konu olduğunu biliyorum ama muhtemelen testleriniz bunu akılda tutarak yeniden yapılmalıdır.
durron597

Hey, bunu ACM'ye konferans bildirisi olarak göndermelisiniz :) Ne çaba!
yerlilbilgin

12

Bu oldukça harika bir konu, ancak kaçırdığınız çok önemli bir şey var. Dedin:

Merakla, kapasite, kapasite + 1, kapasite + 2, kapasite-1 ve hatta kapasite-10'un tümü tamamen aynı sonuçları verir. En az kapasite-1 ve kapasite-10'un daha kötü sonuçlar vermesini beklerdim.

Kaynak kodu, dahili olarak bir sonraki en yüksek ikiye güç olan başlangıç ​​kapasitesini atlar. Bu, örneğin 513, 600, 700, 800, 900, 1000 ve 1024 başlangıç ​​kapasitelerinin hepsinin aynı başlangıç ​​kapasitesini (1024) kullanacağı anlamına gelir. Bu, @ G_H tarafından yapılan testi geçersiz kılmaz, ancak sonuçlarını analiz etmeden önce bunun yapıldığını fark etmek gerekir. Ve bazı testlerin garip davranışlarını açıklıyor.

Bu, JDK kaynağı için yapıcı hakkıdır:

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity];
    init();
}

Bu çok ilginç! Bunun hakkında hiçbir fikrim yoktu. Testlerde gördüklerimi gerçekten açıklıyor. Ve yine, derleyicinin veya kodun arkanızda ne yaptığını gerçekten bilmediğiniz (veya gerçekten bilmeniz gerektiği) için erken optimizasyonun genellikle yararlı olduğunu doğrular. Ve tabii ki sürüm / uygulamaya göre değişebilir. Netleştirdiğiniz için teşekkürler!
G_H

@G_H Bu bilgilere göre daha uygun sayılar seçerek testlerinizin tekrar çalıştığını görmek isterim. Örneğin, 1200 öğem varsa, 1024 harita mı, 2048 harita mı yoksa 4096 harita mı kullanmalıyım? Orijinal sorunun cevabını bilmiyorum, bu yüzden başlamak için bu konuyu buldum. Gerçi, bunu biliyorum Guava çarpar senin expectedSizetarafından 1.33bunu yaptığındaMaps.newHashMap(int expectedSize)
durron597

HashMap, için ikinin üssü değerine yuvarlanmazsa capacity, bazı kovalar asla kullanılmaz. Harita verilerinin nereye yerleştirileceğine ilişkin bölüm dizini tarafından belirlenir bucketIndex = hashCode(key) & (capacity-1). Dolayısıyla capacity, ikinin kuvvetinden başka bir şey olsaydı , ikili gösteriminin içinde (capacity-1)bazı sıfırlar olurdu, bu da &(ikili ve) işleminin hashCode'un belirli alt bitlerini her zaman sıfırlayacağı anlamına gelir . Örnek: (capacity-1)bir 111110yerine (62), 111111(63). Bu durumda yalnızca çift indisli kovalar kullanılabilir.
Michael Geier

2

Sadece devam et 101. Aslında buna ihtiyaç duyulduğundan emin değilim, ama kesin olarak öğrenmeye zahmet etmeye hiç zahmete değmez.

... sadece ekleyin 1.


DÜZENLEME: Cevabım için bazı gerekçeler.

İlk olarak, senin HashMapötesine büyümeyeceğini varsayıyorum 100; eğer öyleyse, yük faktörünü olduğu gibi bırakmalısınız. Benzer şekilde, endişeniz performans ise, yük faktörünü olduğu gibi bırakın . Endişeniz bellekse, statik boyutu ayarlayarak bazılarını kaydedebilirsiniz. Bu belki size bellekte bir sürü şey tıkınma eğer belki değerinde yapıyor; yani, birçok haritayı depoluyor veya yığın alanı vurgulayan boyutlu haritalar oluşturuyor.

İkincisi, değeri seçiyorum 101çünkü daha iyi okunabilirlik sunuyor ... daha sonra kodunuza bakarsam ve başlangıç ​​kapasitesini olarak ayarladığınızı 100ve onu 100öğelerle yüklediğinizi görürsem, bunu yapmak zorunda kalacağım tam olarak ulaştığında yeniden boyutlandırmayacağından emin olmak için Javadoc'u okuyun 100. Elbette cevabı orada bulamayacağım, bu yüzden kaynağa bakmam gerekecek. Buna değmez ... sadece bırakın 101ve herkes mutlu ve kimse kaynak koduna bakmıyor java.util.HashMap. Hoorah.

Üçüncü olarak, bu ayarı iddiası HashMapEğer bir yük faktörü ile beklediğiniz tam kapasite 1 " ye karşılık ve yerleştirme performansı öldürecek " o kalın olarak yapılmış olsa bile, sadece doğru değildir.

... nkovalarınız varsa ve nöğeleri rastgele nkovalara atarsanız, evet, aynı kovadaki eşyalarla karşılaşacaksınız, elbette ... ama bu dünyanın sonu değil ... pratikte, sadece birkaç tane daha eşit karşılaştırma. Aslında esp var. Alternatifin nöğeleri n/0.75kovalara atamak olduğunu düşündüğünüzde çok az fark vardır .

Bunun için sözüme gerek yok ...


Hızlı test kodu:

static Random r = new Random();

public static void main(String[] args){
    int[] tests = {100, 1000, 10000};
    int runs = 5000;

    float lf_sta = 1f;
    float lf_dyn = 0.75f;

    for(int t:tests){
        System.err.println("=======Test Put "+t+"");
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
        long norm_put = testInserts(map, t, runs);
        System.err.print("Norm put:"+norm_put+" ms. ");

        int cap_sta = t;
        map = new HashMap<Integer,Integer>(cap_sta, lf_sta);
        long sta_put = testInserts(map, t, runs);
        System.err.print("Static put:"+sta_put+" ms. ");

        int cap_dyn = (int)Math.ceil((float)t/lf_dyn);
        map = new HashMap<Integer,Integer>(cap_dyn, lf_dyn);
        long dyn_put = testInserts(map, t, runs);
        System.err.println("Dynamic put:"+dyn_put+" ms. ");
    }

    for(int t:tests){
        System.err.println("=======Test Get (hits) "+t+"");
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
        fill(map, t);
        long norm_get_hits = testGetHits(map, t, runs);
        System.err.print("Norm get (hits):"+norm_get_hits+" ms. ");

        int cap_sta = t;
        map = new HashMap<Integer,Integer>(cap_sta, lf_sta);
        fill(map, t);
        long sta_get_hits = testGetHits(map, t, runs);
        System.err.print("Static get (hits):"+sta_get_hits+" ms. ");

        int cap_dyn = (int)Math.ceil((float)t/lf_dyn);
        map = new HashMap<Integer,Integer>(cap_dyn, lf_dyn);
        fill(map, t);
        long dyn_get_hits = testGetHits(map, t, runs);
        System.err.println("Dynamic get (hits):"+dyn_get_hits+" ms. ");
    }

    for(int t:tests){
        System.err.println("=======Test Get (Rand) "+t+"");
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
        fill(map, t);
        long norm_get_rand = testGetRand(map, t, runs);
        System.err.print("Norm get (rand):"+norm_get_rand+" ms. ");

        int cap_sta = t;
        map = new HashMap<Integer,Integer>(cap_sta, lf_sta);
        fill(map, t);
        long sta_get_rand = testGetRand(map, t, runs);
        System.err.print("Static get (rand):"+sta_get_rand+" ms. ");

        int cap_dyn = (int)Math.ceil((float)t/lf_dyn);
        map = new HashMap<Integer,Integer>(cap_dyn, lf_dyn);
        fill(map, t);
        long dyn_get_rand = testGetRand(map, t, runs);
        System.err.println("Dynamic get (rand):"+dyn_get_rand+" ms. ");
    }
}

public static long testInserts(HashMap<Integer,Integer> map, int test, int runs){
    long b4 = System.currentTimeMillis();

    for(int i=0; i<runs; i++){
        fill(map, test);
        map.clear();
    }
    return System.currentTimeMillis()-b4;
}

public static void fill(HashMap<Integer,Integer> map, int test){
    for(int j=0; j<test; j++){
        if(map.put(r.nextInt(), j)!=null){
            j--;
        }
    }
}

public static long testGetHits(HashMap<Integer,Integer> map, int test, int runs){
    long b4 = System.currentTimeMillis();

    ArrayList<Integer> keys = new ArrayList<Integer>();
    keys.addAll(map.keySet());

    for(int i=0; i<runs; i++){
        for(int j=0; j<test; j++){
            keys.get(r.nextInt(keys.size()));
        }
    }
    return System.currentTimeMillis()-b4;
}

public static long testGetRand(HashMap<Integer,Integer> map, int test, int runs){
    long b4 = System.currentTimeMillis();

    for(int i=0; i<runs; i++){
        for(int j=0; j<test; j++){
            map.get(r.nextInt());
        }
    }
    return System.currentTimeMillis()-b4;
}

Test sonuçları:

=======Test Put 100
Norm put:78 ms. Static put:78 ms. Dynamic put:62 ms. 
=======Test Put 1000
Norm put:764 ms. Static put:763 ms. Dynamic put:748 ms. 
=======Test Put 10000
Norm put:12921 ms. Static put:12889 ms. Dynamic put:12873 ms. 
=======Test Get (hits) 100
Norm get (hits):47 ms. Static get (hits):31 ms. Dynamic get (hits):32 ms. 
=======Test Get (hits) 1000
Norm get (hits):327 ms. Static get (hits):328 ms. Dynamic get (hits):343 ms. 
=======Test Get (hits) 10000
Norm get (hits):3304 ms. Static get (hits):3366 ms. Dynamic get (hits):3413 ms. 
=======Test Get (Rand) 100
Norm get (rand):63 ms. Static get (rand):46 ms. Dynamic get (rand):47 ms. 
=======Test Get (Rand) 1000
Norm get (rand):483 ms. Static get (rand):499 ms. Dynamic get (rand):483 ms. 
=======Test Get (Rand) 10000
Norm get (rand):5190 ms. Static get (rand):5362 ms. Dynamic get (rand):5236 ms. 

re: ↑ - bununla ilgili → || ← farklı ayarlar arasında çok fark var .


Çünkü benim orijinal cevap (birinci yatay çizginin üzerinde bit) ile ilgili olarak, kasten glib edildi Çoğu durumda , mikro optimize bu tip iyi değil .


@EJP, tahminim yanlış değil . Yukarıdaki düzenlemelere bakın. Kişisel varsayımları olan varsayım doğruysa ve kimin varsayımları yanlış hakkında yanlış.
badroit

(... belki biraz sinirliyim ... biraz sinirliyim de: P)
badroit

3
EJP'de haklı olarak rahatsız olabilirsiniz, ancak şimdi sıra bende; P - erken optimizasyonun erken boşalmaya çok benzediğini kabul etsem de, lütfen benim durumumda genellikle çabaya değmeyen bir şeyin çabaya değmeyeceğini varsaymayın . Benim durumumda, tahmin etmek istemediğim için yeterince önemli, bu yüzden baktım - benim durumumda +1 gerekli değil (ancak başlangıç ​​/ gerçek kapasitenizin aynı olmadığı ve loadFactor'ın 1 olmadığı durumlarda olabilir, HashMap'te int'e dönüştürülmesine bakın: eşik = (int) (kapasite * loadFactor)).
Domchi

@badroit Siz açıkça bunun gerekli olduğundan emin olmadığımı söylediniz '. Bu nedenle varsayımdı. Şimdi yapılması ve araştırma yayınlanmıştır olduğunu, artık varsayımları olduğunu ve açıkça açıkça önceden yapmamıştı olarak oldu , varsayımları, aksi takdirde emin olurdu. 'Yanlış'a gelince, Javadoc, onlarca yıllık araştırmaların yaptığı gibi, 0.75'lik bir yük faktörü ve G_H'nin cevabını açıkça zorunlu kılıyor. Son olarak, 'çabaya değmez' konusunda Domchi'nin buradaki yorumuna bakın. Genel olarak mikro optimizasyon konusunda sizinle aynı fikirde olmama rağmen, doğru olan pek bir şey bırakmıyor.
user207421

Herkes rahatlasın. Evet, cevabım abartılı şeyler. Çok ağır bir equalsişlevi olmayan 100 nesneniz varsa , bunları bir Listeye koymak ve sadece "içerir" kullanmaktan kurtulabilirsiniz. Bu kadar küçük bir setle, performansta asla büyük farklar olmayacak. Bu gerçekten sadece hız veya hafıza endişeleri her şeyin üstüne çıkıyorsa veya eşittir ve hash çok spesifikse önemlidir. Daha sonra büyük koleksiyonlar ve çeşitli yük faktörleri ve ilk kapasiteler ile dolu olup olmadığımı görmek için bir test yapacağım.
G_H

2

Uygulama açısından, Google Guava'nın uygun bir fabrika yöntemi vardır

Maps.newHashMapWithExpectedSize(expectedSize)

Hangi kapasitesi hesaplar aşağıdaki formül kullanılarak

capacity = expectedSize / 0.75F + 1.0F

1

Gönderen HashMapJavaDoc'u:

Genel bir kural olarak, varsayılan yük faktörü (.75), zaman ve alan maliyetleri arasında iyi bir denge sağlar. Daha yüksek değerler alan ek yükünü azaltır ancak arama maliyetini artırır (alma ve yerleştirme dahil HashMap sınıfının işlemlerinin çoğunda yansıtılır). Yeniden doldurma işlemlerinin sayısını en aza indirmek için, haritadaki beklenen giriş sayısı ve yük faktörü, başlangıç ​​kapasitesini ayarlarken dikkate alınmalıdır. Başlangıç ​​kapasitesi, yük faktörüne bölünen maksimum giriş sayısından daha büyükse, yeniden doldurma işlemi asla gerçekleşmez.

Bu nedenle, 100 giriş bekliyorsanız, belki de 0,75'lik bir yük faktörü ve bir başlangıç ​​tavan kapasitesi (100 / 0,75) en iyisidir. Bu 134'e düşüyor.

Kabul etmeliyim, daha yüksek bir yük faktörü için arama maliyetinin neden daha yüksek olacağından emin değilim. HashMap'in daha "kalabalık" olması, aynı kovaya daha fazla nesnenin yerleştirileceği anlamına gelmez, değil mi? Yanılmıyorsam bu sadece hash kodlarına bağlıdır. Dolayısıyla, iyi bir karma kod yayılımı varsayarsak, yük faktöründen bağımsız olarak çoğu durum hala O (1) olmamalı mı?

DÜZENLEME: Göndermeden önce daha fazlasını okumalıyım ... Elbette karma kod doğrudan bazı dahili dizine eşlenemez. Mevcut kapasiteye uygun bir değere düşürülmelidir. Bu, başlangıç ​​kapasiteniz ne kadar büyükse, hash çarpışmalarının sayısının o kadar az olmasını beklediğiniz anlamına gelir. 1 yük faktörüne sahip nesne kümenizin tam boyutunu (veya +1) ilk kapasitesini seçmek, haritanızın asla yeniden boyutlandırılmamasını sağlar. Ancak, arama ve ekleme performansınızı öldürecektir. Yeniden boyutlandırma hala nispeten hızlıdır ve yalnızca bir kez gerçekleşebilirken, aramalar haritayla hemen hemen ilgili tüm işler üzerinde yapılır. Sonuç olarak, hızlı aramalar için optimize etmek, burada gerçekten istediğiniz şeydir. JavaDoc'un dediği gibi bunu asla yeniden boyutlandırmak zorunda kalmadan birleştirebilirsiniz: gerekli kapasitenizi alın, optimal bir yük faktörüne bölün (örn. 0,75) ve bunu başlangıç ​​kapasitesi olarak bu yük faktörüyle birlikte kullanın. Yuvarlamanın sizi anlamayacağından emin olmak için 1 ekleyin.


1
" arama ve ekleme performansınızı öldürecek ". Bu aşırı abartılı / açık yanlış.
badroit

1
Testlerim, arama performansının yük faktörünün 1 olarak ayarlanmasından etkilenmediğini gösteriyor. Ekleme performansı aslında iyileştirildi; yeniden boyutlandırma olmadığından daha hızlıdır. Dolayısıyla, ifadeniz genel bir durum için doğrudur (az sayıda öğeye sahip bir HashMap için arama 0.75 ile 1'den daha hızlı olacaktır), ancak HashMap her zaman maksimum kapasitesine kadar dolu olduğunda ve asla değişmeyen özel durumum için yanlıştır. Başlangıç ​​boyutunu daha yükseğe ayarlama öneriniz ilginçtir ancak benim durumum için önemsizdir çünkü tablom büyümez, bu nedenle yük faktörü yalnızca yeniden boyutlandırma ışığında önemlidir.
Domchi
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.