好了,让这件事变得轻松,我创建了一个测试应用程序,可以运行几个场景并获得结果的可视化显示。测试方法如下:
- 尝试了多种不同的收集大小:十万,十万和十万个条目。
- 使用的键是由ID唯一标识的类的实例。每个测试使用唯一的键,并以递增的整数作为ID。该
equals
方法仅使用ID,因此没有键映射会覆盖另一个ID。
- 密钥将获得一个哈希码,该哈希码由其ID的模块其余部分和某个预设数字组成。我们将该数字称为哈希限制。这使我能够控制预期的哈希冲突次数。例如,如果我们的集合大小为100,我们将拥有ID范围为0到99的键。如果哈希限制为100,则每个键将具有唯一的哈希码。如果哈希限制为50,则键0的哈希码与键50的哈希码相同,键1的哈希码与51的哈希码相同,依此类推。换句话说,每个键的哈希冲突预期次数是集合大小除以哈希值限制。
- 对于集合大小和哈希限制的每种组合,我使用使用不同设置初始化的哈希映射运行测试。这些设置是负载因子,是表示为收集设置的因子的初始容量。例如,集合大小为100且初始容量因子为1.25的测试将初始化具有初始容量125的哈希图。
- 每个键的值只是一个新值
Object
。
- 每个测试结果都封装在Result类的实例中。在所有测试结束时,结果按从最差的总体性能到最佳的顺序排序。
- 推入和获得的平均时间是按10次推入/获得计算的。
- 所有测试组合都运行一次,以消除JIT编译的影响。之后,将运行测试以获取实际结果。
这是课程:
package hashmaptest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
public class HashMapTest {
private static final List<Result> results = new ArrayList<Result>();
public static void main(String[] args) throws IOException {
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文件。下面给出了该类。如果您希望运行它,请取消注释上面代码中的相应行。警告:visualizer类假定您正在Windows上运行,并且将在C:\ temp中创建文件夹和文件。在其他平台上运行时,请进行调整。
package hashmaptest;
import hashmaptest.HashMapTest.Result;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.imageio.ImageIO;
public class ResultVisualizer {
private static final Map<Integer, Map<Integer, Set<Result>>> sampleSizeToHashLimit =
new HashMap<Integer, Map<Integer, Set<Result>>>();
private static final DecimalFormat df = new DecimalFormat("0.00");
static void visualizeResults(final List<Result> results) throws IOException {
final File tempFolder = new File("C:\\temp");
final File baseFolder = makeFolder(tempFolder, "hashmap_tests");
long bestPutTime = -1L;
long worstPutTime = 0L;
long bestGetTime = -1L;
long worstGetTime = 0L;
for(final Result result : results) {
final Integer sampleSize = result.sampleSize;
final Integer hashLimit = result.hashLimit;
final long putTime = result.averagePutTime;
final long getTime = result.averageGetTime;
if(bestPutTime == -1L || putTime < bestPutTime)
bestPutTime = putTime;
if(bestGetTime <= -1.0f || getTime < bestGetTime)
bestGetTime = getTime;
if(putTime > worstPutTime)
worstPutTime = putTime;
if(getTime > worstGetTime)
worstGetTime = getTime;
Map<Integer, Set<Result>> hashLimitToResults =
sampleSizeToHashLimit.get(sampleSize);
if(hashLimitToResults == null) {
hashLimitToResults = new HashMap<Integer, Set<Result>>();
sampleSizeToHashLimit.put(sampleSize, hashLimitToResults);
}
Set<Result> resultSet = hashLimitToResults.get(hashLimit);
if(resultSet == null) {
resultSet = new HashSet<Result>();
hashLimitToResults.put(hashLimit, resultSet);
}
resultSet.add(result);
}
System.out.println("Best average put time: " + bestPutTime + " ns");
System.out.println("Best average get time: " + bestGetTime + " ns");
System.out.println("Worst average put time: " + worstPutTime + " ns");
System.out.println("Worst average get time: " + worstGetTime + " ns");
for(final Integer sampleSize : sampleSizeToHashLimit.keySet()) {
final File sizeFolder = makeFolder(baseFolder, "sample_size_" + sampleSize);
final Map<Integer, Set<Result>> hashLimitToResults =
sampleSizeToHashLimit.get(sampleSize);
for(final Integer hashLimit : hashLimitToResults.keySet()) {
final File limitFolder = makeFolder(sizeFolder, "hash_limit_" + hashLimit);
final Set<Result> resultSet = hashLimitToResults.get(hashLimit);
final Set<Float> loadFactorSet = new HashSet<Float>();
final Set<Integer> initialCapacitySet = new HashSet<Integer>();
for(final Result result : resultSet) {
loadFactorSet.add(result.loadFactor);
initialCapacitySet.add(result.initialCapacity);
}
final List<Float> loadFactors = new ArrayList<Float>(loadFactorSet);
final List<Integer> initialCapacities = new ArrayList<Integer>(initialCapacitySet);
Collections.sort(loadFactors);
Collections.sort(initialCapacities);
final BufferedImage putImage =
renderMap(resultSet, loadFactors, initialCapacities, worstPutTime, bestPutTime, false);
final BufferedImage getImage =
renderMap(resultSet, loadFactors, initialCapacities, worstGetTime, bestGetTime, true);
final String putFileName = "size_" + sampleSize + "_hlimit_" + hashLimit + "_puts.png";
final String getFileName = "size_" + sampleSize + "_hlimit_" + hashLimit + "_gets.png";
writeImage(putImage, limitFolder, putFileName);
writeImage(getImage, limitFolder, getFileName);
}
}
}
private static File makeFolder(final File parent, final String folder) throws IOException {
final File child = new File(parent, folder);
if(!child.exists())
child.mkdir();
return child;
}
private static BufferedImage renderMap(final Set<Result> results, final List<Float> loadFactors,
final List<Integer> initialCapacities, final float worst, final float best,
final boolean get) {
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初始容量/1.0负载系数与1250初始容量/0.75负载系数处于同一水平。
集合大小:1000。哈希限制:1000。没有重复的哈希码,但现在的样本大小为1000。
这里没有太多要说的。较高的初始容量和0.75的负载系数的组合似乎略胜于1000初始容量和1的负载系数的组合。
集合大小:100_000。哈希限制:10_000。好吧,它现在变得越来越严重,每个密钥的样本大小为十万,并且有100个哈希码重复项。
kes!我认为我们发现了较低的频谱。在这里,加载大小为1的集合大小的初始容量确实做得很好,但除此之外,它遍及整个商店。
集合大小:100_000。哈希限制:90_000。比之前的测试更实际,这里我们的哈希码过载了10%。
左下角仍然是不可取的。较高的初始容量效果最佳。
集合大小:100_000。哈希限制:99_000。好方案,这个。具有1%哈希码重载的大型集合。
在这里使用精确的集合大小作为初始容量(负载因子为1)会胜出!不过,稍大的init容量工作得很好。
集合大小:100_000。哈希限制:100_000。大的那个。具有完善哈希函数的最大集合。
这里有些令人惊讶的东西。初始容量为50%的额外空间(负载系数为1)获胜。
好了,仅此而已。现在,我们将检查获取。请记住,下面的图都是相对于最佳/最差获取时间的,不再考虑放置时间。
获得结果
集合大小:100。哈希限制:50。这意味着每个哈希码应出现两次,并且每个其他键都应在哈希图中碰撞。
嗯...什么?
集合大小:100。哈希限制:90。十分之一的键具有重复的哈希码。
哇,奈利!这是最可能与质问者的问题相关的场景,显然,初始容量为100且负载系数为1是这里最糟糕的事情之一!我发誓我没有假冒。
集合大小:100。散列限制:100。每个键作为其自己的唯一散列码。预计不会发生碰撞。
这看起来更加和平。总体而言,结果大致相同。
集合大小:1000。哈希限制:500。就像在第一个测试中一样,哈希过载为2:1,但是现在有更多条目。
看起来任何设置都会在这里产生不错的结果。
集合大小:1000。哈希限制:900。这意味着十分之一的哈希码将出现两次。关于碰撞的合理场景。
就像与此设置的推杆一样,我们在一个奇怪的地方出现了异常。
集合大小:1000。哈希限制:990。有一些冲突,但只有少数。在这方面非常现实。
除具有高初始容量和低负载因数的组合外,任何地方都具有不错的性能。我希望在puts上使用它,因为可能需要调整两个哈希图的大小。但是为什么要得到呢?
集合大小: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 ns,而我的机器则是最差的5105 ns。每10次获取的最佳平均时间为547 ns,最差的3484 ns。这可能相差6倍,但我们的通话时间不到一毫秒。在比原始海报构思的要大得多的收藏中。
好,就是这样。我希望我的代码不会受到可怕的监督,从而使我在此处发布的所有内容失效。这很有趣,而且我了解到,最终,您可以依靠Java来完成其工作,而不是期望从微小的优化中获得很大的不同。这并不是说不应该避免某些事情,但是我们主要是在谈论在for循环中构造冗长的String,使用错误的数据结构并使O(n ^ 3)算法。