Dung lượng và hệ số tải tối ưu cho HashMap có kích thước cố định là bao nhiêu?


85

Tôi đang cố gắng tìm ra công suất và hệ số tải tối ưu cho một trường hợp cụ thể. Tôi nghĩ rằng tôi đã hiểu được ý chính của nó, nhưng tôi vẫn rất biết ơn vì đã có lời xác nhận từ một người có kiến ​​thức hơn tôi. :)

Nếu tôi biết rằng HashMap của mình sẽ lấp đầy để chứa 100 đối tượng và sẽ dành phần lớn thời gian để có 100 đối tượng, tôi đoán rằng các giá trị tối ưu là dung lượng ban đầu 100 và hệ số tải 1? Hay tôi cần dung lượng 101, hoặc có bất kỳ thứ gì khác không?

CHỈNH SỬA: OK, tôi đã dành một vài giờ và thực hiện một số thử nghiệm. Đây là kết quả:

  • Thật kỳ lạ, công suất, công suất + 1, công suất + 2, công suất-1 và thậm chí công suất-10 đều mang lại kết quả chính xác như nhau. Tôi mong đợi ít nhất năng lực-1 và năng lực-10 sẽ cho kết quả tồi tệ hơn.
  • Sử dụng dung lượng ban đầu (thay vì sử dụng giá trị mặc định là 16) mang lại sự cải thiện đáng kể về put () - nhanh hơn tới 30%.
  • Sử dụng hệ số tải bằng 1 cho hiệu suất bằng nhau đối với số lượng đối tượng nhỏ và hiệu suất tốt hơn đối với số lượng đối tượng lớn hơn (> 100000). Tuy nhiên, điều này không cải thiện tỷ lệ thuận với số lượng đối tượng; Tôi nghi ngờ có thêm yếu tố ảnh hưởng đến kết quả.
  • Hiệu suất get () hơi khác nhau đối với số lượng đối tượng / dung lượng khác nhau, nhưng mặc dù nó có thể hơi khác nhau tùy từng trường hợp, nói chung nó không bị ảnh hưởng bởi dung lượng ban đầu hoặc hệ số tải.

EDIT2: Tôi cũng đang thêm một số biểu đồ. Đây là một minh họa sự khác biệt giữa hệ số tải 0,75 và 1, trong trường hợp tôi khởi tạo HashMap và lấp đầy nó đến hết dung lượng. Theo thang điểm y là thời gian tính bằng ms (càng thấp càng tốt) và thang x là kích thước (số lượng đối tượng). Vì kích thước thay đổi tuyến tính, thời gian cần thiết cũng tăng theo tuyến tính.

Vì vậy, hãy xem những gì tôi nhận được. Hai biểu đồ sau đây cho thấy sự khác biệt về hệ số tải. Biểu đồ đầu tiên cho thấy điều gì sẽ xảy ra khi HashMap được lấp đầy dung lượng; hệ số tải 0,75 hoạt động kém hơn do thay đổi kích thước. Tuy nhiên, nó không tệ hơn liên tục, và có đủ loại va chạm - tôi đoán rằng GC có vai trò chính trong việc này. Hệ số tải trọng 1,25 hoạt động giống như 1, vì vậy nó không được đưa vào biểu đồ.

Đầy đủ

Biểu đồ này chứng minh rằng 0,75 tệ hơn do thay đổi kích thước; nếu chúng ta lấp đầy HashMap đến một nửa dung lượng, 0,75 không tệ hơn, chỉ là ... khác (và nó sẽ sử dụng ít bộ nhớ hơn và có hiệu suất lặp tốt hơn đáng chú ý).

đầy một nửa

Một điều nữa tôi muốn thể hiện. Đây là nhận hiệu suất cho cả ba yếu tố tải và các kích thước HashMap khác nhau. Liên tục không đổi với một chút thay đổi, ngoại trừ một lần tăng đột biến cho hệ số tải 1. Tôi thực sự muốn biết đó là gì (có thể là GC, nhưng ai biết được).

tăng đột biến

Và đây là mã cho những người quan tâm:

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
Tối ưu theo nghĩa là thay đổi giá trị mặc định mang lại hiệu suất tốt hơn (thực thi put () nhanh hơn) cho trường hợp này.
Domchi

2
@Peter GC = thu gom rác.
Domchi

2
Những biểu đồ đó rất gọn gàng ... Bạn sẽ sử dụng gì để tạo / hiển thị chúng?
G_H

1
@G_H Không có gì lạ mắt - kết quả của chương trình trên và Excel. :)
Domchi

2
Lần tới, hãy sử dụng điểm thay vì đường. Nó sẽ làm cho việc so sánh trực quan dễ dàng hơn.
Paul Draper,

Câu trả lời:


74

Được rồi, để tạm dừng việc này, tôi đã tạo một ứng dụng thử nghiệm để chạy một vài tình huống và nhận được một số hình dung về kết quả. Đây là cách các bài kiểm tra được thực hiện:

  • Một số kích thước bộ sưu tập khác nhau đã được thử: một trăm, một nghìn và một trăm nghìn mục nhập.
  • Các khóa được sử dụng là các phiên bản của một lớp được nhận dạng duy nhất bởi một ID. Mỗi bài kiểm tra sử dụng các khóa duy nhất, với các số nguyên tăng dần làm ID. Các equalsphương pháp duy nhất sử dụng ID, vì vậy không có bản đồ chủ chốt ghi đè nhau.
  • Các khóa nhận được một mã băm bao gồm phần còn lại của mô-đun của ID của chúng dựa trên một số đặt trước. Chúng tôi sẽ gọi số đó là giới hạn băm . Điều này cho phép tôi kiểm soát số lượng xung đột băm sẽ được mong đợi. Ví dụ: nếu kích thước bộ sưu tập của chúng tôi là 100, chúng tôi sẽ có các khóa có ID từ 0 đến 99. Nếu giới hạn băm là 100, mọi khóa sẽ có một mã băm duy nhất. Nếu giới hạn băm là 50, khóa 0 sẽ có cùng mã băm với khóa 50, 1 sẽ có cùng mã băm là 51, v.v. Nói cách khác, số lần va chạm băm dự kiến ​​trên mỗi khóa là kích thước tập hợp chia cho hàm băm giới hạn.
  • Đối với mỗi sự kết hợp giữa kích thước bộ sưu tập và giới hạn băm, tôi đã chạy thử nghiệm bằng cách sử dụng bản đồ băm được khởi tạo với các cài đặt khác nhau. Các cài đặt này là hệ số tải và công suất ban đầu được biểu thị như một hệ số của cài đặt thu. Ví dụ: một bài kiểm tra với kích thước tập hợp là 100 và hệ số dung lượng ban đầu là 1,25 sẽ khởi tạo một bản đồ băm với dung lượng ban đầu là 125.
  • Giá trị cho mỗi khóa chỉ đơn giản là một khóa mới Object.
  • Mỗi kết quả kiểm tra được đóng gói trong một thể hiện của lớp Kết quả. Vào cuối tất cả các bài kiểm tra, kết quả được sắp xếp từ hiệu suất tổng thể kém nhất đến tốt nhất.
  • Thời gian trung bình để đặt và nhận được tính trên 10 lần đặt / được.
  • Tất cả các kết hợp thử nghiệm được chạy một lần để loại bỏ ảnh hưởng của quá trình biên dịch JIT. Sau đó, các bài kiểm tra được chạy để có kết quả thực tế.

Đây là lớp học:

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

        }

    }

}

Việc chạy này có thể mất một lúc. Kết quả được in ra tiêu chuẩn. Bạn có thể nhận thấy tôi đã nhận xét ra một dòng. Dòng đó gọi một trình hiển thị hiển thị các biểu diễn trực quan của kết quả thành tệp png. Lớp cho điều này được đưa ra dưới đây. Nếu bạn muốn chạy nó, hãy bỏ ghi chú dòng thích hợp trong đoạn mã trên. Được cảnh báo: lớp trình hiển thị giả định bạn đang chạy trên Windows và sẽ tạo các thư mục và tệp trong C: \ temp. Khi chạy trên nền tảng khác, hãy điều chỉnh điều này.

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

    }

}

Đầu ra được hiển thị như sau:

  • Trước tiên, các thử nghiệm được chia theo kích thước tập hợp, sau đó là giới hạn băm.
  • Đối với mỗi bài kiểm tra, có một hình ảnh đầu ra liên quan đến thời gian đặt trung bình (mỗi 10 lần đặt) và thời gian nhận trung bình (trên 10 lần đặt). Các hình ảnh là "bản đồ nhiệt" hai chiều hiển thị một màu cho mỗi sự kết hợp của công suất ban đầu và hệ số tải.
  • Màu sắc trong hình ảnh dựa trên thời gian trung bình trên thang chuẩn hóa từ kết quả tốt nhất đến xấu nhất, từ màu xanh lá cây bão hòa đến màu đỏ bão hòa. Nói cách khác, thời điểm tốt nhất sẽ có màu xanh hoàn toàn, trong khi thời điểm xấu nhất sẽ có màu đỏ hoàn toàn. Hai phép đo thời gian khác nhau không bao giờ được có cùng màu.
  • Các bản đồ màu được tính toán riêng biệt cho lượt đặt và lượt nhận, nhưng bao gồm tất cả các bài kiểm tra cho các danh mục tương ứng của chúng.
  • Hình ảnh trực quan cho thấy công suất ban đầu trên trục x và hệ số tải trên trục y.

Không cần thêm lời khuyên, hãy cùng xem kết quả. Tôi sẽ bắt đầu với kết quả đặt.

Đặt kết quả


Kích thước bộ sưu tập: 100. Giới hạn băm: 50. Điều này có nghĩa là mỗi mã băm phải xảy ra hai lần và mọi khóa khác xung đột trong bản đồ băm.

size_100_hlimit_50_puts

Chà, điều đó không khởi đầu tốt lắm. Chúng tôi thấy rằng có một điểm phát sóng lớn cho công suất ban đầu cao hơn 25% so với kích thước bộ sưu tập, với hệ số tải là 1. Góc dưới bên trái hoạt động không quá tốt.


Kích thước bộ sưu tập: 100. Giới hạn băm: 90. Một trong mười khóa có mã băm trùng lặp.

size_100_hlimit_90_puts

Đây là một kịch bản thực tế hơn một chút, không có một hàm băm hoàn hảo nhưng vẫn quá tải 10%. Điểm phát sóng đã biến mất, nhưng sự kết hợp giữa dung lượng ban đầu thấp với hệ số tải thấp rõ ràng là không hoạt động.


Kích thước bộ sưu tập: 100. Giới hạn băm: 100. Mỗi khóa như một mã băm duy nhất của riêng nó. Không có va chạm dự kiến ​​nếu có đủ xô.

size_100_hlimit_100_puts

Công suất ban đầu là 100 với hệ số tải là 1 có vẻ ổn. Đáng ngạc nhiên, công suất ban đầu cao hơn với hệ số tải thấp hơn không nhất thiết là tốt.


Kích thước bộ sưu tập: 1000. Giới hạn băm: 500. Ở đây ngày càng nghiêm trọng hơn, với 1000 mục nhập. Giống như trong thử nghiệm đầu tiên, có quá tải băm từ 2 đến 1.

size_1000_hlimit_500_puts

Góc dưới bên trái vẫn không hoạt động tốt. Nhưng dường như có sự đối xứng giữa sự kết hợp của số lượng ban đầu thấp hơn / hệ số tải cao và số lượng ban đầu cao hơn / hệ số tải thấp.


Kích thước tập hợp: 1000. Giới hạn băm: 900. Điều này có nghĩa là một trong mười mã băm sẽ xảy ra hai lần. Kịch bản hợp lý liên quan đến va chạm.

size_1000_hlimit_900_puts

Có điều gì đó rất buồn cười đang xảy ra với sự kết hợp không chắc chắn của công suất ban đầu quá thấp với hệ số tải trên 1, điều này khá phản trực quan. Nếu không, vẫn khá đối xứng.


Kích thước bộ sưu tập: 1000. Giới hạn băm: 990. Một số va chạm, nhưng chỉ một số ít. Khá thực tế về mặt này.

size_1000_hlimit_990_puts

Chúng ta có một sự đối xứng đẹp ở đây. Góc dưới bên trái vẫn chưa tối ưu, nhưng kết hợp công suất 1000 init / hệ số tải 1,0 so với công suất 1250 init / hệ số tải 0,75 là ở cùng một mức.


Kích thước bộ sưu tập: 1000. Giới hạn băm: 1000. Không có mã băm trùng lặp, nhưng hiện có kích thước mẫu là 1000.

size_1000_hlimit_1000_puts

Không có nhiều điều để nói ở đây. Sự kết hợp của công suất ban đầu cao hơn với hệ số tải 0,75 có vẻ tốt hơn một chút so với sự kết hợp của công suất ban đầu 1000 với hệ số tải là 1.


Kích thước bộ sưu tập: 100_000. Giới hạn băm: 10_000. Được rồi, nó đang trở nên nghiêm trọng với kích thước mẫu là một trăm nghìn và 100 bản sao mã băm trên mỗi khóa.

size_100000_hlimit_10000_puts

Rất tiếc! Tôi nghĩ rằng chúng tôi đã tìm thấy phổ thấp hơn của mình. Công suất init có kích thước chính xác với kích thước bộ sưu tập với hệ số tải là 1 đang hoạt động rất tốt ở đây, nhưng khác với điều đó là tất cả các cửa hàng.


Kích thước bộ sưu tập: 100_000. Giới hạn băm: 90_000. Thực tế hơn một chút so với thử nghiệm trước, ở đây chúng tôi có quá tải 10% trong mã băm.

size_100000_hlimit_90000_puts

Góc dưới bên trái vẫn không mong muốn. Công suất ban đầu cao hơn hoạt động tốt nhất.


Kích thước bộ sưu tập: 100_000. Giới hạn băm: 99_000. Kịch bản tốt, điều này. Một bộ sưu tập lớn với quá tải mã băm 1%.

size_100000_hlimit_99000_puts

Sử dụng kích thước tập hợp chính xác làm dung lượng init với hệ số tải là 1 sẽ thắng ở đây! Tuy nhiên, dung lượng init lớn hơn một chút hoạt động khá tốt.


Kích thước bộ sưu tập: 100_000. Giới hạn băm: 100_000. Cái lớn. Bộ sưu tập lớn nhất với hàm băm hoàn hảo.

size_100000_hlimit_100000_puts

Một số thứ đáng ngạc nhiên ở đây. Công suất ban đầu có thêm 50% phòng với hệ số tải là 1 sẽ thắng.


Được rồi, thế là xong. Bây giờ, chúng tôi sẽ kiểm tra những thứ được. Hãy nhớ rằng, tất cả các bản đồ dưới đây đều liên quan đến thời gian tốt nhất / xấu nhất, thời gian đặt không còn được tính đến nữa.

Nhận kết quả


Kích thước bộ sưu tập: 100. Giới hạn băm: 50. Điều này có nghĩa là mỗi mã băm phải xảy ra hai lần và mọi khóa khác dự kiến ​​sẽ xung đột trong bản đồ băm.

size_100_hlimit_50_gets

Hả ... Gì cơ?


Kích thước bộ sưu tập: 100. Giới hạn băm: 90. Một trong mười khóa có mã băm trùng lặp.

size_100_hlimit_90_gets

Whoa Nelly! Đây là tình huống có khả năng xảy ra nhất tương quan với câu hỏi của người hỏi, và rõ ràng công suất ban đầu là 100 với hệ số tải là 1 là một trong những điều tồi tệ nhất ở đây! Tôi thề rằng tôi không giả mạo điều này.


Kích thước bộ sưu tập: 100. Giới hạn băm: 100. Mỗi khóa như một mã băm duy nhất của riêng nó. Không có va chạm mong đợi.

size_100_hlimit_100_gets

Điều này trông yên bình hơn một chút. Hầu hết các kết quả giống nhau trên toàn bộ bảng.


Kích thước bộ sưu tập: 1000. Giới hạn băm: 500. Giống như trong thử nghiệm đầu tiên, có quá tải băm là 2 đến 1, nhưng bây giờ với nhiều mục nhập hơn.

size_1000_hlimit_500_gets

Có vẻ như bất kỳ cài đặt nào cũng sẽ mang lại kết quả tốt ở đây.


Kích thước tập hợp: 1000. Giới hạn băm: 900. Điều này có nghĩa là một trong mười mã băm sẽ xảy ra hai lần. Kịch bản hợp lý liên quan đến va chạm.

size_1000_hlimit_900_gets

Và cũng giống như những lần đặt cho thiết lập này, chúng tôi nhận thấy sự bất thường ở một điểm kỳ lạ.


Kích thước bộ sưu tập: 1000. Giới hạn băm: 990. Một số va chạm, nhưng chỉ một số ít. Khá thực tế về mặt này.

size_1000_hlimit_990_gets

Hiệu suất cao ở mọi nơi, tiết kiệm cho sự kết hợp của công suất ban đầu cao với hệ số tải thấp. Tôi mong đợi điều này cho các lần đặt, vì có thể mong đợi hai lần thay đổi kích thước bản đồ băm. Nhưng tại sao lại được?


Kích thước bộ sưu tập: 1000. Giới hạn băm: 1000. Không có mã băm trùng lặp, nhưng hiện có kích thước mẫu là 1000.

size_1000_hlimit_1000_gets

Một hình ảnh hoàn toàn không có hình ảnh. Điều này dường như hoạt động không có vấn đề gì.


Kích thước bộ sưu tập: 100_000. Giới hạn băm: 10_000. Đi vào 100K một lần nữa, với rất nhiều mã băm trùng lặp.

size_100000_hlimit_10000_gets

Nó trông không đẹp, mặc dù các điểm xấu được bản địa hóa rất nhiều. Hiệu suất ở đây dường như phụ thuộc phần lớn vào sự phối hợp nhất định giữa các cài đặt.


Kích thước bộ sưu tập: 100_000. Giới hạn băm: 90_000. Thực tế hơn một chút so với thử nghiệm trước, ở đây chúng tôi có quá tải 10% trong mã băm.

size_100000_hlimit_90000_gets

Nhiều phương sai, mặc dù nếu bạn liếc mắt, bạn có thể thấy một mũi tên chỉ vào góc trên bên phải.


Kích thước bộ sưu tập: 100_000. Giới hạn băm: 99_000. Kịch bản tốt, điều này. Một bộ sưu tập lớn với quá tải mã băm 1%.

size_100000_hlimit_99000_gets

Hỗn loạn lắm. Thật khó để tìm thấy nhiều cấu trúc ở đây.


Kích thước bộ sưu tập: 100_000. Giới hạn băm: 100_000. Cái lớn. Bộ sưu tập lớn nhất với hàm băm hoàn hảo.

size_100000_hlimit_100000_gets

Có ai khác nghĩ rằng cái này bắt đầu giống đồ họa Atari không? Điều này dường như ưu tiên dung lượng ban đầu bằng chính xác kích thước bộ sưu tập, -25% hoặc + 50%.


Được rồi, bây giờ là lúc kết luận ...

  • Về thời gian thực hiện: bạn sẽ muốn tránh dung lượng ban đầu thấp hơn số lượng mục nhập bản đồ dự kiến. Nếu một con số chính xác được biết trước, con số đó hoặc thứ gì đó cao hơn một chút dường như hoạt động tốt nhất. Hệ số tải cao có thể bù đắp dung lượng ban đầu thấp hơn do thay đổi kích thước bản đồ băm trước đó. Đối với công suất ban đầu cao hơn, chúng dường như không quan trọng lắm.
  • Về thời gian nhận: kết quả hơi hỗn loạn ở đây. Không có nhiều điều để kết luận. Nó dường như phụ thuộc rất nhiều vào tỷ lệ tinh tế giữa chồng chéo mã băm, dung lượng ban đầu và hệ số tải, với một số thiết lập được cho là xấu hoạt động tốt và thiết lập tốt hoạt động kém.
  • Tôi dường như đầy tào lao khi nói đến các giả định về hiệu suất Java. Sự thật là, trừ khi bạn điều chỉnh cài đặt của mình một cách hoàn hảo để triển khai HashMap, kết quả sẽ ở khắp nơi. Nếu có một điều cần gỡ bỏ điều này, đó là kích thước ban đầu mặc định là 16 hơi ngớ ngẩn đối với bất kỳ thứ gì ngoại trừ các bản đồ nhỏ nhất, vì vậy hãy sử dụng một hàm tạo đặt kích thước ban đầu nếu bạn có bất kỳ ý tưởng nào về thứ tự kích thước nó sẽ.
  • Chúng tôi đang đo bằng nano giây ở đây. Thời gian trung bình tốt nhất cho mỗi 10 lần đặt là 1179 ns và kém nhất là 5105 ns trên máy của tôi. Thời gian trung bình tốt nhất cho mỗi 10 người nhận được là 547 ns và tồi tệ nhất là 3484 ns. Đó có thể là yếu tố 6 khác biệt, nhưng chúng ta đang nói chưa đến một phần nghìn giây. Trên các bộ sưu tập lớn hơn rất nhiều so với những gì mà áp phích gốc đã nghĩ đến.

Vâng, đó là nó. Tôi hy vọng mã của tôi không có một số giám sát khủng khiếp làm mất hiệu lực của mọi thứ tôi đã đăng ở đây. Điều này thật thú vị và tôi đã học được rằng cuối cùng thì bạn cũng có thể dựa vào Java để thực hiện công việc của nó hơn là mong đợi nhiều sự khác biệt từ những tối ưu hóa nhỏ. Điều đó không có nghĩa là không nên tránh một số thứ, nhưng sau đó chúng ta chủ yếu nói về việc xây dựng các Chuỗi dài trong vòng lặp for, sử dụng các cấu trúc dữ liệu sai và đưa ra thuật toán O (n ^ 3).


1
Cảm ơn vì những nỗ lực, trông tuyệt vời! Đừng lười biếng, tôi cũng đã thêm một số đồ thị đẹp vào kết quả của mình. Các bài kiểm tra của tôi có phần thô bạo hơn của bạn, nhưng tôi thấy rằng sự khác biệt dễ nhận thấy hơn khi sử dụng bản đồ lớn hơn. Với những tấm bản đồ nhỏ, bạn làm gì cũng không thể bỏ sót. Hiệu suất có xu hướng hỗn loạn, do tối ưu hóa JVM và GC, và tôi có lý thuyết rằng bất kỳ kết luận mạnh mẽ nào đều bị sự hỗn loạn đó ăn mòn đối với một số bộ dữ liệu nhỏ hơn của bạn.
Domchi

Thêm một nhận xét về nhận được hiệu suất. Nó có vẻ hỗn loạn, nhưng tôi thấy rằng nó thay đổi rất nhiều trong một phạm vi rất hẹp, nhưng nhìn chung, nó liên tục và nhàm chán như địa ngục. Tôi đã thỉnh thoảng nhận được một đột biến kỳ lạ, chẳng hạn như bạn đã làm trên 100/90. Tôi không thể giải thích nó, nhưng trong thực tế nó có thể không được chú ý.
Domchi

G_H, hãy xem câu trả lời của tôi, tôi biết đây là một chủ đề rất cũ nhưng có thể các bài kiểm tra của bạn nên được làm lại với điều này.
durron597

Này, bạn nên đăng bài này lên ACM như một báo cáo hội nghị :) Thật là một nỗ lực!
yerlilbilgin

12

Đây là một chủ đề khá tuyệt vời, ngoại trừ có một điều quan trọng mà bạn đang thiếu. Bạn đã nói:

Thật kỳ lạ, công suất, công suất + 1, công suất + 2, công suất-1 và thậm chí công suất-10 đều mang lại kết quả chính xác như nhau. Tôi mong đợi ít nhất năng lực-1 và năng lực-10 sẽ cho kết quả tồi tệ hơn.

Mã nguồn nhảy công suất ban đầu lên công suất cao nhất tiếp theo của hai bên trong. Điều đó có nghĩa là, ví dụ, các dung lượng ban đầu 513, 600, 700, 800, 900, 1000 và 1024 đều sẽ sử dụng cùng một dung lượng ban đầu (1024). Tuy nhiên, điều này không làm mất hiệu lực của thử nghiệm được thực hiện bởi @G_H, người ta nên nhận ra rằng điều này đang được thực hiện trước khi phân tích kết quả của mình. Và nó giải thích hành vi kỳ lạ của một số thử nghiệm.

Đây là hàm tạo cho nguồn 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();
}

Điều đó thật thú vị! Tôi không có ý tưởng về điều này. Thực sự giải thích những gì tôi đã thấy trong các bài kiểm tra. Và, một lần nữa, nó xác nhận rằng tối ưu hóa sớm thường hữu ích bởi vì bạn không thực sự biết (hoặc thực sự cần biết) trình biên dịch hoặc mã có thể đang làm gì sau lưng bạn. Và tất nhiên, nó có thể thay đổi theo phiên bản / cách triển khai. Cảm ơn cho thanh toán bù trừ này lên!
G_H

@G_H Tôi rất muốn thấy các thử nghiệm của bạn chạy lại, chọn số thích hợp hơn với thông tin này. Ví dụ: nếu tôi có 1200 phần tử, tôi nên sử dụng bản đồ 1024, bản đồ 2048 hay bản đồ 4096? Tôi không biết câu trả lời cho câu hỏi ban đầu, đó là lý do tại sao tôi tìm thấy chủ đề này để bắt đầu. Mặc dù, tôi biết rằng Ổi nhân lên của bạn expectedSizebởi 1.33khi bạn làmMaps.newHashMap(int expectedSize)
durron597

Nếu HashMap không làm tròn thành giá trị lũy thừa của hai capacity, một số nhóm sẽ không bao giờ được sử dụng. Chỉ số nhóm cho nơi đặt dữ liệu bản đồ được xác định bởi bucketIndex = hashCode(key) & (capacity-1). Vì vậy, nếu capacitylà bất kỳ điều gì khác ngoài lũy thừa của hai, biểu diễn nhị phân của (capacity-1)sẽ có một số số 0 trong đó, có nghĩa là &hoạt động (nhị phân và) sẽ luôn bằng không các bit thấp hơn nhất định của Mã băm. Ví dụ: (capacity-1)is 111110(62) thay vì 111111(63). Chỉ các nhóm có chỉ số chẵn mới có thể được sử dụng trong trường hợp này.
Michael Geier

2

Chỉ cần đi với 101. Tôi không thực sự chắc chắn rằng nó cần thiết, nhưng nó không thể xứng đáng với nỗ lực để tìm hiểu chắc chắn.

... chỉ cần thêm 1.


CHỈNH SỬA: Một số biện minh cho câu trả lời của tôi.

Đầu tiên, tôi giả định rằng ý chí của bạn HashMapsẽ không phát triển vượt quá 100; nếu có, bạn nên giữ nguyên hệ số tải. Tương tự, nếu mối quan tâm của bạn là hiệu suất, hãy để nguyên hệ số tải . Nếu mối quan tâm của bạn là bộ nhớ, bạn có thể tiết kiệm một số bằng cách đặt kích thước tĩnh. Điều này thể đáng làm nếu bạn đang nhồi nhét quá nhiều thứ trong bộ nhớ; tức là, đang lưu trữ nhiều bản đồ, hoặc tạo các bản đồ có kích thước nhấn mạnh theo không gian.

Thứ hai, tôi chọn giá trị 101vì nó mang lại khả năng đọc tốt hơn ... nếu tôi xem mã của bạn sau đó và thấy rằng bạn đã đặt dung lượng ban đầu thành 100và bạn đang tải nó với 100các phần tử, tôi sẽ phải đọc qua Javadoc để đảm bảo rằng nó sẽ không thay đổi kích thước khi đạt đến chính xác 100. Tất nhiên, tôi sẽ không tìm thấy câu trả lời ở đó, vì vậy tôi sẽ phải xem xét nguồn. Điều này không đáng ... chỉ cần để nó đi 101và mọi người đều vui vẻ và không ai đang tìm kiếm mặc dù mã nguồn của java.util.HashMap. Hoan hô.

Thứ ba, tuyên bố rằng đặt HashMapcông suất chính xác của những gì bạn mong đợi với hệ số tải 1 " sẽ giết chết hiệu suất tra cứu và chèn của bạn " là không đúng, ngay cả khi nó được in đậm.

... nếu bạn có các nthùng và bạn phân chia ngẫu nhiên ncác mục vào các nthùng, vâng, bạn sẽ kết thúc với các mục trong cùng một thùng, chắc chắn ... nhưng đó không phải là ngày tận thế ... trên thực tế, nó chỉ là một vài so sánh ngang bằng hơn. Trong thực tế, có rất nhiều. sự khác biệt nhỏ khi bạn xem xét rằng giải pháp thay thế là gán ncác mục vào các n/0.75nhóm.

Không cần phải nghe lời tôi ...


Mã kiểm tra nhanh:

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

Kết quả kiểm tra:

=======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: ↑ - có về điều này → || ← sự khác biệt nhiều giữa các cài đặt khác nhau .


Đối với câu trả lời ban đầu của tôi (một chút phía trên đường ngang đầu tiên), nó đã cố tình lấp lánh vì trong hầu hết các trường hợp , loại tối ưu hóa vi mô này không tốt .


@EJP, phỏng đoán của tôi không sai. Xem các chỉnh sửa ở trên. Phỏng đoán của bạn không chính xác về việc phỏng đoán của ai là đúng và phỏng đoán của ai là không chính xác.
badroit,

(... có lẽ tôi là trở thành một nhân vật quái gở chút ... Tôi là một chút khó chịu mặc dù: P)
badroit

3
Bạn có thể thực sự khó chịu với EJP, tuy nhiên bây giờ đến lượt tôi; P - trong khi tôi đồng ý rằng tối ưu hóa sớm giống như xuất tinh sớm, xin đừng cho rằng điều gì đó thường không đáng nỗ lực thì không đáng để nỗ lực trong trường hợp của tôi . Trong trường hợp của tôi, điều quan trọng là tôi không muốn đoán, vì vậy tôi đã tra cứu - +1 là không cần thiết trong trường hợp của tôi (nhưng có thể là nơi dung lượng ban đầu / thực tế của bạn không giống nhau và loadFactor không phải là 1, xem điều này được truyền thành int trong HashMap: ngưỡng = (int) (dung lượng * loadFactor)).
Domchi

@badroit Bạn đã nói rõ ràng rằng tôi không thực sự chắc chắn rằng nó cần thiết '. Do đó đó là phỏng đoán. Bây giờ bạn đã thực hiện và đăng nghiên cứu, nó không còn là phỏng đoán nữa và vì rõ ràng bạn đã không thực hiện nó trước đó nên rõ ràng phỏng đoán, nếu không thì bạn đã chắc chắn. Đối với 'không chính xác', Javadoc yêu cầu rõ ràng hệ số tải là 0,75, cũng như nhiều thập kỷ nghiên cứu và câu trả lời của G_H. Cuối cùng là 'nó không thể xứng đáng với nỗ lực', hãy xem bình luận của Domchi ở đây. Điều đó không đúng lắm, mặc dù nói chung tôi đồng ý với bạn về tối ưu hóa vi mô.
Marquis of Lorne

Thư giãn đi mọi người. Vâng, câu trả lời của tôi đã phóng đại mọi thứ. Nếu bạn có 100 đối tượng không có một số equalschức năng quá nặng , bạn có thể bỏ chúng vào Danh sách và chỉ sử dụng `chứa '. Với một bộ nhỏ như vậy, sẽ không bao giờ có sự khác biệt lớn về hiệu suất. Nó chỉ thực sự quan trọng nếu các mối quan tâm về tốc độ hoặc bộ nhớ vượt lên trên tất cả, hoặc bằng và băm rất cụ thể. Tôi sẽ thực hiện một bài kiểm tra sau với các bộ sưu tập lớn và các hệ số tải khác nhau và dung lượng ban đầu để xem liệu tôi có đầy những thứ tào lao hay không.
G_H

2

Về mặt triển khai, Google Guava có một phương pháp xuất xưởng thuận tiện

Maps.newHashMapWithExpectedSize(expectedSize)

Những tính toán khả năng sử dụng công thức

capacity = expectedSize / 0.75F + 1.0F

1

Từ HashMapJavaDoc:

Theo nguyên tắc chung, hệ số tải mặc định (.75) mang lại sự cân bằng tốt giữa chi phí thời gian và không gian. Giá trị cao hơn làm giảm chi phí không gian nhưng làm tăng chi phí tra cứu (được phản ánh trong hầu hết các hoạt động của lớp HashMap, bao gồm get và put). Số lượng mục nhập dự kiến ​​trong bản đồ và hệ số tải của nó nên được tính đến khi thiết lập dung lượng ban đầu của nó, để giảm thiểu số lượng hoạt động rehash. Nếu dung lượng ban đầu lớn hơn số mục nhập tối đa chia cho hệ số tải, sẽ không có hoạt động rehash nào xảy ra.

Vì vậy, nếu bạn đang mong đợi 100 mục nhập, có lẽ hệ số tải 0,75 và công suất ban đầu là trần (100 / 0,75) sẽ là tốt nhất. Điều đó giảm xuống còn 134.

Tôi phải thừa nhận rằng, tôi không chắc tại sao chi phí tra cứu sẽ lớn hơn đối với hệ số tải cao hơn. Chỉ vì HashMap "đông đúc" hơn không có nghĩa là nhiều đối tượng sẽ được đặt trong cùng một thùng, đúng không? Điều đó chỉ phụ thuộc vào mã băm của họ, nếu tôi không nhầm. Vì vậy, giả sử một mã băm lan truyền tốt, không phải hầu hết các trường hợp vẫn là O (1) bất kể hệ số tải sao?

CHỈNH SỬA: Tôi nên đọc thêm trước khi đăng ... Tất nhiên mã băm không thể ánh xạ trực tiếp đến một số chỉ mục nội bộ. Nó phải được giảm xuống giá trị phù hợp với công suất hiện tại. Có nghĩa là dung lượng ban đầu của bạn càng lớn thì số lượng xung đột băm càng nhỏ. Việc chọn dung lượng ban đầu chính xác bằng kích thước (hoặc +1) của tập đối tượng của bạn với hệ số tải là 1 sẽ thực sự đảm bảo rằng bản đồ của bạn không bao giờ bị thay đổi kích thước. Tuy nhiên, nó sẽ giết chết hiệu suất tra cứu và chèn của bạn. Việc thay đổi kích thước vẫn diễn ra tương đối nhanh chóng và có thể chỉ xảy ra một lần, trong khi việc tra cứu được thực hiện trên khá nhiều công việc có liên quan với bản đồ. Do đó, tối ưu hóa để tra cứu nhanh là điều bạn thực sự muốn ở đây. Bạn có thể kết hợp điều đó với việc không bao giờ phải thay đổi kích thước bằng cách làm như JavaDoc nói: lấy dung lượng yêu cầu của bạn, chia cho hệ số tải tối ưu (ví dụ: 0,75) và sử dụng nó làm dung lượng ban đầu, với hệ số tải đó. Thêm 1 để đảm bảo bạn không làm tròn số.


1
" nó sẽ giết chết hiệu suất tra cứu và chèn của bạn ". Điều này là phóng đại quá mức / đơn giản-không chính xác.
badroit,

1
Các thử nghiệm của tôi cho thấy rằng hiệu suất tra cứu không bị ảnh hưởng bởi việc đặt hệ số tải là 1. Hiệu suất chèn thực sự được cải thiện; vì không có thay đổi kích thước, nó nhanh hơn. Vì vậy, tuyên bố của bạn đúng đối với trường hợp chung (tra cứu HashMap với số phần tử nhỏ hơn với 0,75 so với 1), nhưng không chính xác đối với trường hợp cụ thể của tôi khi HashMap luôn đầy đến dung lượng tối đa, điều này không bao giờ thay đổi. Đề xuất của bạn về việc đặt kích thước ban đầu cao hơn rất thú vị nhưng không liên quan đối với trường hợp của tôi vì bảng của tôi không phát triển, do đó hệ số tải chỉ quan trọng khi thay đổi kích thước.
Domchi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.