/*
* Copyright (C) 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.collections;
import com.google.common.base.Preconditions;
import javax.annotation.concurrent.ThreadSafe;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
import static com.google.common.base.Preconditions.checkState;
/**
* Reference implementation of SimpleArray using longs. Attempts to limit memory growth
* by allowing a small growth factor each resize
*
*/
@ThreadSafe
public class LongArray implements Array<Long> {
private static final int DEFAULT_INITIAL_CAPACITY = 3;
// memory conservative growth amount here
private static final double MIN_GROWTH_FACTOR = 0.3;
private static final int MIN_GROWTH_AMOUNT = 8; // intel xeon cache line size is 64 bytes
private static final long EMPTY = -1;
private long[] data;
private volatile int nextWritePosition = 0;
private volatile int size = 0;
public LongArray(int initialCapacity) {
data = new long[initialCapacity];
Arrays.fill(data, EMPTY);
}
public LongArray() {
this(DEFAULT_INITIAL_CAPACITY);
}
@Override
public synchronized Long get(int i) throws IndexOutOfBoundsException {
if (i >= size()) {
throw new ArrayIndexOutOfBoundsException();
}
// return null if the slot is empty
return data[i] >= 0 ? data[i] : null;
}
@Override
public int size() {
return size;
}
@Override
public synchronized int capacity() {
return data.length;
}
@Override
public Long set(int i, Long value) throws ArrayIndexOutOfBoundsException {
if (value == null) {
throw new NullPointerException("null values not allowed");
}
synchronized (this) {
if (i >= data.length) {
throw new ArrayIndexOutOfBoundsException(
String.format("tried to set value at index %d, but max index is %d", i, data.length - 1)
);
}
Long oldValue = data[i];
data[i] = value;
if (oldValue == EMPTY) {
size++;
}
return oldValue;
}
}
@Override
public synchronized int append(Long value) {
int myWritePosition;
if (nextWritePosition >= data.length) {
int sizeIncrease = (int) (data.length * MIN_GROWTH_FACTOR);
sizeIncrease = Math.max(sizeIncrease, MIN_GROWTH_AMOUNT);
internalResize(data.length + sizeIncrease);
}
// find an empty slot if the current one isn't (may have been set by set(int i, Long value)
while (!isEmpty(nextWritePosition)) {
nextWritePosition++;
if (nextWritePosition >= data.length - 1) {
// resize in advance for next write
resize(data.length + MIN_GROWTH_AMOUNT);
}
}
checkState(isEmpty(nextWritePosition) && nextWritePosition < data.length);
myWritePosition = nextWritePosition;
// prepare for next write
nextWritePosition++;
size++;
data[myWritePosition] = value;
return myWritePosition;
}
@Override
public synchronized Long remove(int i) throws ArrayIndexOutOfBoundsException {
if (isEmpty(i)) {
return null;
} else {
Long previousValue = data[i];
data[i] = EMPTY;
size--;
return convertValue(previousValue);
}
}
@Override
public int resize(int sizeHint) {
return internalResize(sizeHint);
}
/**
* @param sizeHint effectively the new size (NOT the increase). May be changed for memory or
* cache alignments as implementations see fit
* @return actual new size
*/
private synchronized int internalResize(int sizeHint) {
Preconditions.checkArgument(sizeHint > 0, "sizeHint must be > 0");
// we can use the sizeHint directly. Cases we wouldn't would be to align the array on
// word boundaries and not waste space (ex: int arrays always even sized, etc. We could even
// use cache line boundaries to maximize the chance of cache write hits (believed 64 bytes
// on our Xeon L5520 processors "cache_alignment : 64")
int newSize = sizeHint;
long[] newData = new long[newSize];
System.arraycopy(data, 0, newData, 0, data.length);
Arrays.fill(newData, data.length, newData.length, EMPTY);
data = newData;
return newSize;
}
/**
* @return
*/
@Override
public Iterator<Long> iterator() {
return new Iter();
}
/**
* since the contract to clients is that null => empty slot, we convert our empty indicators
* to null here
*
* @param value input from data
* @return input if >= 0, else null
*/
private Long convertValue(long value) {
return value >= 0 ? value : null;
}
private boolean isEmpty(int position) {
return data[position] < 0;
}
/**
* Thread safe iterator. Note: at the moment the iterator is created, sizeSnapshot elements exist
* in the array. It will terminate upon seeing the first sizeSnapshot elements, or when position
* is >= capacitySnapshot. This means fewer elements or different elements may be seen, but
* so goes the life of concurrent data structure access
* <p/>
* It also supports remove, but again, note that this may not have the intended effect if the
* underlying array is resized. Prefer directly removing an element with
* {@link #LongSimpleArray.remove(int i)}
*/
private class Iter implements Iterator<Long> {
// index to iterate through the array.
private int position = 0;
// -2 is no read called yet, -1 means remove was calledon this position
private int lastReadPosition = -2;
private int numSeen = 0;
private long sizeSnapshot = size;
private int capacitySnapshot = data.length;
private Long nextValue = null;
// indicates if we've read the next value
private boolean readNextValue = false;
// indicates if last read attemp found a value
private boolean hasNextValue = false;
@Override
public boolean hasNext() {
synchronized (LongArray.this) {
if (readNextValue) {
return hasNextValue;
}
boolean retVal = numSeen < sizeSnapshot && position < capacitySnapshot;
// this loop finds the next non-null value, or due to concurrent changes, it may simply
// terminate if it iterates over the entire capacity of the array w/o finding sizeSnapshot
// elements
while (retVal) {
nextValue = convertValue(data[position]);
if (nextValue != null) {
break;
}
position++;
// since deletions could occur, hitting the end of capacity before the size is possible
retVal = numSeen < sizeSnapshot && position < capacitySnapshot;
}
if (retVal) {
lastReadPosition = position;
}
readNextValue = true;
hasNextValue = retVal;
return retVal;
}
}
@Override
public Long next() {
synchronized (LongArray.this) {
if (!hasNext()) {
throw new NoSuchElementException();
}
readNextValue = false;
position++;
return nextValue;
}
}
@Override
public void remove() {
synchronized (LongArray.this) {
if (lastReadPosition == -2) {
throw new IllegalStateException("next() has not been called yet");
}
if (lastReadPosition == -1) {
throw new IllegalStateException("remove already called for this position");
}
data[lastReadPosition] = EMPTY;
size--;
lastReadPosition = -1;
}
}
}
}