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