├── .gitignore ├── README.md └── src ├── CacheTest.java ├── cache ├── ConcurrentHashMapCache.java ├── Ehcache.java ├── ICache.java ├── JCSCache.java ├── MemoryCacheConfiguration.java └── UnsafeMemoryCache.java └── util ├── JavaInternals.java └── MappedFile.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | shared-memory-cache 2 | =================== 3 | 4 | Odnoklassniki Unsafe-based MemoryCache -------------------------------------------------------------------------------- /src/CacheTest.java: -------------------------------------------------------------------------------- 1 | import cache.*; 2 | 3 | import java.util.Random; 4 | 5 | public class CacheTest { 6 | private static final long K = 1024; 7 | private static final long M = 1024*K; 8 | private static final long G = 1024*M; 9 | 10 | private static final long MAGIC = 54331; 11 | 12 | private static final int WARMUP_COUNT = 100000; 13 | private static final int RUN_COUNT = 1000000; 14 | 15 | public static void testWrite(ICache cache, int count) { 16 | Random random = new Random(0); 17 | for (int i = 0; i < count; i++) { 18 | long key = random.nextInt(1 << 20) * MAGIC; 19 | cache.put(key, new byte[random.nextInt(8192)]); 20 | } 21 | } 22 | 23 | public static void testRead(ICache cache, int count) { 24 | Random random = new Random(1); 25 | for (int i = 0; i < count; i++) { 26 | long key = random.nextInt(1 << 20) * MAGIC; 27 | cache.get(key); 28 | } 29 | } 30 | 31 | public static void testRead9Write1(ICache cache, int count) { 32 | Random random = new Random(2); 33 | for (int i = 0; i < count; i++) { 34 | long key = random.nextInt(1 << 20) * MAGIC; 35 | if (random.nextInt(10) == 0) { 36 | cache.put(key, new byte[random.nextInt(8192)]); 37 | } else { 38 | cache.get(key); 39 | } 40 | } 41 | } 42 | 43 | public static void testAll(ICache cache) { 44 | // Warm-up 45 | testWrite(cache, WARMUP_COUNT); 46 | testRead(cache, WARMUP_COUNT); 47 | testRead9Write1(cache, WARMUP_COUNT); 48 | 49 | String cacheClass = cache.getClass().getSimpleName(); 50 | long start, end; 51 | 52 | start = System.currentTimeMillis(); 53 | testWrite(cache, RUN_COUNT); 54 | end = System.currentTimeMillis(); 55 | System.out.println(cacheClass + " write: " + (end - start)); 56 | 57 | start = System.currentTimeMillis(); 58 | testRead(cache, RUN_COUNT); 59 | end = System.currentTimeMillis(); 60 | System.out.println(cacheClass + " read: " + (end - start)); 61 | 62 | start = System.currentTimeMillis(); 63 | testRead9Write1(cache, RUN_COUNT); 64 | end = System.currentTimeMillis(); 65 | System.out.println(cacheClass + " read-write: " + (end - start)); 66 | } 67 | 68 | public static void main(String[] args) throws Exception { 69 | String type = args.length == 0 ? null : args[0]; 70 | ICache cache; 71 | if ("ehcache".equals(type)) { 72 | cache = new Ehcache(2*G); 73 | } else if ("jcs".equals(type)) { 74 | cache = new JCSCache("sampleCache"); 75 | } else if ("chm".equals(type)) { 76 | cache = new ConcurrentHashMapCache(3000000, 256); 77 | } else { 78 | cache = new UnsafeMemoryCache(new MemoryCacheConfiguration(2*G, 200*K, "/dev/shm/cache-test")); 79 | } 80 | testAll(cache); 81 | cache.close(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/cache/ConcurrentHashMapCache.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | 5 | public class ConcurrentHashMapCache extends ConcurrentHashMap implements ICache { 6 | 7 | public ConcurrentHashMapCache(int capacity, int concurrencyLevel) { 8 | super(capacity, 0.75f, concurrencyLevel); 9 | } 10 | 11 | @Override 12 | public byte[] get(long key) { 13 | return super.get(key); 14 | } 15 | 16 | @Override 17 | public boolean put(long key, byte[] value) { 18 | super.put(key, value); 19 | return true; 20 | } 21 | 22 | @Override 23 | public void close() { 24 | clear(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/cache/Ehcache.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | import net.sf.ehcache.Cache; 4 | import net.sf.ehcache.CacheManager; 5 | import net.sf.ehcache.Element; 6 | import net.sf.ehcache.config.CacheConfiguration; 7 | import net.sf.ehcache.config.MemoryUnit; 8 | 9 | public class Ehcache implements ICache { 10 | private Cache cache; 11 | 12 | public Ehcache(long offHeap) { 13 | CacheConfiguration config = new CacheConfiguration("sample-offheap-cache", 0). 14 | overflowToOffHeap(true).maxBytesLocalOffHeap(offHeap, MemoryUnit.BYTES); 15 | CacheManager manager = CacheManager.create(); 16 | manager.addCache(new Cache(config)); 17 | this.cache = manager.getCache("sample-offheap-cache"); 18 | } 19 | 20 | @Override 21 | public byte[] get(long key) { 22 | Element element = cache.get(key); 23 | return element != null ? (byte[]) element.getValue() : null; 24 | } 25 | 26 | @Override 27 | public boolean put(long key, byte[] value) { 28 | cache.put(new Element(key, value)); 29 | return true; 30 | } 31 | 32 | @Override 33 | public void close() { 34 | cache.dispose(); 35 | CacheManager.create().removeCache("sample-offheap-cache"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/cache/ICache.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | public interface ICache { 4 | byte[] get(long key); 5 | boolean put(long key, byte[] value); 6 | void close(); 7 | } 8 | -------------------------------------------------------------------------------- /src/cache/JCSCache.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | import org.apache.jcs.JCS; 4 | import org.apache.jcs.access.exception.CacheException; 5 | 6 | public class JCSCache implements ICache { 7 | private JCS cache; 8 | 9 | public JCSCache(String name) throws CacheException { 10 | this.cache = JCS.getInstance(name); 11 | } 12 | 13 | @Override 14 | public byte[] get(long key) { 15 | return (byte[]) cache.get(key); 16 | } 17 | 18 | @Override 19 | public boolean put(long key, byte[] value) { 20 | try { 21 | cache.put(key, value); 22 | return true; 23 | } catch (CacheException e) { 24 | return false; 25 | } 26 | } 27 | 28 | @Override 29 | public void close() { 30 | cache.dispose(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/cache/MemoryCacheConfiguration.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | public class MemoryCacheConfiguration { 4 | private long capacity; 5 | private long segmentSize; 6 | private String imageFile; 7 | 8 | public MemoryCacheConfiguration(long capacity, long segmentSize, String imageFile) { 9 | this.capacity = capacity; 10 | this.segmentSize = segmentSize; 11 | this.imageFile = imageFile; 12 | } 13 | 14 | public long getCapacity() { 15 | return capacity; 16 | } 17 | 18 | public long getSegmentSize() { 19 | return segmentSize; 20 | } 21 | 22 | public String getImageFile() { 23 | return imageFile; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/cache/UnsafeMemoryCache.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | import util.MappedFile; 4 | import util.JavaInternals; 5 | 6 | import sun.misc.Unsafe; 7 | 8 | import java.util.concurrent.Semaphore; 9 | 10 | public class UnsafeMemoryCache implements ICache { 11 | private static final Unsafe unsafe = JavaInternals.getUnsafe(); 12 | private static final int BYTE_ARRAY_OFFSET = unsafe.arrayBaseOffset(byte[].class); 13 | private static final int WRITE_PERMITS = 1024; 14 | 15 | private static final int MAX_KEY_COUNT = 256; 16 | private static final int KEY_SIZE = 8; 17 | private static final int KEY_SPACE = MAX_KEY_COUNT * KEY_SIZE; 18 | private static final int DATA_START = KEY_SPACE * 2; 19 | private static final int OFFSET = KEY_SPACE + 0; 20 | private static final int LENGTH = KEY_SPACE + 4; 21 | 22 | private MappedFile mmap; 23 | private int segmentSize; 24 | private int segmentMask; 25 | private Segment[] segments; 26 | 27 | static final class Segment extends Semaphore { 28 | final long start; 29 | int tail; 30 | int count; 31 | 32 | Segment(long start, int size) { 33 | super(WRITE_PERMITS, true); 34 | this.start = start; 35 | verify(start, size); 36 | } 37 | 38 | private void verify(long start, int size) { 39 | int maxTail = DATA_START; 40 | long prevKey = 0; 41 | long pos = start; 42 | 43 | for (long keysEnd = start + KEY_SPACE; pos < keysEnd; pos += KEY_SIZE) { 44 | long key = unsafe.getLong(pos); 45 | if (key <= prevKey) { 46 | break; 47 | } 48 | 49 | int offset = unsafe.getInt(pos + OFFSET); 50 | int length = unsafe.getInt(pos + LENGTH); 51 | int newTail = (offset + length + 7) & ~7; 52 | if (offset < DATA_START || length < 0 || newTail > size) { 53 | break; 54 | } 55 | 56 | if (newTail > maxTail) { 57 | maxTail = newTail; 58 | } 59 | prevKey = key; 60 | } 61 | 62 | this.tail = maxTail; 63 | this.count = (int) (pos - start) >>> 3; 64 | } 65 | } 66 | 67 | public UnsafeMemoryCache(MemoryCacheConfiguration configuration) throws Exception { 68 | long requestedCapacity = configuration.getCapacity(); 69 | long desiredSegmentSize = configuration.getSegmentSize(); 70 | int segmentCount = calculateSegmentCount(requestedCapacity, desiredSegmentSize); 71 | long segmentSize = (requestedCapacity / segmentCount + 31) & ~31L; 72 | 73 | this.mmap = new MappedFile(configuration.getImageFile(), segmentSize * segmentCount); 74 | this.segmentSize = (int) segmentSize; 75 | this.segmentMask = segmentCount - 1; 76 | this.segments = new Segment[segmentCount]; 77 | 78 | for (int i = 0; i < segmentCount; i++) { 79 | segments[i] = new Segment(mmap.getAddr() + segmentSize * i, this.segmentSize); 80 | } 81 | } 82 | 83 | @Override 84 | public void close() { 85 | mmap.close(); 86 | segments = null; 87 | } 88 | 89 | @Override 90 | public byte[] get(long key) { 91 | Segment segment = segmentFor(key); 92 | segment.acquireUninterruptibly(); 93 | try { 94 | long segmentStart = segment.start; 95 | long keysEnd = segmentStart + (segment.count << 3); 96 | long keyAddr = binarySearch(key, segmentStart, keysEnd); 97 | 98 | if (keyAddr > 0) { 99 | int offset = unsafe.getInt(keyAddr + OFFSET); 100 | int length = unsafe.getInt(keyAddr + LENGTH); 101 | byte[] result = new byte[length]; 102 | unsafe.copyMemory(null, segmentStart + offset, result, BYTE_ARRAY_OFFSET, length); 103 | return result; 104 | } 105 | 106 | return null; 107 | } finally { 108 | segment.release(); 109 | } 110 | } 111 | 112 | @Override 113 | public boolean put(long key, byte[] value) { 114 | int length = value.length; 115 | if (length >= segmentSize >> 1) { 116 | return false; 117 | } 118 | 119 | Segment segment = segmentFor(key); 120 | segment.acquireUninterruptibly(WRITE_PERMITS); 121 | try { 122 | long segmentStart = segment.start; 123 | int tail = segment.tail; 124 | int newTail = (tail + length + 7) & ~7; 125 | 126 | if (newTail > segmentSize) { 127 | tail = DATA_START; 128 | newTail = (tail + length + 7) & ~7; 129 | } 130 | 131 | purgeOverlappingRegion(segment, tail, newTail); 132 | 133 | int count = segment.count; 134 | if (count == MAX_KEY_COUNT) { 135 | return false; 136 | } 137 | 138 | long keysEnd = segmentStart + (count << 3); 139 | long keyAddr = binarySearch(key, segmentStart, keysEnd); 140 | if (keyAddr < 0) { 141 | keyAddr = ~keyAddr; 142 | unsafe.copyMemory(null, keyAddr, null, keyAddr + KEY_SIZE, keysEnd - keyAddr); 143 | unsafe.copyMemory(null, keyAddr + KEY_SPACE, null, keyAddr + (KEY_SPACE + KEY_SIZE), keysEnd - keyAddr); 144 | segment.count = count + 1; 145 | } 146 | 147 | unsafe.putLong(keyAddr, key); 148 | unsafe.putInt(keyAddr + OFFSET, tail); 149 | unsafe.putInt(keyAddr + LENGTH, length); 150 | unsafe.copyMemory(value, BYTE_ARRAY_OFFSET, null, segmentStart + tail, length); 151 | 152 | segment.tail = newTail; 153 | return true; 154 | } finally { 155 | segment.release(WRITE_PERMITS); 156 | } 157 | } 158 | 159 | public int count() { 160 | int count = 0; 161 | for (Segment segment : segments) { 162 | count += segment.count; 163 | } 164 | return count; 165 | } 166 | 167 | private int calculateSegmentCount(long requestedCapacity, long segmentSize) { 168 | int segmentCount = 1; 169 | while (segmentSize * segmentCount < requestedCapacity) { 170 | segmentCount <<= 1; 171 | } 172 | return segmentCount; 173 | } 174 | 175 | private Segment segmentFor(long key) { 176 | return segments[((int) (key ^ (key >>> 16))) & segmentMask]; 177 | } 178 | 179 | private static long binarySearch(long key, long low, long high) { 180 | for (high -= KEY_SIZE; low <= high; ) { 181 | long mid = ((low + high) >>> 1) & ~7L; 182 | long midVal = unsafe.getLong(mid); 183 | 184 | if (midVal < key) { 185 | low = mid + KEY_SIZE; 186 | } else if (midVal > key) { 187 | high = mid - KEY_SIZE; 188 | } else { 189 | return mid; 190 | } 191 | } 192 | return ~low; 193 | } 194 | 195 | private static void purgeOverlappingRegion(Segment segment, int from, int to) { 196 | long pos = segment.start + OFFSET; 197 | int count = segment.count; 198 | long end = pos + (count << 3); 199 | 200 | for (long newPos = pos; pos < end; pos += KEY_SIZE) { 201 | int offset = unsafe.getInt(pos); 202 | if (offset >= from && offset < to) { 203 | count--; 204 | } else { 205 | if (newPos != pos) { 206 | unsafe.putInt(newPos, offset); 207 | unsafe.putInt(newPos + 4, unsafe.getInt(pos + 4)); 208 | unsafe.putLong(newPos - KEY_SPACE, unsafe.getLong(pos - KEY_SPACE)); 209 | } 210 | newPos += KEY_SIZE; 211 | } 212 | } 213 | 214 | segment.count = count; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/util/JavaInternals.java: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | import sun.misc.Cleaner; 4 | import sun.misc.Unsafe; 5 | 6 | import java.lang.reflect.Field; 7 | import java.lang.reflect.Method; 8 | import java.nio.Buffer; 9 | import java.nio.ByteBuffer; 10 | 11 | public class JavaInternals { 12 | private static final Unsafe unsafe; 13 | 14 | static { 15 | try { 16 | unsafe = (Unsafe) getField(Unsafe.class, "theUnsafe").get(null); 17 | } catch (IllegalAccessException e) { 18 | throw new IllegalStateException(e); 19 | } 20 | } 21 | 22 | public static Field getField(Class cls, String name) { 23 | try { 24 | Field f = cls.getDeclaredField(name); 25 | f.setAccessible(true); 26 | return f; 27 | } catch (Exception e) { 28 | throw new IllegalStateException(e); 29 | } 30 | } 31 | 32 | public static Method getMethod(Class cls, String name, Class... params) { 33 | try { 34 | Method m = cls.getDeclaredMethod(name, params); 35 | m.setAccessible(true); 36 | return m; 37 | } catch (Exception e) { 38 | throw new IllegalStateException(e); 39 | } 40 | } 41 | 42 | public static Unsafe getUnsafe() { 43 | return unsafe; 44 | } 45 | 46 | public static long allocateMemory(long size, Object holder) { 47 | final long address = unsafe.allocateMemory(size); 48 | Cleaner.create(holder, new Runnable() { 49 | @Override 50 | public void run() { 51 | unsafe.freeMemory(address); 52 | } 53 | }); 54 | return address; 55 | } 56 | 57 | public static long getByteBufferAddress(ByteBuffer buffer) { 58 | try { 59 | return getField(Buffer.class, "address").getLong(buffer); 60 | } catch (IllegalAccessException e) { 61 | throw new IllegalStateException(e); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/util/MappedFile.java: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | import sun.nio.ch.FileChannelImpl; 4 | 5 | import java.io.RandomAccessFile; 6 | import java.lang.reflect.Method; 7 | import java.nio.channels.FileChannel; 8 | 9 | public class MappedFile { 10 | private static Method map0 = JavaInternals.getMethod(FileChannelImpl.class, "map0", int.class, long.class, long.class); 11 | private static Method unmap0 = JavaInternals.getMethod(FileChannelImpl.class, "unmap0", long.class, long.class); 12 | 13 | private long addr; 14 | private long size; 15 | 16 | public MappedFile(String name, long size) throws Exception { 17 | size = (size + 0xfffL) & ~0xfffL; 18 | 19 | RandomAccessFile f = new RandomAccessFile(name, "rw"); 20 | FileChannel ch = null; 21 | 22 | try { 23 | f.setLength(size); 24 | ch = f.getChannel(); 25 | this.addr = (Long) map0.invoke(ch, 1, 0L, size); 26 | this.size = size; 27 | } finally { 28 | if (ch != null) { 29 | ch.close(); 30 | } 31 | f.close(); 32 | } 33 | } 34 | 35 | public void close() { 36 | if (addr != 0) { 37 | try { 38 | unmap0.invoke(null, addr, size); 39 | } catch (Exception e) { 40 | // ignore 41 | } 42 | addr = 0; 43 | } 44 | } 45 | 46 | public final long getAddr() { 47 | return addr; 48 | } 49 | 50 | public final long getSize() { 51 | return size; 52 | } 53 | } 54 | --------------------------------------------------------------------------------