5

Problem:

  • I need to compare 2 hash table implementations (well basically HashMap with another one) and make a reasonable conclusion.

  • I am not interested in 100% accuracy but just being in the right direction in my estimation.

  • I am interested in the difference not only per operation but mainly on the hashtable as a "whole".

  • I don't have a strict requirement on speed so if the other implementation is reasonably slower I can accept it but I do expect/require that the memory usage be better (since one of the hashtables is backed by primitive table).

What I did so far:

Originally I created my own custom "benchmark" with loops and many calls to hint for gc to get a feeling of the difference but I am reading online that using a standard tool is more reliable/appropriate.
Example of my approach (MapInterface is just a wrapper so I can switch among implementations.):

int[] keys = new int[10000000];
String[] values = new String[10000000];  
for(int i = 0; i < keys.length; ++i) {  
   keys[i] = i;  
   values[i] = "" + i;
}

if(operation.equals("put", keys, values)) {  
   runPutOperation(map);  
}  

public static long[] runOperation(MapInterface map, Integer[] keys, String[] values) {  
    long min = Long.MAX_VALUE;  
    long max = Long.MIN_VALUE;  
    long run = 0;  
    for(int i = 0; i < 10; ++i) {  
       long start = System.currentTimeMillis();  
       for(int i = 0; i < keys.length; ++i) {          
            map.put(keys[i], values[i]);  
        }
        long total = System.currentTimeMillis() - start;  
        System.out.println(total/1000d + " seconds");    
        if(total < min) {
            min = time;
        }
        if(total > max) {
            max = time;
         }
         run += time;  
         map = null;  
         map = createNewHashMap();
         hintsToGC();    
   }  
  return new long[] {min, max, run};
 }     


public void hintsToGC() {  
    for(int i = 0; i < 20; ++i) {
            System.out.print(". ");
            System.gc();            
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {              
                e.printStackTrace();
          }           
       } 
}


private HashMapInterface<String> createNewHashMap() {  
    if(jdk) {  
        return new JDKHashMapWrapper<String>();  
    }  
    else {
        return new AlternativeHashMapWrapper<String>();   
    }  
 }  



public class JDKHashMapWrapper implements HashMapInterface<String>  {
    HashMap<Integer, String> hashMap;         
    JDKHashMapWrapper() {   
       hashMap = new HashMap<Integer, String>();  
    }  
    public String put(Integer key, String value)  {
       return hashMap.put(key, value);  
    }  
 //etc  
}

(I want to test put, get, contains and the memory utilization)
Can I be sure by using my approach that I can get reasonable measurements?
If not what would be the most appropriate tool to use and how?

Update:
- I also test with random numbers (also ~10M random numbers) using SecureRandom.
- When the hash table resizes I print the logical size of the hash table/size of the actual table to get the load factor

Update:
For my specific case, where I am interested also in integers what can of pitfalls are there with my approach?

UPDATE after @dimo414 comments:

Well at a minimum the hashtable as a "whole" isn't meaningful

I mean how the hashtable behaves under various loads both at runtime and in memory consumption.

Every data structure is a tradeoff of different methods

I agree. My trade-off is an acceptable access penalty for memory improvement

You need to identify what features you're interested in verifying

1) put(key, value);
2) get(key, value);
3) containsKey(key);
4) all the above when having many entries in the hash table

Cratylus
  • 52,998
  • 69
  • 209
  • 339
  • One thing you could do would be to use System.nanoTime() rather than System.currentTimeMillis(). It is better suited for this type of benchmarking. – bhspencer Jul 23 '15 at 19:46
  • 2
    I trust you've seen http://stackoverflow.com/q/504103/113632 ? – dimo414 Jul 23 '15 at 19:55
  • @dimo414:I have. 1) It recommends some extra JVM options to use so I guess my approach with that JVM options could be combined to have more confidence 2) I have checked the frameworks in the last rule. `Bill and Paul's etc` does pretty much the same as what I do. Caliper is for me who is a first time user and not very experienced in benchmarking a black-box with not really helpful documentation and gives apparently gives a micro benching per operation. I have no idea how the hash table would be tested. JHM to be honest I need to read up if it can help me or not – Cratylus Jul 23 '15 at 20:31
  • I'm not sure what you mean by your second comment - benchmarks don't need to be fast, they need to be accurate. It is *very* difficult to create accurate benchmarks. – dimo414 Jul 23 '15 at 20:51
  • @dimo414:Ok but what I am trying to find out is what kind of wrong conclusion could I end up with my "simplistic" brute approach – Cratylus Jul 23 '15 at 20:59
  • Well, if your benchmark is inaccurate, you'll have meaningless numbers. You could come to any number of wrong conclusions by looking at meaningless numbers. Without more context and specifics I can't really answer your question better than that. – dimo414 Jul 23 '15 at 22:06
  • @dimo414:What more info/context is needed? I'll update the OP accordingly? – Cratylus Jul 23 '15 at 22:08
  • @dimo414:If you think that more info are needed I can update the OP accordingly – Cratylus Jul 24 '15 at 18:08
  • Well at a minimum *the hashtable as a "whole"* isn't meaningful. Every data structure is a tradeoff of different methods (e.g. hash structures have fast lookup, but more memory, linked lists have fast insertion but slow seek, etc.). You need to identify what features you're interested in verifying. Then you need to write tests which isolate that behavior. How you isolate it depends on what you're testing, but the linked thread covers many common issues. – dimo414 Jul 24 '15 at 18:49
  • @dimo414:Please see latest update. Please let me know how can I improve the post. – Cratylus Jul 29 '15 at 19:29

3 Answers3

1

Some key consideration for using Hash tables is the size of the "buckets" allocation, the collision resolution strategy, and the shape of your data. Essentially, a Hash table takes the key supplied by the application and then hashes it to a value less than or equal to the number of allocated buckets. When two key values hash to the same bucket, the implementation has to resolve the collision and return the right value. For example, one could have a sorted linked list for each bucket and that list is searched.

If your data happens to have a lot of collisions, then your performance will suffer, because the Hash table implementation will spend too much time resolving the collision. On the other hand, if you have a very large number of buckets, you solve the collision problem at the expense of memory. Also, Java's built-in HashMap implementation will "rehash" if the number of entries gets larger than a certain amount - I imagine this is an expensive operation that is worth avoiding.

Since your key data is the positive integers from 1 to 10M, your test data looks good. I would also ensure that the different hash tables implementations were initialized to the same bucket size for a given test, otherwise it's not a fair comparison. Finally, I would vary the bucket size over a pretty significant range and rerun the tests to see how the implementations changed their behavior.

schtever
  • 3,210
  • 16
  • 25
  • Valid points. Perhaps I should update the OP 1) I also test with random numbers (also ~10M random numbers) using SecureRandom. 2) When the hash table resizes I print the logical size of the hash table/size of the actual table to get the load factor – Cratylus Jul 23 '15 at 20:50
  • @Cratylus Will the application be using Integers as the key for the HashMap? – schtever Jul 23 '15 at 20:55
  • Then your test dataset makes much more sense. Can any integer be a key, or are the Integers restricted to some range? For example, US Social Security Numbers are all 9 digit integers. – schtever Jul 23 '15 at 21:02
  • In reality for my application my integers are restricted to positive integers up to ~10M. But I am also testing random numbers (i.e. negative also) because I am not clear on what pitfalls can my test have and might be misleaded – Cratylus Jul 23 '15 at 21:05
  • @Cratylus Then your test dataset is perfect. One final question: is `remove()` performance a consideration? – schtever Jul 23 '15 at 21:13
  • Not really up till now but I am testing that also for completeness. For that I remove X% of the table – Cratylus Jul 23 '15 at 21:16
  • Just loop over, grab a random number and if < 0.3 then I remove, to simulate the removal e.g. of 30% – Cratylus Jul 23 '15 at 21:18
  • @Cratylus Updated my answer based on your clarifications. – schtever Jul 23 '15 at 21:28
1

As I understand you are interested in both operations execution time and memory consumption of the maps in the test.

I will start with memory consumption as this seams not to be answered at all. What I propose is to use a small library called Classmexer. I personally used it when I need to get the 100% correct memory consumption of any object. It has the java agent approach (because it's using the Instrumentation API), which means that you need to add it as the parameter to the JVM executing your tests:

-javaagent: [PATH_TO]/classmexer.jar

The usage of the Classmexer is very simple. At any point of time you can get the memory consumption in bytes by executing:

MemoryUtil.deepMemoryUsageOf(mapIamInterestedIn, VisibilityFilter.ALL)

Note that with visibility filter you can specify if the memory calculation should be done for the object (our map) plus all other reachable object through references. That's what VisibilityFilter.ALL is for. However, this would mean that the size you get back includes all objects you used for keys and values. Thus if you have 100 Integer/String entries the reported size will include those as well.

For the timing aspect I would propose JMH tool, as this tool is made for micro bench-marking. There are plenty examples online, for example this article has map testing examples that can guide you pretty good.

Note that I should be careful when do you call the Classmexer's Memory Util as it will interfere with the time results if you call it during the time measuring. Furthermore, I am sure that there are many other tools similar to Classmexer, but I like it because it small and simple.

Ivan Senic
  • 539
  • 8
  • 13
0

I was just doing something similar to this, and I ended up using the built in profiler in the Netbeans IDE. You can get really detailed info on both CPU and memory usage. I had originally written all my code in Eclipse, but Netbeans has an import feature for bringing in Eclipse projects and it set it all up no problem, if that is possibly your situation too.

For timing, you might also look at the StopWatch class in Apache Commons. It's a much more intuitive way of tracking time on targeted operations, e.g.:

StopWatch myMapTimer = new StopWatch();
HashMap<Integer, Integer> hashMap = new HashMap<>();

myMapTimer.start();
for (int i = 0; i < numElements; i++)
    hashMap.put(i, i);
myMapTimer.stop();

System.out.println(myMapTimer.getTime()); // time will be in milliseconds
adamconkey
  • 4,104
  • 5
  • 32
  • 61
  • Is there any other benefit except cleaner code using StopWatch? – Cratylus Jul 23 '15 at 20:41
  • Not that I'm aware of, but I generally like to use an established API, cuts down on silly mistakes. There are other StopWatch classes too, in Guava and the Spring Framework. – adamconkey Jul 24 '15 at 13:51