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
17package com.google.common.collect;
18
19import static com.google.common.base.Preconditions.checkArgument;
20import static com.google.common.collect.CollectPreconditions.checkNonnegative;
21
22import com.google.caliper.BeforeExperiment;
23import com.google.caliper.Benchmark;
24import com.google.caliper.Param;
25import com.google.common.annotations.VisibleForTesting;
26import com.google.common.primitives.Ints;
27import com.google.common.util.concurrent.ThreadFactoryBuilder;
28
29import java.util.Iterator;
30import java.util.List;
31import java.util.Map;
32import java.util.Random;
33import java.util.Set;
34import java.util.concurrent.Callable;
35import java.util.concurrent.ConcurrentHashMap;
36import java.util.concurrent.ConcurrentMap;
37import java.util.concurrent.ExecutionException;
38import java.util.concurrent.ExecutorService;
39import java.util.concurrent.Executors;
40import java.util.concurrent.Future;
41
42import javax.annotation.Nullable;
43
44/**
45 * Benchmarks for {@link ConcurrentHashMultiset}.
46 *
47 * @author mike nonemacher
48 */
49public 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
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
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