• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.collect;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.collect.CollectPreconditions.checkNonnegative;
21 
22 import com.google.caliper.BeforeExperiment;
23 import com.google.caliper.Benchmark;
24 import com.google.caliper.Param;
25 import com.google.common.annotations.VisibleForTesting;
26 import com.google.common.primitives.Ints;
27 import com.google.common.util.concurrent.ThreadFactoryBuilder;
28 
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Random;
33 import java.util.Set;
34 import java.util.concurrent.Callable;
35 import java.util.concurrent.ConcurrentHashMap;
36 import java.util.concurrent.ConcurrentMap;
37 import java.util.concurrent.ExecutionException;
38 import java.util.concurrent.ExecutorService;
39 import java.util.concurrent.Executors;
40 import java.util.concurrent.Future;
41 
42 import javax.annotation.Nullable;
43 
44 /**
45  * Benchmarks for {@link ConcurrentHashMultiset}.
46  *
47  * @author mike nonemacher
48  */
49 public class ConcurrentHashMultisetBenchmark {
50   @Param({"1", "2", "4", "8"}) int threads;
51   @Param({"3", "30", "300"}) int size;
52   @Param MultisetSupplier implSupplier;
53 
54   private Multiset<Integer> multiset;
55   private ImmutableList<Integer> keys;
56   private ExecutorService threadPool;
57 
setUp()58   @BeforeExperiment void setUp() throws Exception {
59     multiset = implSupplier.get();
60     ImmutableList.Builder<Integer> builder = ImmutableList.builder();
61     for (int i = 0; i < size; i++) {
62       builder.add(i);
63     }
64     keys = builder.build();
65     threadPool =
66         Executors.newFixedThreadPool(threads, new ThreadFactoryBuilder().setDaemon(true).build());
67   }
68 
add(final int reps)69   @Benchmark long add(final int reps) throws ExecutionException, InterruptedException {
70     return doMultithreadedLoop(
71         new Callable<Long>() {
72           @Override public Long call() {
73             return runAddSingleThread(reps);
74           }
75         });
76   }
77 
78   @Benchmark long addRemove(final int reps) throws ExecutionException, InterruptedException {
79     return doMultithreadedLoop(
80         new Callable<Long>() {
81           @Override public Long call() {
82             return runAddRemoveSingleThread(reps);
83           }
84         });
85   }
86 
87   private long doMultithreadedLoop(Callable<Long> task)
88       throws InterruptedException, ExecutionException {
89 
90     List<Future<Long>> futures = Lists.newArrayListWithCapacity(threads);
91     for (int i = 0; i < threads; i++) {
92       futures.add(threadPool.submit(task));
93     }
94     long total = 0;
95     for (Future<Long> future : futures) {
96       total += future.get();
97     }
98     return total;
99   }
100 
101   private long runAddSingleThread(int reps) {
102     Random random = new Random();
103     int nKeys = keys.size();
104     long blah = 0;
105     for (int i = 0; i < reps; i++) {
106       Integer key = keys.get(random.nextInt(nKeys));
107       int delta = random.nextInt(5);
108       blah += delta;
109       multiset.add(key, delta);
110     }
111     return blah;
112   }
113 
114   private long runAddRemoveSingleThread(int reps) {
115     Random random = new Random();
116     int nKeys = keys.size();
117     long blah = 0;
118     for (int i = 0; i < reps; i++) {
119       Integer key = keys.get(random.nextInt(nKeys));
120       // This range is [-5, 4] - slight negative bias so we often hit zero, which brings the
121       // auto-removal of zeroes into play.
122       int delta = random.nextInt(10) - 5;
123       blah += delta;
124       if (delta >= 0) {
125         multiset.add(key, delta);
126       } else {
127         multiset.remove(key, -delta);
128       }
129     }
130     return blah;
131   }
132 
133   private enum MultisetSupplier {
134     CONCURRENT_HASH_MULTISET() {
135       @Override Multiset<Integer> get() {
136         return ConcurrentHashMultiset.create();
137       }
138     },
139     BOXED_ATOMIC_REPLACE() {
140       @Override Multiset<Integer> get() {
141         return OldConcurrentHashMultiset.create();
142       }
143     },
144     SYNCHRONIZED_MULTISET() {
145       @Override Multiset<Integer> get() {
146         return Synchronized.multiset(HashMultiset.<Integer>create(), null);
147       }
148     },
149     ;
150 
151     abstract Multiset<Integer> get();
152   }
153 
154   /**
155    * Duplication of the old version of ConcurrentHashMultiset (with some unused stuff removed, like
156    * serialization code) which used a map with boxed integers for the values.
157    */
158   private static final class OldConcurrentHashMultiset<E> extends AbstractMultiset<E> {
159     /** The number of occurrences of each element. */
160     private final transient ConcurrentMap<E, Integer> countMap;
161 
162     /**
163      * Creates a new, empty {@code OldConcurrentHashMultiset} using the default
164      * initial capacity, load factor, and concurrency settings.
165      */
166     public static <E> OldConcurrentHashMultiset<E> create() {
167       return new OldConcurrentHashMultiset<E>(new ConcurrentHashMap<E, Integer>());
168     }
169 
170     @VisibleForTesting OldConcurrentHashMultiset(ConcurrentMap<E, Integer> countMap) {
171       checkArgument(countMap.isEmpty());
172       this.countMap = countMap;
173     }
174 
175     // Query Operations
176 
177     /**
178      * Returns the number of occurrences of {@code element} in this multiset.
179      *
180      * @param element the element to look for
181      * @return the nonnegative number of occurrences of the element
182      */
183     @Override public int count(@Nullable Object element) {
184       try {
185         return unbox(countMap.get(element));
186       } catch (NullPointerException e) {
187         return 0;
188       } catch (ClassCastException e) {
189         return 0;
190       }
191     }
192 
193     /**
194      * {@inheritDoc}
195      *
196      * <p>If the data in the multiset is modified by any other threads during this
197      * method, it is undefined which (if any) of these modifications will be
198      * reflected in the result.
199      */
200     @Override public int size() {
201       long sum = 0L;
202       for (Integer value : countMap.values()) {
203         sum += value;
204       }
205       return Ints.saturatedCast(sum);
206     }
207 
208     /*
209     * Note: the superclass toArray() methods assume that size() gives a correct
210     * answer, which ours does not.
211     */
212 
213     @Override public Object[] toArray() {
214       return snapshot().toArray();
215     }
216 
217     @Override public <T> T[] toArray(T[] array) {
218       return snapshot().toArray(array);
219     }
220 
221     /*
222     * We'd love to use 'new ArrayList(this)' or 'list.addAll(this)', but
223     * either of these would recurse back to us again!
224     */
225     private List<E> snapshot() {
226       List<E> list = Lists.newArrayListWithExpectedSize(size());
227       for (Multiset.Entry<E> entry : entrySet()) {
228         E element = entry.getElement();
229         for (int i = entry.getCount(); i > 0; i--) {
230           list.add(element);
231         }
232       }
233       return list;
234     }
235 
236     // Modification Operations
237 
238     /**
239      * Adds a number of occurrences of the specified element to this multiset.
240      *
241      * @param element the element to add
242      * @param occurrences the number of occurrences to add
243      * @return the previous count of the element before the operation; possibly
244      *     zero
245      * @throws IllegalArgumentException if {@code occurrences} is negative, or if
246      *     the resulting amount would exceed {@link Integer#MAX_VALUE}
247      */
248     @Override public int add(E element, int occurrences) {
249       if (occurrences == 0) {
250         return count(element);
251       }
252       checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
253 
254       while (true) {
255         int current = count(element);
256         if (current == 0) {
257           if (countMap.putIfAbsent(element, occurrences) == null) {
258             return 0;
259           }
260         } else {
261           checkArgument(occurrences <= Integer.MAX_VALUE - current,
262               "Overflow adding %s occurrences to a count of %s",
263               occurrences, current);
264           int next = current + occurrences;
265           if (countMap.replace(element, current, next)) {
266             return current;
267           }
268         }
269         // If we're still here, there was a race, so just try again.
270       }
271     }
272 
273     /**
274      * Removes a number of occurrences of the specified element from this
275      * multiset. If the multiset contains fewer than this number of occurrences to
276      * begin with, all occurrences will be removed.
277      *
278      * @param element the element whose occurrences should be removed
279      * @param occurrences the number of occurrences of the element to remove
280      * @return the count of the element before the operation; possibly zero
281      * @throws IllegalArgumentException if {@code occurrences} is negative
282      */
283     @Override public int remove(@Nullable Object element, int occurrences) {
284       if (occurrences == 0) {
285         return count(element);
286       }
287       checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
288 
289       while (true) {
290         int current = count(element);
291         if (current == 0) {
292           return 0;
293         }
294         if (occurrences >= current) {
295           if (countMap.remove(element, current)) {
296             return current;
297           }
298         } else {
299           // We know it's an "E" because it already exists in the map.
300           @SuppressWarnings("unchecked")
301           E casted = (E) element;
302 
303           if (countMap.replace(casted, current, current - occurrences)) {
304             return current;
305           }
306         }
307         // If we're still here, there was a race, so just try again.
308       }
309     }
310 
311     /**
312      * Removes <b>all</b> occurrences of the specified element from this multiset.
313      * This method complements {@link Multiset#remove(Object)}, which removes only
314      * one occurrence at a time.
315      *
316      * @param element the element whose occurrences should all be removed
317      * @return the number of occurrences successfully removed, possibly zero
318      */
319     private int removeAllOccurrences(@Nullable Object element) {
320       try {
321         return unbox(countMap.remove(element));
322       } catch (NullPointerException e) {
323         return 0;
324       } catch (ClassCastException e) {
325         return 0;
326       }
327     }
328 
329     /**
330      * Removes exactly the specified number of occurrences of {@code element}, or
331      * makes no change if this is not possible.
332      *
333      * <p>This method, in contrast to {@link #remove(Object, int)}, has no effect
334      * when the element count is smaller than {@code occurrences}.
335      *
336      * @param element the element to remove
337      * @param occurrences the number of occurrences of {@code element} to remove
338      * @return {@code true} if the removal was possible (including if {@code
339      *     occurrences} is zero)
340      */
341     public boolean removeExactly(@Nullable Object element, int occurrences) {
342       if (occurrences == 0) {
343         return true;
344       }
345       checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
346 
347       while (true) {
348         int current = count(element);
349         if (occurrences > current) {
350           return false;
351         }
352         if (occurrences == current) {
353           if (countMap.remove(element, occurrences)) {
354             return true;
355           }
356         } else {
357           @SuppressWarnings("unchecked") // it's in the map, must be an "E"
358               E casted = (E) element;
359           if (countMap.replace(casted, current, current - occurrences)) {
360             return true;
361           }
362         }
363         // If we're still here, there was a race, so just try again.
364       }
365     }
366 
367     /**
368      * Adds or removes occurrences of {@code element} such that the {@link #count}
369      * of the element becomes {@code count}.
370      *
371      * @return the count of {@code element} in the multiset before this call
372      * @throws IllegalArgumentException if {@code count} is negative
373      */
374     @Override public int setCount(E element, int count) {
375       checkNonnegative(count, "count");
376       return (count == 0)
377           ? removeAllOccurrences(element)
378           : unbox(countMap.put(element, count));
379     }
380 
381     /**
382      * Sets the number of occurrences of {@code element} to {@code newCount}, but
383      * only if the count is currently {@code oldCount}. If {@code element} does
384      * not appear in the multiset exactly {@code oldCount} times, no changes will
385      * be made.
386      *
387      * @return {@code true} if the change was successful. This usually indicates
388      *     that the multiset has been modified, but not always: in the case that
389      *     {@code oldCount == newCount}, the method will return {@code true} if
390      *     the condition was met.
391      * @throws IllegalArgumentException if {@code oldCount} or {@code newCount} is
392      *     negative
393      */
394     @Override public boolean setCount(E element, int oldCount, int newCount) {
395       checkNonnegative(oldCount, "oldCount");
396       checkNonnegative(newCount, "newCount");
397       if (newCount == 0) {
398         if (oldCount == 0) {
399           // No change to make, but must return true if the element is not present
400           return !countMap.containsKey(element);
401         } else {
402           return countMap.remove(element, oldCount);
403         }
404       }
405       if (oldCount == 0) {
406         return countMap.putIfAbsent(element, newCount) == null;
407       }
408       return countMap.replace(element, oldCount, newCount);
409     }
410 
411     // Views
412 
413     @Override Set<E> createElementSet() {
414       final Set<E> delegate = countMap.keySet();
415       return new ForwardingSet<E>() {
416         @Override protected Set<E> delegate() {
417           return delegate;
418         }
419         @Override public boolean remove(Object object) {
420           try {
421             return delegate.remove(object);
422           } catch (NullPointerException e) {
423             return false;
424           } catch (ClassCastException e) {
425             return false;
426           }
427         }
428       };
429     }
430 
431     private transient EntrySet entrySet;
432 
433     @Override public Set<Multiset.Entry<E>> entrySet() {
434       EntrySet result = entrySet;
435       if (result == null) {
436         entrySet = result = new EntrySet();
437       }
438       return result;
439     }
440 
441     @Override int distinctElements() {
442       return countMap.size();
443     }
444 
445     @Override public boolean isEmpty() {
446       return countMap.isEmpty();
447     }
448 
449     @Override Iterator<Entry<E>> entryIterator() {
450       final Iterator<Map.Entry<E, Integer>> backingIterator =
451           countMap.entrySet().iterator();
452       return new Iterator<Entry<E>>() {
453         @Override public boolean hasNext() {
454           return backingIterator.hasNext();
455         }
456 
457         @Override public Multiset.Entry<E> next() {
458           Map.Entry<E, Integer> backingEntry = backingIterator.next();
459           return Multisets.immutableEntry(backingEntry.getKey(),
460               backingEntry.getValue());
461         }
462 
463         @Override public void remove() {
464           backingIterator.remove();
465         }
466       };
467     }
468 
469     @Override public void clear() {
470       countMap.clear();
471     }
472 
473     private class EntrySet extends AbstractMultiset<E>.EntrySet {
474       @Override Multiset<E> multiset() {
475         return OldConcurrentHashMultiset.this;
476       }
477 
478       /*
479       * Note: the superclass toArray() methods assume that size() gives a correct
480       * answer, which ours does not.
481       */
482 
483       @Override public Object[] toArray() {
484         return snapshot().toArray();
485       }
486 
487       @Override public <T> T[] toArray(T[] array) {
488         return snapshot().toArray(array);
489       }
490 
491       private List<Multiset.Entry<E>> snapshot() {
492         List<Multiset.Entry<E>> list = Lists.newArrayListWithExpectedSize(size());
493         // not Iterables.addAll(list, this), because that'll forward back here
494         Iterators.addAll(list, iterator());
495         return list;
496       }
497 
498       @Override public boolean remove(Object object) {
499         if (object instanceof Multiset.Entry) {
500           Multiset.Entry<?> entry = (Multiset.Entry<?>) object;
501           Object element = entry.getElement();
502           int entryCount = entry.getCount();
503           return countMap.remove(element, entryCount);
504         }
505         return false;
506       }
507 
508       /**
509        * The hash code is the same as countMap's, though the objects aren't equal.
510        */
511       @Override public int hashCode() {
512         return countMap.hashCode();
513       }
514     }
515 
516     /**
517      * We use a special form of unboxing that treats null as zero.
518      */
519     private static int unbox(@Nullable Integer i) {
520       return (i == null) ? 0 : i;
521     }
522   }
523 }
524