Какова оптимальная емкость и коэффициент загрузки для HashMap фиксированного размера? - PullRequest
76 голосов
/ 19 августа 2011

Я пытаюсь выяснить оптимальную емкость и коэффициент загрузки для конкретного случая. Я думаю, что понял суть, но я все равно был бы благодарен за подтверждение от кого-то более знающего, чем я. :)

Если я знаю, что мой HashMap заполнится, скажем, 100 объектами и будет проводить большую часть времени, имея 100 объектов, я предполагаю, что оптимальными значениями являются начальная емкость 100 и коэффициент загрузки 1? Или мне нужна емкость 101, или есть еще какие-то ошибки?

РЕДАКТИРОВАТЬ: ОК, я отложил несколько часов и провел некоторое тестирование. Вот результаты:

  • Любопытно, что емкость, емкость + 1, емкость + 2, емкость-1 и даже емкость-10 - все дают одинаковые результаты. Я ожидаю, что по крайней мере емкость-1 и емкость-10 приведут к худшим результатам.
  • Использование начальной емкости (в отличие от использования значения по умолчанию 16) дает заметное улучшение пут () - на 30% быстрее.
  • Использование коэффициента загрузки 1 дает равную производительность для небольшого числа объектов и лучшую производительность для большего числа объектов (> 100000). Однако это не улучшается пропорционально количеству объектов; Я подозреваю, что есть дополнительный фактор, который влияет на результаты.
  • Производительность get () немного отличается для разного количества объектов / емкости, но, хотя она может незначительно отличаться от случая к случаю, как правило, на нее не влияют начальная емкость или коэффициент загрузки.

РЕДАКТИРОВАТЬ 2: Добавление некоторых диаграмм с моей стороны, а также. Вот пример, иллюстрирующий разницу между коэффициентом загрузки 0,75 и 1 в тех случаях, когда я инициализирую HashMap и заполняю его до полной емкости. По шкале y - время в мс (чем меньше, тем лучше), а по шкале x - размер (количество объектов). Поскольку размер изменяется линейно, необходимое время также увеличивается линейно.

Итак, посмотрим, что я получил. Следующие две диаграммы показывают разницу в коэффициентах нагрузки. Первая диаграмма показывает, что происходит, когда HashMap заполнен до предела; коэффициент нагрузки 0,75 работает хуже из-за изменения размеров. Однако, это не всегда хуже, и есть все виды ударов и прыжков - я думаю, что GC играет главную роль в этом. Коэффициент загрузки 1.25 работает так же, как 1, поэтому он не включен в диаграмму.

fully filled

Этот график доказывает, что 0,75 было хуже из-за изменения размера; если заполнить HashMap до половины емкости, 0,75 не хуже, а просто ... отличается (и он должен использовать меньше памяти и иметь заметно более высокую производительность итерации).

half full

Еще одна вещь, которую я хочу показать. Это производительность для всех трех факторов нагрузки и разных размеров HashMap. Постоянно постоянный с небольшим разбросом, за исключением одного скачка для коэффициента нагрузки 1. Я бы очень хотел знать, что это такое (вероятно, GC, но кто знает).

go spike

А вот код для интересующихся:

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

}

Ответы [ 4 ]

69 голосов
/ 23 августа 2011

Хорошо, чтобы положить конец этому, я создал тестовое приложение для запуска нескольких сценариев и получения некоторых визуализаций результатов.Вот как выполняются тесты:

  • Было опробовано несколько разных размеров коллекции: сто, одна тысяча и сто тысяч записей.
  • Используемые ключи являются экземплярамикласс, который уникально идентифицируется идентификатором.Каждый тест использует уникальные ключи с инкрементными целыми числами в качестве идентификаторов.Метод equals использует только идентификатор, поэтому никакое сопоставление клавиш не перезаписывает другое.
  • Ключи получают хеш-код, который состоит из остатка модуля от их идентификатора и некоторого предустановленного номера.Мы назовем этот номер пределом хеша .Это позволило мне контролировать количество ожидаемых коллизий хешей.Например, если размер нашей коллекции равен 100, у нас будут ключи с идентификаторами в диапазоне от 0 до 99. Если предел хеш-функции равен 100, у каждого ключа будет уникальный хэш-код.Если предел хеширования равен 50, ключ 0 будет иметь тот же хеш-код, что и ключ 50, 1 будет иметь тот же хеш-код, что и 51 и т. Д. Другими словами, ожидаемое количество коллизий хеш-функции на ключ - это размер коллекции, деленный на хешlimit.
  • Для каждой комбинации размера коллекции и ограничения хеша я запустил тест, используя хеш-карты, инициализированные с различными настройками.Эти настройки представляют собой коэффициент загрузки и начальную емкость, которая выражается как коэффициент настройки сбора.Например, тест с размером коллекции 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.Класс для этого приведен ниже.Если вы хотите запустить его, раскомментируйте соответствующую строку в коде выше.Будьте осторожны: класс визуализатора предполагает, что вы работаете в 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. У каждого десятого ключа есть дублирующий хеш-код.

size_100_hlimit_90_puts

Это немного более реалистичный сценарий, не имеющий идеальной хэш-функции, но перегрузка на 10%.Горячая точка исчезла, но сочетание низкой начальной емкости с низким коэффициентом загрузки, очевидно, не работает.


Размер коллекции: 100. Лимит хеширования: 100. Каждый ключ имеет свой уникальный хэшкод.При наличии достаточного количества контейнеров столкновений не ожидается.

size_100_hlimit_100_puts

Начальная вместимость 100 с коэффициентом нагрузки 1 кажется хорошей.Удивительно, но более высокая начальная емкость с более низким коэффициентом загрузки не обязательно является хорошей.


Размер коллекции: 1000. Лимит хеширования: 500. Здесь все становится более серьезным, с 1000 записей.Как и в первом тесте, перегрузка хеша составляет от 2 до 1.

size_1000_hlimit_500_puts

В левом нижнем углу все еще плохо.Но, похоже, существует симметрия между комбинацией более низкого начального счета / высокого коэффициента загрузки и более высокого начального счета / низкого коэффициента загрузки.


Размер коллекции: 1000. Предел хэша: 900. Это означает, что один вдесять хеш-кодов появятся дважды.Разумный сценарий в отношении столкновений.

size_1000_hlimit_900_puts

Что-то очень забавное происходит с маловероятным комбинированием начальной емкости, которое слишком мало с коэффициентом загрузки выше 1, что довольно нелогично,В остальном все равно довольно симметрично.


Размер коллекции: 1000. Предел хеша: 990. Некоторые коллизии, но только несколько.Весьма реалистично в этом отношении.

size_1000_hlimit_990_puts

У нас здесь хорошая симметрия.Нижний левый угол все еще неоптимальный, но комбинационная емкость 1000 init / коэффициент загрузки 1.0 против 1250 init init / коэффициент нагрузки 0,75 находятся на том же уровне.


Размер сбора: 1000. Предел хеша:1000. Никаких дублирующих хеш-кодов, но теперь с размером выборки 1000.

size_1000_hlimit_1000_puts

Здесь особо нечего сказать.Комбинация более высокой начальной емкости с коэффициентом загрузки 0,75, кажется, немного превосходит комбинацию из 1000 начальной емкости с коэффициентом нагрузки 1.


Размер сбора: 100_000.Лимит хеша: 10_000.Хорошо, сейчас все серьезно, размер выборки составляет сто тысяч и 100 дубликатов хеш-кода на ключ.

size_100000_hlimit_10000_puts

Yikes!Я думаю, что мы нашли наш более низкий спектр.Емкость инициализации, точно равная размеру коллекции с коэффициентом загрузки 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

Eh... Что?


Размер коллекции: 100. Предел хеш-функции: 90. У каждого десятого ключа есть повторяющийся хэш-код.

size_100_hlimit_90_gets

ОооНелли!Это наиболее вероятный сценарий для корреляции с вопросом Аскера, и, по-видимому, начальная емкость 100 с коэффициентом загрузки 1 является одной из худших вещей здесь!Клянусь, я не притворялся.


Размер коллекции: 100. Предел хеш-функции: 100. Каждый ключ имеет свой уникальный хэш-код.Никаких столкновений не ожидается.

size_100_hlimit_100_gets

Это выглядит немного спокойнее.Практически одинаковые результаты по всем направлениям.


Размер коллекции: 1000. Предел хеш-функции: 500. Как и в первом тесте, есть перегрузка хэш-функции от 2 до 1, но теперь с гораздо большим количеством записей.

size_1000_hlimit_500_gets

Похоже, что любой параметр даст здесь достойный результат.


Размер коллекции: 1000. Предел хеша: 900. Это означает, что один вдесять хеш-кодов появятся дважды.Разумный сценарий в отношении столкновений.

size_1000_hlimit_900_gets

И так же, как и с путами для этой установки, мы получаем аномалию в странном месте.


Размер коллекции: 1000. Лимит хеширования: 990. Некоторые коллизии, но только несколько.Весьма реалистично в этом отношении.

size_1000_hlimit_990_gets

Достойная производительность везде, за исключением комбинации высокой начальной емкости с низким коэффициентом нагрузки.Я ожидаю, что это будет для путов, поскольку можно ожидать изменения размеров двух хэш-карт.Но почему на получение?


Размер коллекции: 1000. Предел хеш-функции: 1000. Нет повторяющихся хеш-кодов, но теперь с размером выборки 1000.

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 нс, а худшее 5105 нс на моей машине.Лучшее среднее время на 10 ударов составило 547 нс, а худшее - 3484 нс.Это может быть разница в 6 раз, но мы говорим меньше миллисекунды.О коллекциях, которые намного больше, чем предполагалось в оригинальном плакате.

Ну, вот и все.Я надеюсь, что мой код не имеет какой-то ужасной оплошности, которая лишает законной силы все, что я разместил здесь.Это было весело, и я узнал, что в конце концов вы можете положиться на Java, чтобы выполнять свою работу, чем ожидать большой разницы от крошечных оптимизаций.Это не означает, что некоторых вещей не следует избегать, но мы в основном говорим о построении длинных строк в циклах for, использовании неправильных структур данных и создании алгоритмов O (n ^ 3).

10 голосов
/ 27 июня 2013

Это очень хорошая тема, за исключением одной важной вещи, которую вы упускаете.Вы сказали:

Любопытно, что емкость, емкость + 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();
}
2 голосов
/ 19 августа 2011

Просто перейдите с 101. На самом деле я не уверен, что это необходимо, но это не может стоить усилий, чтобы когда-нибудь узнать наверняка.

... просто добавьте 1.


РЕДАКТИРОВАТЬ: Некоторые оправдания для моего ответа.

Во-первых, я предполагаю, что ваш HashMap не вырастет за пределы 100; Если это так, вы должны оставить коэффициент загрузки таким, какой он есть. Аналогичным образом, если ваша проблема связана с производительностью, оставьте коэффициент загрузки таким, как . Если вас беспокоит память, вы можете сохранить некоторые, установив статический размер. Это может может стоить сделать, если вы запихиваете много вещей в память; т. е. хранит много карт или создает карты размером с кучу-пространство.

Во-вторых, я выбираю значение 101, поскольку оно обеспечивает лучшую читаемость ... если я потом посмотрю на ваш код и увижу, что вы установили начальную емкость на 100 и загружаете его с помощью 100 элементов, мне нужно прочитать Javadoc, чтобы убедиться, что он не изменится, когда достигнет точно 100. Конечно, я не найду там ответа, поэтому мне придется посмотреть на источник. Это того не стоит ... просто оставьте это 101 и все довольны, и никто не смотрит на исходный код java.util.HashMap. Hoorah.

В-третьих, утверждение о том, что установка 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: & uarr; & Mdash; есть об этом & rarr; || & larr; большая разница между различными настройками .


Что касается моего первоначального ответа (чуть выше первой горизонтальной линии), он был намеренно бойким, потому что в большинстве случаев , этот тип микрооптимизации не подходит .

1 голос
/ 19 августа 2011

Из HashMap JavaDoc:

Как правило, коэффициент загрузки по умолчанию (0,75) обеспечивает хороший компромисс между временными и пространственными затратами. Более высокие значения уменьшают затраты пространства, но увеличивают стоимость поиска (отражается в большинстве операций класса HashMap, включая get и put). Ожидаемое количество записей на карте и коэффициент загрузки должны учитываться при настройке начальной емкости, чтобы минимизировать количество операций перефразировки. Если начальная емкость превышает максимальное количество записей, деленное на коэффициент загрузки, операции перефразирования никогда не будут выполняться.

Так что, если вы ожидаете 100 записей, лучше всего подойдет коэффициент загрузки 0,75 и начальная емкость потолка (100 / 0,75). Это сводится к 134.

Должен признать, я не уверен, почему стоимость поиска будет выше при более высоком коэффициенте загрузки. Тот факт, что HashMap более «переполнен», не означает, что больше объектов будет помещено в одну корзину, верно? Это зависит только от их хэш-кода, если я не ошибаюсь. Таким образом, предполагая, что приличное распространение хеш-кода не должно быть в большинстве случаев все равно O (1) независимо от коэффициента загрузки?

РЕДАКТИРОВАТЬ: я должен прочитать больше, прежде чем публиковать ... Конечно, хэш-код не может напрямую отображаться в некоторый внутренний индекс. Он должен быть уменьшен до значения, которое соответствует текущей емкости. Это означает, что чем больше ваша начальная емкость, тем меньше вы можете ожидать количество коллизий хешей. Выбор начальной емкости в точности равным размеру (или +1) вашего объекта, установленного с коэффициентом загрузки 1, действительно гарантирует, что ваша карта никогда не изменится. Однако убьет производительность поиска и вставки . Изменение размера все еще относительно быстро и может произойти, может быть, только один раз, в то время как поиск выполняется практически при любой соответствующей работе с картой. В результате, оптимизация для быстрого поиска - вот что вам действительно нужно. Вы можете комбинировать это с отсутствием необходимости изменять размеры, выполняя, как говорит JavaDoc: взять требуемую емкость, разделить ее на оптимальный коэффициент загрузки (например, 0,75) и использовать ее в качестве начальной емкости с этим коэффициентом загрузки. Добавьте 1, чтобы убедиться, что округление не поможет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...