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