ความจุและปัจจัยโหลดที่เหมาะสมที่สุดสำหรับ HashMap ขนาดคงที่คืออะไร?


86

ฉันกำลังพยายามหาค่าความจุและโหลดแฟกเตอร์ที่เหมาะสมที่สุดสำหรับบางกรณี ฉันคิดว่าฉันมีส่วนสำคัญของมัน แต่ฉันก็ยังขอบคุณสำหรับคำยืนยันจากคนที่มีความรู้มากกว่าฉัน :)

ถ้าฉันรู้ว่า HashMap ของฉันจะเติมเต็มให้มีวัตถุ 100 ชิ้นและจะใช้เวลาส่วนใหญ่ในการมีวัตถุ 100 ชิ้นฉันเดาว่าค่าที่เหมาะสมที่สุดคือความจุเริ่มต้น 100 และปัจจัยการโหลด 1? หรือฉันต้องการความจุ 101 หรือมี gotcha อื่น ๆ ?

แก้ไข: ตกลงฉันตั้งเวลาไว้สองสามชั่วโมงและทำการทดสอบบางอย่าง นี่คือผลลัพธ์:

  • อยากรู้อยากเห็นความจุความจุ +1 ความจุ +2 ความจุ -1 และแม้แต่ความจุ -10 ทั้งหมดให้ผลลัพธ์ที่เหมือนกันทุกประการ ฉันคาดหวังว่าอย่างน้อยความจุ -1 และความจุ -10 จะให้ผลลัพธ์ที่แย่ลง
  • การใช้ความจุเริ่มต้น (ซึ่งตรงข้ามกับการใช้ค่าเริ่มต้นคือ 16) ช่วยให้การปรับปรุง put () ที่สังเกตเห็นได้ - เร็วขึ้นถึง 30%
  • การใช้โหลดแฟกเตอร์ 1 จะให้ประสิทธิภาพที่เท่าเทียมกันสำหรับวัตถุจำนวนน้อยและประสิทธิภาพที่ดีกว่าสำหรับวัตถุจำนวนมาก (> 100000) อย่างไรก็ตามสิ่งนี้ไม่ได้ปรับปรุงตามสัดส่วนของจำนวนวัตถุ ฉันสงสัยว่ามีปัจจัยเพิ่มเติมที่ส่งผลต่อผลลัพธ์
  • ประสิทธิภาพ get () แตกต่างกันเล็กน้อยสำหรับจำนวนวัตถุ / ความจุที่แตกต่างกัน แต่อาจแตกต่างกันเล็กน้อยในแต่ละกรณีโดยทั่วไปจะไม่ได้รับผลกระทบจากความจุเริ่มต้นหรือปัจจัยการโหลด

EDIT2: การเพิ่มแผนภูมิในส่วนของฉันด้วย นี่คือข้อแตกต่างที่แสดงให้เห็นถึงความแตกต่างระหว่าง load factor 0.75 และ 1 ในกรณีที่ฉันเริ่มต้น HashMap และเติมเต็มความจุ ในมาตราส่วน y คือเวลาเป็นมิลลิวินาที (ต่ำกว่าดีกว่า) และมาตราส่วน x คือขนาด (จำนวนวัตถุ) เนื่องจากขนาดเปลี่ยนแปลงไปในเชิงเส้นเวลาที่ต้องการจึงเพิ่มขึ้นเป็นเชิงเส้นเช่นกัน

มาดูกันว่าฉันได้อะไร สองแผนภูมิต่อไปนี้แสดงความแตกต่างของปัจจัยการโหลด แผนภูมิแรกแสดงให้เห็นว่าจะเกิดอะไรขึ้นเมื่อ HashMap เต็มความจุ โหลดแฟคเตอร์ 0.75 ทำงานได้แย่ลงเนื่องจากการปรับขนาด อย่างไรก็ตามมันไม่ได้แย่ลงอย่างต่อเนื่องและมีการกระแทกและการกระโดดทุกประเภท - ฉันเดาว่า GC มีบทบาทสำคัญในเรื่องนี้ โหลดแฟคเตอร์ 1.25 ทำงานเหมือนกับ 1 ดังนั้นจึงไม่รวมอยู่ในแผนภูมิ

เต็ม

แผนภูมินี้แสดงให้เห็นว่า 0.75 แย่ลงเนื่องจากการปรับขนาด หากเราเติม HashMap ให้เต็มความจุเพียงครึ่งเดียว 0.75 ก็ไม่ได้แย่ไปกว่านั้นเพียงแค่ ...

เต็มครึ่ง

อีกสิ่งหนึ่งที่ฉันอยากจะแสดง นี่คือประสิทธิภาพสำหรับปัจจัยการโหลดทั้งสามและขนาด HashMap ที่แตกต่างกัน ค่าคงที่สม่ำเสมอโดยมีการเปลี่ยนแปลงเล็กน้อยยกเว้นการเพิ่มขึ้นหนึ่งครั้งสำหรับปัจจัยการโหลด 1 ฉันอยากรู้จริงๆว่ามันคืออะไร (อาจเป็น GC แต่ใครจะรู้)

ไปขัดขวาง

และนี่คือรหัสสำหรับผู้ที่สนใจ:

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
เหมาะสมที่สุดในแง่ที่การเปลี่ยนค่าดีฟอลต์ให้ประสิทธิภาพที่ดีขึ้น (การดำเนินการใส่ () ที่เร็วขึ้น) สำหรับกรณีนี้
Domchi

2
@Peter GC = การเก็บขยะ
Domchi

2
แผนภูมิเหล่านั้นเรียบร้อย ... คุณใช้อะไรสร้าง / แสดงผล?
G_H

1
@G_H ไม่มีอะไรแฟนซี - ผลลัพธ์ของโปรแกรมข้างต้นและ Excel :)
Domchi

2
ครั้งต่อไปให้ใช้จุดแทนเส้น จะทำให้การเปรียบเทียบง่ายขึ้นด้วยสายตา
Paul Draper

คำตอบ:


74

เอาล่ะเพื่อหยุดสิ่งนี้ฉันได้สร้างแอปทดสอบเพื่อเรียกใช้สถานการณ์สองสามสถานการณ์และรับภาพผลลัพธ์บางอย่าง นี่คือวิธีการทดสอบ:

  • มีการลองคอลเลกชันขนาดต่างๆจำนวนหนึ่ง: หนึ่งร้อยหนึ่งพันและหนึ่งแสนรายการ
  • คีย์ที่ใช้คืออินสแตนซ์ของคลาสที่ ID ระบุโดยไม่ซ้ำกัน การทดสอบแต่ละครั้งใช้คีย์ที่ไม่ซ้ำกันโดยเพิ่มจำนวนเต็มเป็น ID equalsวิธีเดียวที่จะใช้รหัสจึงไม่มีการทำแผนที่ที่สำคัญอีกคนหนึ่งเขียนทับ
  • คีย์จะได้รับรหัสแฮชที่ประกอบด้วยส่วนที่เหลือของโมดูลของ ID เทียบกับหมายเลขที่ตั้งไว้ล่วงหน้า เราจะโทรไปยังหมายเลขที่วงเงินกัญชา สิ่งนี้ทำให้ฉันสามารถควบคุมจำนวนการชนกันของแฮชที่คาดว่าจะเกิดขึ้นได้ ตัวอย่างเช่นหากขนาดคอลเลกชันของเราคือ 100 เราจะมีคีย์ที่มี ID ตั้งแต่ 0 ถึง 99 หากแฮช จำกัด คือ 100 คีย์ทุกคีย์จะมีรหัสแฮชที่ไม่ซ้ำกัน หากขีด จำกัด แฮชคือ 50 คีย์ 0 จะมีรหัสแฮชเช่นเดียวกับคีย์ 50 โดย 1 จะมีรหัสแฮชเหมือนกับ 51 เป็นต้นกล่าวอีกนัยหนึ่งจำนวนแฮชที่คาดหวังต่อคีย์คือขนาดคอลเลกชันหารด้วยแฮช ขีด จำกัด
  • สำหรับการรวมกันของขนาดคอลเลกชันและขีด จำกัด แฮชฉันได้ทำการทดสอบโดยใช้แผนที่แฮชที่เริ่มต้นด้วยการตั้งค่าที่แตกต่างกัน การตั้งค่าเหล่านี้เป็นปัจจัยการโหลดและความจุเริ่มต้นที่แสดงเป็นปัจจัยของการตั้งค่าการรวบรวม ตัวอย่างเช่นการทดสอบที่มีขนาดคอลเลกชัน 100 และปัจจัยความจุเริ่มต้นที่ 1.25 จะเริ่มต้นแผนที่แฮชที่มีความจุเริ่มต้น 125
  • Objectคุ้มค่าสำหรับแต่ละคีย์เป็นเพียงใหม่
  • ผลการทดสอบแต่ละรายการจะถูกห่อหุ้มในอินสแตนซ์ของคลาส Result ในตอนท้ายของการทดสอบทั้งหมดผลลัพธ์จะเรียงลำดับจากประสิทธิภาพโดยรวมที่แย่ที่สุดไปจนถึงดีที่สุด
  • เวลาเฉลี่ยสำหรับการวางและรับจะคำนวณต่อ 10 ครั้ง / รับ
  • ชุดทดสอบทั้งหมดจะรันครั้งเดียวเพื่อกำจัดอิทธิพลการรวบรวม JIT หลังจากนั้นการทดสอบจะดำเนินการเพื่อผลลัพธ์ที่แท้จริง

นี่คือชั้นเรียน:

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

        }

    }

}

การดำเนินการนี้อาจใช้เวลาสักครู่ ผลลัพธ์จะถูกพิมพ์ออกมาตามมาตรฐาน คุณอาจสังเกตเห็นว่าฉันได้แสดงความคิดเห็นไว้ในบรรทัด บรรทัดนั้นเรียกวิชวลไลเซอร์ที่แสดงผลการแสดงผลไปยังไฟล์ png ชั้นเรียนสำหรับสิ่งนี้ได้รับด้านล่าง หากคุณต้องการเรียกใช้ให้ยกเลิกการใส่เครื่องหมายบรรทัดที่เหมาะสมในโค้ดด้านบน ขอเตือน: คลาส Visualizer จะถือว่าคุณใช้งาน Windows และจะสร้างโฟลเดอร์และไฟล์ใน C: \ temp เมื่อทำงานบนแพลตฟอร์มอื่นให้ปรับสิ่งนี้

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

    }

}

ผลลัพธ์ที่มองเห็นได้มีดังนี้:

  • การทดสอบจะแบ่งตามขนาดคอลเลกชันก่อนจากนั้นตามขีด จำกัด แฮช
  • สำหรับการทดสอบแต่ละครั้งจะมีภาพที่แสดงเกี่ยวกับเวลาในการใส่เฉลี่ย (ต่อ 10 ครั้ง) และเวลารับเฉลี่ย (ต่อ 10 ครั้ง) ภาพเหล่านี้เป็น "แผนที่ความร้อน" สองมิติที่แสดงสีต่อการรวมกันของความจุเริ่มต้นและปัจจัยการรับน้ำหนัก
  • สีในภาพจะขึ้นอยู่กับเวลาเฉลี่ยในระดับมาตรฐานจากผลที่ดีที่สุดไปจนถึงแย่ที่สุดตั้งแต่สีเขียวอิ่มตัวไปจนถึงสีแดงอิ่มตัว กล่าวอีกนัยหนึ่งเวลาที่ดีที่สุดจะเป็นสีเขียวเต็มที่ในขณะที่เวลาที่เลวร้ายที่สุดจะเป็นสีแดงเต็มที่ การวัดเวลาที่ต่างกันสองค่าไม่ควรมีสีเดียวกัน
  • แผนที่สีจะคำนวณแยกกันสำหรับการใส่และการรับ แต่ครอบคลุมการทดสอบทั้งหมดสำหรับหมวดหมู่ที่เกี่ยวข้อง
  • การแสดงภาพแสดงความจุเริ่มต้นบนแกน x และโหลดแฟกเตอร์บนแกน y

โดยไม่ต้องกังวลใจอีกต่อไปมาดูผลลัพธ์กันดีกว่า ฉันจะเริ่มต้นด้วยผลลัพธ์สำหรับการวาง

ใส่ผลลัพธ์


ขนาดคอลเลกชัน: 100 ขีด จำกัด แฮช: 50 ซึ่งหมายความว่าแต่ละรหัสแฮชควรเกิดขึ้นสองครั้งและคีย์อื่น ๆ ทั้งหมดจะชนกันในแผนที่แฮช

size_100_hlimit_50_puts

ดีที่เริ่มต้นได้ไม่ดีนัก เราเห็นว่ามีฮอตสปอตขนาดใหญ่สำหรับความจุเริ่มต้น 25% เหนือขนาดคอลเลกชันโดยมีปัจจัยการโหลด 1 ที่มุมล่างซ้ายทำงานได้ไม่ดีนัก


ขนาดคอลเลคชัน: 100 ขีด จำกัด แฮช: 90 คีย์ 1 ใน 10 มีรหัสแฮชซ้ำกัน

size_100_hlimit_90_puts

นี่เป็นสถานการณ์ที่เหมือนจริงกว่าเล็กน้อยไม่มีฟังก์ชันแฮชที่สมบูรณ์แบบ แต่ยังเกิน 10% ฮอตสปอตหายไป แต่เห็นได้ชัดว่าการรวมกันของความจุเริ่มต้นต่ำกับปัจจัยการโหลดต่ำไม่ทำงาน


ขนาดคอลเลกชัน: 100 ขีด จำกัด แฮช: 100 แต่ละคีย์เป็นรหัสแฮชเฉพาะของตัวเอง คาดว่าจะไม่มีการชนกันหากมีที่เก็บข้อมูลเพียงพอ

size_100_hlimit_100_puts

ความจุเริ่มต้นที่ 100 พร้อมตัวประกอบการโหลด 1 ดูเหมือนจะดี น่าแปลกที่ความจุเริ่มต้นที่สูงขึ้นพร้อมกับปัจจัยการรับน้ำหนักที่ต่ำกว่านั้นไม่จำเป็นต้องดี


ขนาดคอลเลกชัน: 1,000 ขีด จำกัด แฮช: 500 รายการเริ่มจริงจังมากขึ้นที่นี่โดยมี 1,000 รายการ เช่นเดียวกับในการทดสอบครั้งแรกแฮชเกินกำลัง 2 ต่อ 1

size_1000_hlimit_500_puts

มุมล่างซ้ายยังทำได้ไม่ดี แต่ดูเหมือนว่าจะมีความสมมาตรระหว่างคำสั่งผสมของจำนวนเริ่มต้นที่ต่ำกว่า / ปัจจัยการรับน้ำหนักสูงและจำนวนเริ่มต้นที่สูงขึ้น / ปัจจัยการโหลดต่ำ


ขนาดคอลเล็กชัน: 1,000 ขีด จำกัด แฮช: 900 ซึ่งหมายความว่ารหัสแฮช 1 ใน 10 จะเกิดขึ้นสองครั้ง สถานการณ์ที่เหมาะสมเกี่ยวกับการชน

size_1000_hlimit_900_puts

มีบางอย่างที่ตลกมากที่เกิดขึ้นกับคอมโบที่ไม่น่าจะเป็นไปได้ของความจุเริ่มต้นที่ต่ำเกินไปโดยมีปัจจัยการโหลดสูงกว่า 1 ซึ่งค่อนข้างตอบโต้ได้ง่าย มิฉะนั้นจะยังค่อนข้างสมมาตร


ขนาดคอลเลกชัน: 1,000 ขีด จำกัด แฮช: 990 การชนกันบางครั้ง แต่มีเพียงไม่กี่ชิ้น ค่อนข้างสมจริงในแง่นี้

size_1000_hlimit_990_puts

เรามีความสมมาตรที่ดีตรงนี้ มุมล่างด้านซ้ายยังคงไม่เหมาะสม แต่คอมโบ 1,000 init capacity / 1.0 load factor เทียบกับ 1250 init capacity / 0.75 load factor อยู่ในระดับเดียวกัน


ขนาดคอลเลกชัน: 1,000 ขีด จำกัด แฮช: 1,000 ไม่มีรหัสแฮชซ้ำ แต่ตอนนี้มีขนาดตัวอย่าง 1,000

size_1000_hlimit_1000_puts

ไม่มากที่จะกล่าวที่นี่ การรวมกันของกำลังการผลิตเริ่มต้นที่สูงขึ้นกับปัจจัยการโหลด 0.75 ดูเหมือนว่าจะมีประสิทธิภาพดีกว่าการรวมกำลังการผลิตเริ่มต้น 1,000 ครั้งโดยมีค่าโหลด 1 เท่า


ขนาดคอลเลกชัน: 100_000. ขีด จำกัด แฮช: 10_000 เอาล่ะตอนนี้เริ่มจริงจังแล้วโดยมีขนาดตัวอย่างรหัสแฮช 1 แสนและ 100 รายการต่อคีย์

size_100000_hlimit_10000_puts

อือ! ฉันคิดว่าเราพบสเปกตรัมที่ต่ำกว่า ความจุในการเริ่มต้นของขนาดคอลเลกชันที่มีค่าน้ำหนักเท่ากับ 1 นั้นทำได้ดีมากที่นี่ แต่นอกเหนือจากนั้นก็มีอยู่ทั่วร้าน


ขนาดคอลเลกชัน: 100_000. ขีด จำกัด แฮช: 90_000 มีความเป็นจริงมากกว่าการทดสอบก่อนหน้าเล็กน้อยที่นี่เรามีแฮชโค้ดเกินพิกัด 10%

size_100000_hlimit_90000_puts

มุมล่างซ้ายยังไม่เป็นที่ต้องการ ความจุเริ่มต้นที่สูงขึ้นจะทำงานได้ดีที่สุด


ขนาดคอลเลกชัน: 100_000. ขีด จำกัด แฮช: 99_000 สถานการณ์ที่ดีนี้ คอลเลกชันขนาดใหญ่ที่มีโค้ดแฮชเกิน 1%

size_100000_hlimit_99000_puts

การใช้ขนาดคอลเลกชันที่แน่นอนเป็นความจุเริ่มต้นด้วยปัจจัยการโหลด 1 ชนะที่นี่ ความสามารถในการเริ่มต้นที่ใหญ่กว่าเล็กน้อยทำงานได้ค่อนข้างดี


ขนาดคอลเลกชัน: 100_000. ขีด จำกัด แฮช: 100_000 อันใหญ่. คอลเลกชันที่ใหญ่ที่สุดพร้อมฟังก์ชันแฮชที่สมบูรณ์แบบ

size_100000_hlimit_100000_puts

บางสิ่งที่น่าแปลกใจที่นี่ ความจุเริ่มต้นพร้อมห้องเพิ่มเติม 50% ที่ปัจจัยการบรรทุก 1 ครั้ง


เอาล่ะเท่านี้ก็เรียบร้อย ตอนนี้เราจะตรวจสอบการได้รับ โปรดจำไว้ว่าแผนที่ด้านล่างทั้งหมดสัมพันธ์กับเวลาที่ได้รับดีที่สุด / แย่ที่สุดเวลาวางจะไม่ถูกนำมาพิจารณาอีกต่อไป

รับผลลัพธ์


ขนาดคอลเล็กชัน: 100 ขีด จำกัด แฮช: 50 ซึ่งหมายความว่าแต่ละรหัสแฮชควรเกิดขึ้นสองครั้งและคาดว่าคีย์อื่น ๆ จะชนกันในแผนที่แฮช

size_100_hlimit_50_gets

เอ๊ะ ... อะไรนะ?


ขนาดคอลเลคชัน: 100 ขีด จำกัด แฮช: 90 คีย์ 1 ใน 10 มีรหัสแฮชซ้ำกัน

size_100_hlimit_90_gets

โอ้โฮเนลลี่! นี่เป็นสถานการณ์ที่น่าจะมีความสัมพันธ์กับคำถามของผู้ถามมากที่สุดและเห็นได้ชัดว่าความจุเริ่มต้นที่ 100 โดยมีปัจจัยการรับน้ำหนักเป็น 1 เป็นหนึ่งในสิ่งที่แย่ที่สุดที่นี่! ฉันสาบานว่าฉันไม่ได้แกล้งทำสิ่งนี้


ขนาดคอลเลกชัน: 100 ขีด จำกัด แฮช: 100 แต่ละคีย์เป็นรหัสแฮชเฉพาะของตัวเอง ไม่คาดว่าจะมีการชนกัน

size_100_hlimit_100_gets

นี้ดูสงบขึ้นเล็กน้อย ส่วนใหญ่ผลลัพธ์เดียวกันทั่วกระดาน


ขนาดคอลเลกชัน: 1,000 ขีด จำกัด แฮช: 500 เช่นเดียวกับในการทดสอบครั้งแรกมีแฮชเกิน 2 ต่อ 1 แต่ตอนนี้มีจำนวนรายการมากขึ้น

size_1000_hlimit_500_gets

ดูเหมือนว่าการตั้งค่าใด ๆ จะให้ผลลัพธ์ที่ดีที่นี่


ขนาดคอลเล็กชัน: 1,000 ขีด จำกัด แฮช: 900 ซึ่งหมายความว่ารหัสแฮช 1 ใน 10 จะเกิดขึ้นสองครั้ง สถานการณ์ที่เหมาะสมเกี่ยวกับการชน

size_1000_hlimit_900_gets

และเช่นเดียวกับที่วางไว้สำหรับการตั้งค่านี้เราได้รับความผิดปกติในจุดแปลก ๆ


ขนาดคอลเลกชัน: 1,000 ขีด จำกัด แฮช: 990 การชนกันบางครั้ง แต่มีเพียงไม่กี่ชิ้น ค่อนข้างสมจริงในแง่นี้

size_1000_hlimit_990_gets

ประสิทธิภาพที่ดีทุกที่ประหยัดสำหรับการรวมกันของความจุเริ่มต้นที่สูงและปัจจัยการโหลดต่ำ ฉันคาดหวังสิ่งนี้ไว้สำหรับการวางเนื่องจากอาจคาดว่าจะมีการปรับขนาดแผนที่แฮชสองรายการ แต่ทำไมถึงได้รับ?


ขนาดคอลเลกชัน: 1,000 ขีด จำกัด แฮช: 1,000 ไม่มีรหัสแฮชซ้ำ แต่ตอนนี้มีขนาดตัวอย่าง 1,000

size_1000_hlimit_1000_gets

การแสดงภาพที่ไม่ชัดเจนทั้งหมด ดูเหมือนว่าจะได้ผลไม่ว่าจะเกิดอะไรขึ้น


ขนาดคอลเลกชัน: 100_000. ขีด จำกัด แฮช: 10_000 เข้าสู่ 100K อีกครั้งโดยมีรหัสแฮชทับซ้อนกันมากมาย

size_100000_hlimit_10000_gets

มันดูไม่สวยแม้ว่าจุดที่ไม่ดีจะมีการแปลเป็นภาษาท้องถิ่นมากก็ตาม ประสิทธิภาพที่นี่ดูเหมือนจะขึ้นอยู่กับการทำงานร่วมกันระหว่างการตั้งค่าเป็นส่วนใหญ่


ขนาดคอลเลกชัน: 100_000. ขีด จำกัด แฮช: 90_000 มีความเป็นจริงมากกว่าการทดสอบก่อนหน้าเล็กน้อยที่นี่เรามีแฮชโค้ดเกินพิกัด 10%

size_100000_hlimit_90000_gets

ความแปรปรวนมากแม้ว่าคุณจะเหล่คุณจะเห็นลูกศรชี้ไปที่มุมขวาบน


ขนาดคอลเลกชัน: 100_000. ขีด จำกัด แฮช: 99_000 สถานการณ์ที่ดีนี้ คอลเลกชันขนาดใหญ่ที่มีโค้ดแฮชเกิน 1%

size_100000_hlimit_99000_gets

วุ่นวายมาก. มันยากที่จะพบโครงสร้างมากมายที่นี่


ขนาดคอลเลกชัน: 100_000. ขีด จำกัด แฮช: 100_000 อันใหญ่. คอลเลกชันที่ใหญ่ที่สุดพร้อมฟังก์ชันแฮชที่สมบูรณ์แบบ

size_100000_hlimit_100000_gets

มีใครคิดว่าสิ่งนี้เริ่มดูเหมือนกราฟิก Atari หรือไม่? ดูเหมือนว่าจะรองรับความจุเริ่มต้นของขนาดคอลเลกชันที่แน่นอนคือ -25% หรือ + 50%


เอาล่ะได้เวลาสรุปแล้ว ...

  • เกี่ยวกับเวลาใส่: คุณจะต้องหลีกเลี่ยงความจุเริ่มต้นที่ต่ำกว่าจำนวนรายการแผนที่ที่คาดไว้ หากทราบตัวเลขที่แน่นอนไว้ก่อนตัวเลขนั้นหรือสูงกว่าเล็กน้อยดูเหมือนจะทำงานได้ดีที่สุด ปัจจัยที่มีภาระงานสูงสามารถชดเชยขีดความสามารถเริ่มต้นที่ลดลงเนื่องจากการปรับขนาดแผนที่แฮชก่อนหน้านี้ สำหรับความจุเริ่มต้นที่สูงขึ้นดูเหมือนว่าจะไม่สำคัญมากนัก
  • เกี่ยวกับเวลารับ: ผลลัพธ์จะวุ่นวายเล็กน้อยที่นี่ ไม่มีอะไรมากที่จะสรุป ดูเหมือนว่าจะต้องพึ่งพาอัตราส่วนที่ละเอียดอ่อนระหว่างการทับซ้อนของรหัสแฮชความจุเริ่มต้นและปัจจัยการโหลดโดยที่การตั้งค่าที่ไม่ดีบางอย่างจะทำงานได้ดีและการตั้งค่าที่ดีมีประสิทธิภาพอย่างมาก
  • เห็นได้ชัดว่าฉันเต็มไปด้วยเรื่องไร้สาระเมื่อพูดถึงสมมติฐานเกี่ยวกับประสิทธิภาพของ Java ความจริงก็คือเว้นแต่คุณจะปรับแต่งการตั้งค่าของคุณให้เข้ากับการใช้งานได้อย่างสมบูรณ์แบบHashMapผลลัพธ์ก็จะเป็นไปได้ หากมีสิ่งหนึ่งที่ต้องหลีกเลี่ยงจากสิ่งนี้ก็คือขนาดเริ่มต้นเริ่มต้นที่ 16 นั้นค่อนข้างโง่สำหรับทุกอย่างยกเว้นแผนที่ที่เล็กที่สุดดังนั้นให้ใช้ตัวสร้างที่กำหนดขนาดเริ่มต้นหากคุณมีความคิดเกี่ยวกับลำดับของขนาด มันจะเป็น
  • เรากำลังวัดเป็นนาโนวินาทีที่นี่ เวลาเฉลี่ยที่ดีที่สุดต่อ 10 ครั้งคือ 1179 ns และ 5105 ns ที่แย่ที่สุดในเครื่องของฉัน เวลาเฉลี่ยที่ดีที่สุดต่อ 10 ครั้งคือ 547 ns และ 3484 ns ที่แย่ที่สุด นั่นอาจเป็นความแตกต่างของปัจจัยที่ 6 แต่เรากำลังพูดถึงไม่ถึงมิลลิวินาที ในคอลเลกชันที่มีขนาดใหญ่กว่าที่ผู้โพสต์คิดไว้อย่างมาก

ก็แค่นั้นแหละ ฉันหวังว่ารหัสของฉันจะไม่มีการกำกับดูแลที่น่ากลัวซึ่งทำให้ทุกสิ่งที่ฉันโพสต์ที่นี่เป็นโมฆะ นี่เป็นเรื่องสนุกและฉันได้เรียนรู้ว่าในท้ายที่สุดคุณก็อาจพึ่งพา Java ในการทำงานได้ดีกว่าที่จะคาดหวังความแตกต่างอย่างมากจากการเพิ่มประสิทธิภาพเพียงเล็กน้อย นั่นไม่ได้หมายความว่าไม่ควรหลีกเลี่ยงบางสิ่ง แต่ส่วนใหญ่เรากำลังพูดถึงการสร้างสตริงที่ยาวสำหรับลูปโดยใช้โครงสร้างข้อมูลที่ไม่ถูกต้องและสร้างอัลกอริทึม O (n ^ 3)


1
ขอบคุณสำหรับความพยายามดูดีมาก! เพื่อไม่ให้ขี้เกียจฉันเพิ่มกราฟสวย ๆ ลงในผลลัพธ์ของฉันด้วย การทดสอบของฉันดุร้ายกว่าของคุณเล็กน้อย แต่ฉันพบว่าความแตกต่างนั้นชัดเจนกว่าเมื่อใช้แผนที่ที่ใหญ่กว่า ด้วยแผนที่ขนาดเล็กไม่ว่าจะทำอะไรก็พลาดไม่ได้ ประสิทธิภาพมีแนวโน้มที่จะวุ่นวายเนื่องจากการเพิ่มประสิทธิภาพ JVM และ GC และฉันมีทฤษฎีที่ว่าข้อสรุปที่ชัดเจนใด ๆ ได้รับผลจากความสับสนวุ่นวายนั้นสำหรับชุดข้อมูลที่มีขนาดเล็กของคุณ
Domchi

อีกหนึ่งความคิดเห็นเกี่ยวกับการเพิ่มประสิทธิภาพ ดูเหมือนวุ่นวาย แต่ฉันพบว่ามันแตกต่างกันไปมากในช่วงที่แคบมาก แต่โดยรวมแล้วมันคงที่และน่าเบื่อเหมือนนรก ฉันได้รับ spikes แปลก ๆ เป็นครั้งคราวเช่นที่คุณทำใน 100/90 ฉันไม่สามารถอธิบายได้ แต่ในทางปฏิบัติมันอาจจะสังเกตไม่ได้
Domchi

G_H โปรดดูคำตอบของฉันฉันรู้ว่านี่เป็นกระทู้เก่ามาก แต่การทดสอบของคุณควรได้รับการพิจารณาใหม่โดยคำนึงถึงสิ่งนี้
durron597

เฮ้คุณควรโพสต์สิ่งนี้ไปยัง ACM เป็นเอกสารการประชุม :) ช่างเป็นความพยายาม!
yerlilbilgin

12

นี่เป็นหัวข้อที่ยอดเยี่ยมมากยกเว้นว่ามีสิ่งสำคัญอย่างหนึ่งที่คุณขาดหายไป คุณพูดว่า:

อยากรู้อยากเห็นความจุความจุ +1 ความจุ +2 ความจุ -1 และแม้แต่ความจุ -10 ทั้งหมดให้ผลลัพธ์ที่เหมือนกันทุกประการ ฉันคาดหวังว่าอย่างน้อยความจุ -1 และความจุ -10 จะให้ผลลัพธ์ที่แย่ลง

ซอร์สโค้ดจะเพิ่มขีดความสามารถเริ่มต้นด้วยกำลังสูงสุดของสองภายใน นั่นหมายความว่าตัวอย่างเช่นความจุเริ่มต้นที่ 513, 600, 700, 800, 900, 1000 และ 1024 ทั้งหมดจะใช้ความจุเริ่มต้นเดียวกัน (1024) สิ่งนี้ไม่ได้ทำให้การทดสอบที่ทำโดย @G_H เป็นโมฆะ แต่เราควรตระหนักว่ากำลังดำเนินการอยู่ก่อนที่จะวิเคราะห์ผลลัพธ์ของเขา และยังอธิบายพฤติกรรมแปลก ๆ ของการทดสอบบางอย่าง

นี่คือตัวสร้างที่เหมาะสมสำหรับแหล่งที่มา JDK:

/**
 * 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();
}

ที่น่าสนใจมาก! ฉันไม่รู้เรื่องนี้ อธิบายสิ่งที่ฉันเห็นในการทดสอบ และอีกครั้งเป็นการยืนยันว่าการเพิ่มประสิทธิภาพก่อนกำหนดมักมีประโยชน์เพราะคุณไม่รู้จริงๆ (หรือควรจะต้องรู้) ว่าคอมไพเลอร์หรือโค้ดอาจทำอะไรอยู่ด้านหลังของคุณ และแน่นอนว่าอาจแตกต่างกันไปตามเวอร์ชัน / การนำไปใช้งาน ขอขอบคุณที่แก้ไขปัญหานี้!
G_H

@G_H ฉันอยากเห็นการทดสอบของคุณดำเนินการอีกครั้งโดยเลือกตัวเลขที่เหมาะสมกว่าเนื่องจากข้อมูลนี้ ตัวอย่างเช่นถ้าฉันมี 1200 องค์ประกอบฉันควรใช้แผนที่ 1024 แผนที่ 2048 หรือแผนที่ 4096 ฉันไม่รู้คำตอบของคำถามเดิมนั่นคือเหตุผลที่ฉันพบว่าหัวข้อนี้เริ่มต้นด้วย แต่ฉันรู้ว่าฝรั่งคูณของคุณexpectedSizeโดย1.33เมื่อคุณทำMaps.newHashMap(int expectedSize)
durron597

หาก HashMap ไม่ปัดเศษขึ้นเป็นค่ากำลังสองสำหรับที่capacityเก็บข้อมูลบางส่วนจะไม่ถูกใช้ bucketIndex = hashCode(key) & (capacity-1)ดัชนีถังสำหรับที่จะนำข้อมูลแผนที่ที่ถูกกำหนดโดย ดังนั้นหากcapacityเป็นสิ่งอื่นที่ไม่ใช่กำลังสองการแทนค่าฐานสองของ(capacity-1)จะมีศูนย์อยู่ซึ่งหมายความว่าการดำเนินการ&(ไบนารีและ) จะทำให้บิตที่ต่ำกว่าของ hashCode เป็นศูนย์เสมอ ตัวอย่าง: (capacity-1)คือ111110(62) แทนที่จะเป็น111111(63) ในกรณีนี้สามารถใช้ได้เฉพาะที่เก็บข้อมูลที่มีดัชนีคู่เท่านั้น
Michael Geier

2

เพียงแค่ไปกับ101. ฉันไม่แน่ใจจริงๆว่ามันจำเป็น แต่มันก็ไม่คุ้มค่ากับความพยายามที่จะต้องกังวลอย่างแน่นอน

... เพียงแค่เพิ่มไฟล์1.


แก้ไข:เหตุผลบางประการสำหรับคำตอบของฉัน

อย่างแรกฉันคิดว่าคุณHashMapจะไม่เติบโตไปไกลกว่า100นั้น ถ้าเป็นเช่นนั้นคุณควรปล่อยให้โหลดแฟคเตอร์เหมือนเดิม ในทำนองเดียวกันถ้าคุณกังวลประสิทธิภาพปล่อยให้โหลดปัจจัยที่เป็นอยู่ หากข้อกังวลของคุณคือหน่วยความจำคุณสามารถบันทึกบางส่วนได้โดยการตั้งค่าขนาดคงที่ สิ่งนี้อาจจะคุ้มค่าหากคุณยัดเยียดสิ่งต่างๆมากมายในความทรงจำ กล่าวคือกำลังจัดเก็บแผนที่จำนวนมากหรือสร้างแผนที่ขนาดใหญ่ที่เน้นพื้นที่

อย่างที่สองฉันเลือกค่านี้101เพราะมีความสามารถในการอ่านที่ดีกว่า ... ถ้าฉันดูโค้ดของคุณในภายหลังและเห็นว่าคุณได้ตั้งค่าความจุเริ่มต้น100และคุณกำลังโหลดด้วย100องค์ประกอบฉันจะต้อง อ่านผ่าน Javadoc 100เพื่อให้แน่ใจว่าจะไม่ปรับขนาดเมื่อมันมาถึงได้อย่างแม่นยำ แน่นอนฉันจะไม่พบคำตอบที่นั่นดังนั้นฉันจะต้องดูที่มา สิ่งนี้ไม่คุ้มค่า ... เพียงแค่ปล่อยไว้101และทุกคนก็มีความสุขและไม่มีใครมองหาซอร์สโค้ดของjava.util.HashMap. ฮูราห์

ประการที่สามการอ้างว่าการตั้งค่าHashMapความจุที่แน่นอนของสิ่งที่คุณคาดหวังด้วยปัจจัยการโหลด1 " จะฆ่าประสิทธิภาพการค้นหาและการแทรกของคุณ "นั้นไม่เป็นความจริงแม้ว่าจะทำเป็นตัวหนาก็ตาม

... ถ้าคุณมีที่nเก็บข้อมูลและสุ่มกำหนดnไอเทมลงในnถังใช่คุณจะต้องจบลงด้วยไอเท็มในถังเดียวกันแน่นอน ... แต่นั่นไม่ใช่จุดจบของโลก ... ในทางปฏิบัติ มันเป็นเพียงการเปรียบเทียบที่เท่าเทียมกันอีกสองสามข้อ ในความเป็นจริงมี esp. ความแตกต่างเล็กน้อยเมื่อคุณพิจารณาว่าทางเลือกคือการกำหนดnรายการลงในn/0.75ถัง

ไม่ต้องใช้คำพูดของฉันมัน ...


รหัสทดสอบด่วน:

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 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: ↑ - มีประมาณนี้ → || ← ความแตกต่างระหว่างการตั้งค่าต่างๆ


ด้วยความเคารพต่อคำตอบเดิมของฉัน (บิตเหนือเส้นแนวนอนแรก) มันก็จงใจกะล่อนเพราะในกรณีส่วนใหญ่ , ประเภทของไมโครเพิ่มประสิทธิภาพนี้ไม่ดี


@EJP ฉันเดาไม่ถูก ดูการแก้ไขด้านบน การคาดเดาของคุณไม่ถูกต้องเกี่ยวกับการคาดเดาของใครถูกต้องและการคาดเดาของใครไม่ถูกต้อง
badroit

(... บางทีฉันอาจจะเป็นคน
ขี้ขลาด

3
คุณอาจจะรำคาญ EJP อย่างถูกต้อง แต่ตอนนี้ถึงตาของฉันแล้ว P - ในขณะที่ฉันยอมรับว่าการเพิ่มประสิทธิภาพก่อนกำหนดนั้นเหมือนกับการหลั่งเร็ว แต่อย่าคิดว่าสิ่งที่มักจะไม่คุ้มค่ากับความพยายามนั้นไม่คุ้มกับความพยายามในกรณีของฉัน . ในกรณีของฉันมันสำคัญพอที่ฉันไม่อยากจะเดาฉันจึงค้นหามัน - ไม่จำเป็นต้องใช้ +1 ในกรณีของฉัน (แต่อาจเป็นเพราะความจุเริ่มต้น / จริงของคุณไม่เท่ากันและ loadFactor ไม่ใช่ 1 ดูการร่ายนี้เป็น int ใน HashMap: threshold = (int) (capacity * loadFactor))
Domchi

@badroit คุณพูดอย่างชัดเจนว่าฉันไม่แน่ใจจริงๆว่ามันจำเป็น '. ดังนั้นจึงเป็นการคาดเดา ตอนนี้คุณได้ทำและโพสต์งานวิจัยแล้วมันไม่ใช่เรื่องที่ต้องคาดเดาอีกต่อไปและอย่างที่คุณไม่เคยทำมาก่อนเห็นได้ชัดว่าเป็นการคาดเดาไม่เช่นนั้นคุณจะต้องแน่ใจ สำหรับคำว่า "ไม่ถูกต้อง" Javadoc กำหนดให้โหลดแฟ็กเตอร์ 0.75 อย่างชัดเจนเช่นเดียวกับการวิจัยหลายทศวรรษและคำตอบของ G_H ในที่สุด 'มันอาจไม่คุ้มกับความพยายาม' ดูความคิดเห็นของ Domchi ที่นี่ ไม่ปล่อยให้สิ่งที่ถูกต้องมากนักแม้ว่าโดยทั่วไปฉันเห็นด้วยกับคุณเกี่ยวกับการเพิ่มประสิทธิภาพไมโคร
user207421

ผ่อนคลายนะทุกคน ใช่คำตอบของฉันพูดเกินจริง หากคุณมีวัตถุ 100 ชิ้นที่ไม่มีequalsฟังก์ชั่นหนักมากคุณอาจจะไม่ต้องใส่มันในรายการและใช้แค่คำว่า "มี" ด้วยชุดเล็ก ๆ เช่นนี้จะไม่มีความแตกต่างอย่างมากในด้านประสิทธิภาพ มันสำคัญมากก็ต่อเมื่อความกังวลเรื่องความเร็วหรือหน่วยความจำเหนือสิ่งอื่นใดหรือเท่ากับและแฮชมีความเฉพาะเจาะจงมาก ฉันจะทำการทดสอบในภายหลังด้วยคอลเลคชันขนาดใหญ่และปัจจัยโหลดต่างๆและความสามารถเริ่มต้นเพื่อดูว่าฉันเต็มไปด้วยอึหรือไม่
G_H


1

จากHashMapJavaDoc:

ตามกฎทั่วไปค่าเริ่มต้นโหลดแฟคเตอร์ (.75) ให้การแลกเปลี่ยนที่ดีระหว่างต้นทุนเวลาและพื้นที่ ค่าที่สูงกว่าจะลดค่าใช้จ่ายของพื้นที่ แต่เพิ่มต้นทุนการค้นหา (สะท้อนให้เห็นในการดำเนินการส่วนใหญ่ของคลาส HashMap รวมถึงการรับและวาง) จำนวนรายการที่คาดไว้ในแผนที่และปัจจัยการรับน้ำหนักควรนำมาพิจารณาเมื่อตั้งค่าความจุเริ่มต้นเพื่อลดจำนวนการดำเนินการ rehash ให้น้อยที่สุด หากความจุเริ่มต้นมากกว่าจำนวนสูงสุดของรายการหารด้วยปัจจัยการโหลดจะไม่มีการดำเนินการ rehash เกิดขึ้น

ดังนั้นหากคุณคาดหวังว่าจะมี 100 รายการบางทีอาจต้องใช้ load factor ที่ 0.75 และความจุเริ่มต้นของเพดาน (100 / 0.75) จะดีที่สุด ที่ลงมาเป็น 134

ฉันต้องยอมรับว่าฉันไม่แน่ใจว่าทำไมค่าใช้จ่ายในการค้นหาจึงสูงกว่าสำหรับปัจจัยการโหลดที่สูงขึ้น เพียงเพราะ HashMap "แออัด" มากขึ้นไม่ได้หมายความว่าจะมีวัตถุจำนวนมากขึ้นในถังเดียวกันใช่ไหม ขึ้นอยู่กับรหัสแฮชของพวกเขาเท่านั้นถ้าฉันจำไม่ผิด ดังนั้นสมมติว่ามีการแพร่กระจายรหัสแฮชที่ดีกรณีส่วนใหญ่ไม่ควรเป็น O (1) โดยไม่คำนึงถึงปัจจัยการโหลดหรือไม่?

แก้ไข: ฉันควรอ่านเพิ่มเติมก่อนโพสต์ ... แน่นอนว่ารหัสแฮชไม่สามารถแมปโดยตรงกับดัชนีภายในบางรายการได้ จะต้องลดลงเป็นค่าที่เหมาะสมกับความจุปัจจุบัน หมายความว่ายิ่งความจุเริ่มต้นของคุณมากเท่าไหร่คุณก็ยิ่งคาดหวังว่าจำนวนการชนกันของแฮชจะน้อยลงเท่านั้น การเลือกความจุเริ่มต้นให้ตรงกับขนาด (หรือ +1) ของวัตถุของคุณที่ตั้งค่าด้วยปัจจัยการโหลดเป็น 1 จะทำให้แน่ใจได้ว่าแผนที่ของคุณจะไม่ถูกปรับขนาด อย่างไรก็ตามมันจะฆ่าการค้นหาและประสิทธิภาพการแทรกของคุณ. การปรับขนาดยังค่อนข้างรวดเร็วและอาจเกิดขึ้นเพียงครั้งเดียวในขณะที่การค้นหาเสร็จสิ้นในงานที่เกี่ยวข้องกับแผนที่ ด้วยเหตุนี้การเพิ่มประสิทธิภาพสำหรับการค้นหาอย่างรวดเร็วจึงเป็นสิ่งที่คุณต้องการที่นี่ คุณสามารถรวมสิ่งนั้นเข้าด้วยกันโดยไม่ต้องปรับขนาดโดยทำตามที่ JavaDoc บอก: ใช้ความจุที่คุณต้องการหารด้วยโหลดแฟกเตอร์ที่เหมาะสมที่สุด (เช่น 0.75) และใช้เป็นความจุเริ่มต้นกับโหลดแฟกเตอร์นั้น เพิ่ม 1 เพื่อให้แน่ใจว่าการปัดเศษไม่ได้รับคุณ


1
" มันจะฆ่าการค้นหาและประสิทธิภาพการแทรกของคุณ " นี่เป็นการพูดเกินจริง / ธรรมดา - ไม่ถูกต้อง
badroit

1
การทดสอบของฉันแสดงให้เห็นว่าประสิทธิภาพการค้นหาไม่ได้รับผลกระทบจากการตั้งค่าโหลดแฟกเตอร์ของ 1 ประสิทธิภาพการแทรกดีขึ้นจริง เนื่องจากไม่มีการปรับขนาดจึงเร็วกว่า ดังนั้นคำสั่งของคุณจึงถูกต้องสำหรับกรณีทั่วไป (การค้นหา HashMap ที่มีองค์ประกอบจำนวนน้อยจะเร็วกว่า 0.75 โดยมี 1) แต่ไม่ถูกต้องสำหรับกรณีเฉพาะของฉันเมื่อ HashMap เต็มตามความจุสูงสุดเสมอซึ่งไม่เคยเปลี่ยนแปลง คำแนะนำของคุณในการตั้งค่าขนาดเริ่มต้นให้สูงขึ้นนั้นน่าสนใจ แต่ไม่เกี่ยวข้องกับกรณีของฉันเนื่องจากตารางของฉันไม่โตขึ้นดังนั้นปัจจัยการรับน้ำหนักจึงมีความสำคัญในแง่ของการปรับขนาดเท่านั้น
Domchi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.