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