Home | History | Annotate | Download | only in collect
      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 
     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