Подтвердить что ты не робот

Какова оптимальная емкость и коэффициент загрузки для HashMap с фиксированным размером?

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

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

EDIT: Хорошо, я отложил несколько часов и провел некоторое тестирование. Вот результаты:

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

EDIT2: добавление некоторых графиков с моей стороны. Здесь показан пример разницы между коэффициентом загрузки 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);
  }

}
4b9b3361

Ответ 1

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

  • Было опробовано несколько разных размеров сбора: сто, одна тысяча и сто тысяч записей.
  • Используемые ключи - это экземпляры класса, которые однозначно идентифицируются идентификатором. Каждый тест использует уникальные ключи с добавлением целых чисел в виде идентификаторов. Метод equals использует только идентификатор, поэтому никакое сопоставление ключей не перезаписывает другой.
  • Ключи получают хеш-код, который состоит из остатка модуля от их идентификатора к некоторому предварительно установленному номеру. Мы будем называть это число хэш-предел. Это позволило мне контролировать количество столкновений хэшей, которые ожидались. Например, если наш размер коллекции равен 100, у нас будут ключи с идентификаторами от 0 до 99. Если предел хэша 100, каждый ключ будет иметь уникальный хэш-код. Если хэш-предел равен 50, ключ 0 будет иметь тот же хэш-код, что и ключ 50, 1 будет иметь тот же хеш-код, что и 51. Другими словами, ожидаемое количество столкновений хэша на ключ - это размер коллекции, деленный на хэш предел.
  • Для каждой комбинации размера коллекции и хэш-лимита я запускаю тест с использованием хеш-карт, инициализированных различными настройками. Эти параметры являются коэффициентом загрузки и начальной мощностью, которая выражается в качестве коэффициента настройки коллекции. Например, тест с размером коллекции 100 и начальным коэффициентом емкости 1,25 инициализирует хэш-карту с начальной емкостью 125.
  • Значение для каждого ключа - это просто новый Object.
  • Каждый результат теста инкапсулируется в экземпляр класса Result. В конце всех тестов результаты упорядочены от худшей общей производительности до лучших.
  • Среднее время для puts и gets рассчитывается на 10 puts/gets.
  • Все тестовые комбинации запускаются один раз, чтобы исключить влияние компиляции 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 получает). Изображения представляют собой двумерные "карты тепла", которые показывают цвет за комбинацию начальной емкости и коэффициента загрузки.
  • Цвета в изображениях основаны на среднем времени в нормализованном масштабе от наилучшего до наихудшего результата, от насыщенного зеленого до насыщенного красного. Другими словами, лучшее время будет полностью зеленым, а худшее время будет полностью красным. Два разных измерения времени никогда не должны иметь один и тот же цвет.
  • Цветовые карты рассчитываются отдельно для puts и gets, но охватывают все тесты для соответствующих категорий.
  • Визуализации показывают начальную емкость по оси x и коэффициент нагрузки по оси y.

Без дальнейших церемоний взгляните на результаты. Я начну с результатов для puts.

Положите результаты


Размер коллекции: 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 единиц мощности /1,0 и коэффициент нагрузки 1250/0,75 нагрузки находятся на одном уровне.


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

size_1000_hlimit_1000_puts

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


Размер коллекции: 100_000. Предел хеширования: 10_000. Хорошо, теперь это становится серьезным, с размером выборки сто тысяч и 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

Используя точный размер коллекции как емкость init с коэффициентом загрузки 1 выигрывает здесь! Однако немного большие возможности инициализации работают довольно хорошо.


Размер коллекции: 100_000. Предел хэша: 100_000. Большая. Самая большая коллекция с идеальной хэш-функцией.

size_100000_hlimit_100000_puts

Некоторые удивительные вещи здесь. Начальная емкость с 50% дополнительной комнатой при коэффициенте нагрузки 1 побед.


Хорошо, что это для puts. Теперь мы проверим все. Помните, что приведенные ниже карты относятся к лучшим/худшим временам получения, время старта больше не учитывается.

Получить результаты


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

<Т411 >

Эх... Что?


Размер коллекции: 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

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


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

size_1000_hlimit_990_gets

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


Размер коллекции: 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, чтобы выполнять свою работу, а не рассчитывать на большую разницу между крошечными оптимизациями. Это не означает, что некоторые вещи не следует избегать, но тогда мы в основном говорим о построении длинных строк для циклов, использовании неправильных структур данных и создании алгоритма O (n ^ 3).

Ответ 2

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

Любопытно, что производительность, емкость + 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();
}

Ответ 3

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

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


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

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

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

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

... если у вас есть n ведра, и вы произвольно назначаете n элементы в n ведра, да, вы будете в конечном итоге с элементами в том же ведро, конечно... но это а не конец света... на практике это всего лишь пара сравнений сравнений. На самом деле, там особенно. Небольшая разница, если вы считаете, что альтернатива назначает 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; — там об этом → || & larr; большая разница между различными настройками.


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

Ответ 4

Из HashMap JavaDoc:

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

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

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

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