001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.util; 019 020import java.io.PrintStream; 021import java.util.ConcurrentModificationException; 022import java.util.Iterator; 023 024import org.apache.hadoop.HadoopIllegalArgumentException; 025import org.apache.hadoop.classification.InterfaceAudience; 026import org.apache.hadoop.util.StringUtils; 027 028import com.google.common.annotations.VisibleForTesting; 029 030/** 031 * A low memory footprint {@link GSet} implementation, 032 * which uses an array for storing the elements 033 * and linked lists for collision resolution. 034 * 035 * No rehash will be performed. 036 * Therefore, the internal array will never be resized. 037 * 038 * This class does not support null element. 039 * 040 * This class is not thread safe. 041 * 042 * @param <K> Key type for looking up the elements 043 * @param <E> Element type, which must be 044 * (1) a subclass of K, and 045 * (2) implementing {@link LinkedElement} interface. 046 */ 047@InterfaceAudience.Private 048public class LightWeightGSet<K, E extends K> implements GSet<K, E> { 049 /** 050 * Elements of {@link LightWeightGSet}. 051 */ 052 public static interface LinkedElement { 053 /** Set the next element. */ 054 public void setNext(LinkedElement next); 055 056 /** Get the next element. */ 057 public LinkedElement getNext(); 058 } 059 060 static final int MAX_ARRAY_LENGTH = 1 << 30; //prevent int overflow problem 061 static final int MIN_ARRAY_LENGTH = 1; 062 063 /** 064 * An internal array of entries, which are the rows of the hash table. 065 * The size must be a power of two. 066 */ 067 private final LinkedElement[] entries; 068 /** A mask for computing the array index from the hash value of an element. */ 069 private final int hash_mask; 070 /** The size of the set (not the entry array). */ 071 private int size = 0; 072 /** Modification version for fail-fast. 073 * @see ConcurrentModificationException 074 */ 075 private int modification = 0; 076 077 /** 078 * @param recommended_length Recommended size of the internal array. 079 */ 080 public LightWeightGSet(final int recommended_length) { 081 final int actual = actualArrayLength(recommended_length); 082 if (LOG.isDebugEnabled()) { 083 LOG.debug("recommended=" + recommended_length + ", actual=" + actual); 084 } 085 entries = new LinkedElement[actual]; 086 hash_mask = entries.length - 1; 087 } 088 089 //compute actual length 090 private static int actualArrayLength(int recommended) { 091 if (recommended > MAX_ARRAY_LENGTH) { 092 return MAX_ARRAY_LENGTH; 093 } else if (recommended < MIN_ARRAY_LENGTH) { 094 return MIN_ARRAY_LENGTH; 095 } else { 096 final int a = Integer.highestOneBit(recommended); 097 return a == recommended? a: a << 1; 098 } 099 } 100 101 @Override 102 public int size() { 103 return size; 104 } 105 106 private int getIndex(final K key) { 107 return key.hashCode() & hash_mask; 108 } 109 110 private E convert(final LinkedElement e){ 111 @SuppressWarnings("unchecked") 112 final E r = (E)e; 113 return r; 114 } 115 116 @Override 117 public E get(final K key) { 118 //validate key 119 if (key == null) { 120 throw new NullPointerException("key == null"); 121 } 122 123 //find element 124 final int index = getIndex(key); 125 for(LinkedElement e = entries[index]; e != null; e = e.getNext()) { 126 if (e.equals(key)) { 127 return convert(e); 128 } 129 } 130 //element not found 131 return null; 132 } 133 134 @Override 135 public boolean contains(final K key) { 136 return get(key) != null; 137 } 138 139 @Override 140 public E put(final E element) { 141 //validate element 142 if (element == null) { 143 throw new NullPointerException("Null element is not supported."); 144 } 145 if (!(element instanceof LinkedElement)) { 146 throw new HadoopIllegalArgumentException( 147 "!(element instanceof LinkedElement), element.getClass()=" 148 + element.getClass()); 149 } 150 final LinkedElement e = (LinkedElement)element; 151 152 //find index 153 final int index = getIndex(element); 154 155 //remove if it already exists 156 final E existing = remove(index, element); 157 158 //insert the element to the head of the linked list 159 modification++; 160 size++; 161 e.setNext(entries[index]); 162 entries[index] = e; 163 164 return existing; 165 } 166 167 /** 168 * Remove the element corresponding to the key, 169 * given key.hashCode() == index. 170 * 171 * @return If such element exists, return it. 172 * Otherwise, return null. 173 */ 174 private E remove(final int index, final K key) { 175 if (entries[index] == null) { 176 return null; 177 } else if (entries[index].equals(key)) { 178 //remove the head of the linked list 179 modification++; 180 size--; 181 final LinkedElement e = entries[index]; 182 entries[index] = e.getNext(); 183 e.setNext(null); 184 return convert(e); 185 } else { 186 //head != null and key is not equal to head 187 //search the element 188 LinkedElement prev = entries[index]; 189 for(LinkedElement curr = prev.getNext(); curr != null; ) { 190 if (curr.equals(key)) { 191 //found the element, remove it 192 modification++; 193 size--; 194 prev.setNext(curr.getNext()); 195 curr.setNext(null); 196 return convert(curr); 197 } else { 198 prev = curr; 199 curr = curr.getNext(); 200 } 201 } 202 //element not found 203 return null; 204 } 205 } 206 207 @Override 208 public E remove(final K key) { 209 //validate key 210 if (key == null) { 211 throw new NullPointerException("key == null"); 212 } 213 return remove(getIndex(key), key); 214 } 215 216 @Override 217 public Iterator<E> iterator() { 218 return new SetIterator(); 219 } 220 221 @Override 222 public String toString() { 223 final StringBuilder b = new StringBuilder(getClass().getSimpleName()); 224 b.append("(size=").append(size) 225 .append(String.format(", %08x", hash_mask)) 226 .append(", modification=").append(modification) 227 .append(", entries.length=").append(entries.length) 228 .append(")"); 229 return b.toString(); 230 } 231 232 /** Print detailed information of this object. */ 233 public void printDetails(final PrintStream out) { 234 out.print(this + ", entries = ["); 235 for(int i = 0; i < entries.length; i++) { 236 if (entries[i] != null) { 237 LinkedElement e = entries[i]; 238 out.print("\n " + i + ": " + e); 239 for(e = e.getNext(); e != null; e = e.getNext()) { 240 out.print(" -> " + e); 241 } 242 } 243 } 244 out.println("\n]"); 245 } 246 247 public class SetIterator implements Iterator<E> { 248 /** The starting modification for fail-fast. */ 249 private int iterModification = modification; 250 /** The current index of the entry array. */ 251 private int index = -1; 252 private LinkedElement cur = null; 253 private LinkedElement next = nextNonemptyEntry(); 254 private boolean trackModification = true; 255 256 /** Find the next nonempty entry starting at (index + 1). */ 257 private LinkedElement nextNonemptyEntry() { 258 for(index++; index < entries.length && entries[index] == null; index++); 259 return index < entries.length? entries[index]: null; 260 } 261 262 private void ensureNext() { 263 if (trackModification && modification != iterModification) { 264 throw new ConcurrentModificationException("modification=" + modification 265 + " != iterModification = " + iterModification); 266 } 267 if (next != null) { 268 return; 269 } 270 if (cur == null) { 271 return; 272 } 273 next = cur.getNext(); 274 if (next == null) { 275 next = nextNonemptyEntry(); 276 } 277 } 278 279 @Override 280 public boolean hasNext() { 281 ensureNext(); 282 return next != null; 283 } 284 285 @Override 286 public E next() { 287 ensureNext(); 288 if (next == null) { 289 throw new IllegalStateException("There are no more elements"); 290 } 291 cur = next; 292 next = null; 293 return convert(cur); 294 } 295 296 @SuppressWarnings("unchecked") 297 @Override 298 public void remove() { 299 ensureNext(); 300 if (cur == null) { 301 throw new IllegalStateException("There is no current element " + 302 "to remove"); 303 } 304 LightWeightGSet.this.remove((K)cur); 305 iterModification++; 306 cur = null; 307 } 308 309 public void setTrackModification(boolean trackModification) { 310 this.trackModification = trackModification; 311 } 312 } 313 314 /** 315 * Let t = percentage of max memory. 316 * Let e = round(log_2 t). 317 * Then, we choose capacity = 2^e/(size of reference), 318 * unless it is outside the close interval [1, 2^30]. 319 */ 320 public static int computeCapacity(double percentage, String mapName) { 321 return computeCapacity(Runtime.getRuntime().maxMemory(), percentage, 322 mapName); 323 } 324 325 @VisibleForTesting 326 static int computeCapacity(long maxMemory, double percentage, 327 String mapName) { 328 if (percentage > 100.0 || percentage < 0.0) { 329 throw new HadoopIllegalArgumentException("Percentage " + percentage 330 + " must be greater than or equal to 0 " 331 + " and less than or equal to 100"); 332 } 333 if (maxMemory < 0) { 334 throw new HadoopIllegalArgumentException("Memory " + maxMemory 335 + " must be greater than or equal to 0"); 336 } 337 if (percentage == 0.0 || maxMemory == 0) { 338 return 0; 339 } 340 //VM detection 341 //See http://java.sun.com/docs/hotspot/HotSpotFAQ.html#64bit_detection 342 final String vmBit = System.getProperty("sun.arch.data.model"); 343 344 //Percentage of max memory 345 final double percentDivisor = 100.0/percentage; 346 final double percentMemory = maxMemory/percentDivisor; 347 348 //compute capacity 349 final int e1 = (int)(Math.log(percentMemory)/Math.log(2.0) + 0.5); 350 final int e2 = e1 - ("32".equals(vmBit)? 2: 3); 351 final int exponent = e2 < 0? 0: e2 > 30? 30: e2; 352 final int c = 1 << exponent; 353 354 LOG.info("Computing capacity for map " + mapName); 355 LOG.info("VM type = " + vmBit + "-bit"); 356 LOG.info(percentage + "% max memory " 357 + StringUtils.TraditionalBinaryPrefix.long2String(maxMemory, "B", 1) 358 + " = " 359 + StringUtils.TraditionalBinaryPrefix.long2String((long) percentMemory, 360 "B", 1)); 361 LOG.info("capacity = 2^" + exponent + " = " + c + " entries"); 362 return c; 363 } 364 365 public void clear() { 366 for (int i = 0; i < entries.length; i++) { 367 entries[i] = null; 368 } 369 size = 0; 370 } 371}