Добре, щоб заспокоїти цю справу, я створив тестовий додаток для запуску декількох сценаріїв та отримання візуалізації результатів. Ось як проводяться тести:
- Випробувано низку різних розмірів колекції: сто, тисяча і сто тисяч записів.
- Використовувані ключі - це екземпляри класу, які однозначно ідентифікуються ідентифікатором. У кожному тесті використовуються унікальні ключі із збільшенням цілих чисел як ідентифікаторів.
equals
Метод використовує тільки ідентифікатор, тому ні одна клавіша відображення об'єкт не перезаписує інший.
- Клавіші отримують хеш-код, який складається із залишку модуля від їх ідентифікатора проти деякого попередньо встановленого номера. Ми назвемо це число лімітом хешування . Це дозволило мені контролювати кількість хеш-зіткнень, яку можна було очікувати. Наприклад, якщо розмір нашої колекції становить 100, ми матимемо ключі з ідентифікаторами від 0 до 99. Якщо обмеження хешу - 100, кожен ключ матиме унікальний хеш-код. Якщо обмеження хешу дорівнює 50, ключ 0 матиме той самий хеш-код, що і ключ 50, 1 матиме той самий хеш-код, що і 51 і т.д. Іншими словами, очікувана кількість зіткнень хешу на ключ - це розмір колекції, поділений на хеш обмеження.
- Для кожної комбінації розміру колекції та обмеження хешу я провів тест, використовуючи хеш-карти, ініціалізовані з різними налаштуваннями. Ці налаштування є коефіцієнтом навантаження та початковою ємністю, яка виражається як коефіцієнт налаштування збору. Наприклад, тест із розміром колекції 100 та початковим коефіцієнтом ємності 1,25 ініціалізує хеш-карту з початковою ємністю 125.
- Значення кожного ключа просто нове
Object
.
- Кожен результат тесту інкапсульований у екземпляр класу Result. В кінці всіх тестів результати впорядковуються від найгірших загальних показників до найкращих.
- Середній час путів та отримання розраховується на 10 путів / отримує.
- Усі тестові комбінації запускаються один раз, щоб усунути вплив компіляції JIT. Після цього проводяться тести на реальні результати.
Ось клас:
package hashmaptest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
public class HashMapTest {
private static final List<Result> results = new ArrayList<Result>();
public static void main(String[] args) throws IOException {
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};
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();
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();
}
}
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 + "%");
final List<Key> keys = generateSamples(hashLimit, sampleSize);
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);
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) {
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) {
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. Це означає, що кожен хеш-код повинен зустрічатися двічі, а кожен інший ключ стикається на хеш-карті.
Ну, це починається не дуже добре. Ми бачимо, що є велика гаряча точка для початкової ємності на 25% вище розміру колекції, з коефіцієнтом завантаження 1. Нижній лівий кут працює не дуже добре.
Розмір колекції: 100. Обмеження хешу: 90. Кожен десятий ключ має повторюваний хеш-код.
Це трохи більш реалістичний сценарій, не маючи ідеальної хеш-функції, але все одно перевантаження 10%. Точки доступу немає, але поєднання низької початкової ємності з низьким коефіцієнтом навантаження, очевидно, не працює.
Розмір колекції: 100. Обмеження хешу: 100. Кожен ключ як власний унікальний хеш-код. Якщо достатньо сегментів, не очікується зіткнень.
Початкова потужність 100 з коефіцієнтом навантаження 1 здається чудовою. Дивно, але більша початкова потужність з меншим коефіцієнтом навантаження не обов'язково хороша.
Розмір колекції: 1000. Обмеження хешу: 500. Тут стає все серйозніше, з 1000 записів. Як і в першому тесті, є перевантаження хешу від 2 до 1.
У нижньому лівому куті все ще не все добре. Але, схоже, існує симетрія між комбінацією нижчого початкового рахунку / високого коефіцієнта навантаження та вищого початкового числа / низького коефіцієнта навантаження.
Розмір колекції: 1000. Обмеження хешу: 900. Це означає, що кожен десятий хеш-код повторюється двічі. Розумний сценарій щодо зіткнень.
Щось дуже смішне відбувається з малоймовірним комбінованим початковим потенціалом, який є занадто низьким з коефіцієнтом навантаження вище 1, що є досить інтуїтивним. В іншому випадку все-таки досить симетрично.
Розмір колекції: 1000. Обмеження хешу: 990. Деякі зіткнення, але лише деякі. Цілком реалістично в цьому відношенні.
У нас тут приємна симетрія. Нижній лівий кут все ще є неоптимальним, але комбіновані 1000 init ємність / 1,0 коефіцієнт навантаження проти 1250 init ємність / 0,75 коефіцієнт навантаження на тому ж рівні.
Розмір колекції: 1000. Обмеження хешу: 1000. Немає повторюваних хеш-кодів, але тепер із розміром вибірки 1000.
Тут можна сказати не так багато. Поєднання більш високої початкової потужності з коефіцієнтом навантаження 0,75, здається, трохи перевершує комбінацію 1000 початкової потужності з коефіцієнтом навантаження 1.
Розмір колекції: 100_000. Обмеження хешу: 10_000. Добре, зараз це стає серйозним, з розміром вибірки сто тисяч і 100 дублікатів хеш-коду на ключ.
Так! Я думаю, ми знайшли наш нижчий спектр. Ініційна ємність з точністю до колекції з коефіцієнтом навантаження 1 тут справляється дуже добре, але крім того, що це по всьому магазину.
Розмір колекції: 100_000. Обмеження хешу: 90_000. Трохи реалістичніше, ніж у попередньому тесті, тут ми маємо 10% перевантаження хеш-кодів.
Нижній лівий кут все ще небажаний. Найвищі початкові можливості працюють найкраще.
Розмір колекції: 100_000. Обмеження хешу: 99_000. Хороший сценарій, це. Велика колекція з перевантаженням хеш-коду на 1%.
Тут виграє точний розмір колекції як потужність ініціативи з коефіцієнтом завантаження 1! Однак трохи більші потужності ініціалізації працюють досить добре.
Розмір колекції: 100_000. Обмеження хешу: 100_000. Великий. Найбільша колекція з досконалою хеш-функцією.
Тут є щось дивовижне. Виграє початкова ємність із додатковим приміщенням на 50% при коефіцієнті завантаження 1.
Добре, це все для путів. Зараз ми перевіримо отримання. Пам'ятайте, що наведені нижче карти відносяться до найкращого / найгіршого часу отримання, встановлений час більше не враховується.
Отримати результати
Розмір колекції: 100. Обмеження хешу: 50. Це означає, що кожен хеш-код повинен зустрічатися двічі, і кожен другий ключ повинен був зіткнутися на хеш-карті.
Е ... Що?
Розмір колекції: 100. Обмеження хешу: 90. Кожен десятий ключ має повторюваний хеш-код.
Ой, Неллі! Це найбільш вірогідний сценарій, який може співвідноситися з питанням запитувача, і, мабуть, початкова потужність 100 з коефіцієнтом навантаження 1 - одна з найгірших речей тут! Клянусь, я не підробив цього.
Розмір колекції: 100. Обмеження хешу: 100. Кожен ключ як власний унікальний хеш-код. Жодних зіткнень не очікується.
Це виглядає дещо спокійніше. Переважно однакові результати.
Розмір колекції: 1000. Обмеження хешу: 500. Так само, як і в першому тесті, є перевантаження хешем від 2 до 1, але тепер із набагато більше записів.
Схоже, будь-яке налаштування дасть тут гідний результат.
Розмір колекції: 1000. Обмеження хешу: 900. Це означає, що кожен десятий хеш-код повторюється двічі. Розумний сценарій щодо зіткнень.
І так само, як і з путами для цієї установки, ми отримуємо аномалію в дивному місці.
Розмір колекції: 1000. Обмеження хешу: 990. Деякі зіткнення, але лише деякі. Цілком реалістично в цьому відношенні.
Гідна продуктивність скрізь, за винятком поєднання високої початкової потужності з низьким коефіцієнтом навантаження. Я би очікував цього для путів, оскільки можна очікувати дві зміни розміру хеш-карти. Але чому на отримує?
Розмір колекції: 1000. Обмеження хешу: 1000. Немає повторюваних хеш-кодів, але тепер із розміром вибірки 1000.
Повністю не вражаюча візуалізація. Здається, це працює незалежно від того, що.
Розмір колекції: 100_000. Обмеження хешу: 10_000. Знову переходимо до 100K з великою кількістю хеш-кодів, що перекриваються.
Це не виглядає красиво, хоча погані місця дуже локалізовані. Ефективність тут, здається, багато в чому залежить від певної синергії між налаштуваннями.
Розмір колекції: 100_000. Обмеження хешу: 90_000. Трохи реалістичніше, ніж у попередньому тесті, тут ми маємо 10% перевантаження хеш-кодів.
Значна дисперсія, хоча, якщо ви примружили очі, ви побачите стрілку, спрямовану в правий верхній кут.
Розмір колекції: 100_000. Обмеження хешу: 99_000. Хороший сценарій, це. Велика колекція з перевантаженням хеш-коду на 1%.
Дуже хаотично. Тут важко знайти велику структуру.
Розмір колекції: 100_000. Обмеження хешу: 100_000. Великий. Найбільша колекція з досконалою хеш-функцією.
Хтось ще думає, що це починає виглядати як графіка Atari? Здається, це сприяє початковій потужності саме розміру колекції, -25% або + 50%.
Гаразд, зараз час робити висновки ...
- Щодо часу накладання: ви хочете уникнути початкової ємності, яка нижча за очікувану кількість записів на карті. Якщо точне число відомо заздалегідь, це число або щось трохи вище, здається, працює найкраще. Високі коефіцієнти навантаження можуть компенсувати нижчі початкові потужності завдяки попереднім змінам розміру хеш-карти. Для більш високих початкових можливостей вони, здається, не так важливі.
- Щодо часу отримання: результати тут трохи хаотичні. Існує не так багато висновків. Здається, він дуже покладається на тонкі співвідношення між перекриттям хеш-коду, початковою ємністю та коефіцієнтом навантаження, причому деякі нібито погані установки добре працюють, а хороші - жахливо.
- Я, мабуть, сповнений глупоти, коли справа доходить до припущень про продуктивність Java. Правда в тому, що якщо ви не ідеально налаштуєте свої налаштування на реалізацію
HashMap
, результати будуть скрізь. Якщо у цього є щось одне, це те, що початковий розмір за замовчуванням 16 трохи німий для будь-чого, крім найменших карт, тому використовуйте конструктор, який встановлює початковий розмір, якщо у вас є уявлення про порядок розміру це буде.
- Тут ми вимірюємо в наносекундах. Найкращий середній час на 10 путів - 1179 нс, а найгірший - 5105 нс на моїй машині. Найкращий середній час за 10 прийомів був 547 нс, а найгірший 3484 нс. Це може бути фактором 6, але ми говоримо менше мілісекунди. На колекціях, які значно більші за те, що мав на увазі оригінальний плакат.
Ну ось і все. Сподіваюся, мій код не має жахливого недогляду, який робить недійсним усе, що я тут розмістив. Це було весело, і я дізнався, що врешті-решт ви можете так само покладатися на Java, щоб робити свою роботу, ніж очікувати великої різниці від крихітних оптимізацій. Це не означає, що деяких речей не слід уникати, але тоді ми в основному говоримо про побудову довгих рядків для циклів for, використання неправильних структур даних та створення алгоритмів O (n ^ 3).