I finally got around
to writing a testcase to verify the numbers I presented. The following testcase
and results are for the lowest level disk operations. On my machine reading from
the cache, vs. going to disk (even when the data is in the OS cache) is 30%-40%
faster. Since Lucene makes extensive use of disk IO and often reads the same
data (e.g. reading the terms), a localized user-level cache can provide
significant performance benefits.
Using a 4mb file (so
I could be "guarantee" the disk data would be in the OS cache as well), the test
shows the following results.
Most of the CPU time
is actually used during the synchronization with multiple threads. I hacked
together a version of MemoryLRUCache that used a ConcurrentHashMap from JDK 1.5,
and it was another 50% faster ! At a minimum, if the ReadWriteLock class was
modified to use the 1.5 facilities some significant additional performance
gains should be realized.
filesize is 4194304
non-cached time = 10578, avg = 0.010578
non-cached threaded (3 threads) time = 32094, avg = 0.010698
cached time = 6125, avg = 0.006125
cache hits 996365
cache misses 3635
cached threaded (3 threads) time = 20734, avg = 0.0069113333333333336
cache hits 3989089
cache misses 10911
When using the shared test (which is more like the lucene usage, since a single "file" is shared by multiple threads), the difference is even more dramatic with multiple threads (since the cache size is effectively reduced by the number of threads). This test also shows the value of using multiple file handles when using multiple threads to read a single file (rather than using a shared file handle).filesize is 4194304
non-cached time = 10594, avg = 0.010594
non-cached threaded (3 threads) time = 42110, avg = 0.014036666666666666
cached time = 6047, avg = 0.006047
cache hits 996827
cache misses 3173
cached threaded (3 threads) time = 20079, avg = 0.006693
cache hits 3995776
cache misses 4224
package org.apache.lucene.util; import java.io.*; import java.util.Random;
import junit.framework.TestCase; public class TestNioFilePerf extends TestCase { static final String FILENAME = "testfile.dat"; static final int BLOCKSIZE = 2048; static final int NBLOCKS = 2048; // 4 mb file static final int NREADS = 500000; static final int NTHREADS = 3; static { System.setProperty("org.apache.lucene.CachePercent","90"); } public void setUp() throws Exception { FileOutputStream f = new FileOutputStream(FILENAME); Random r = new Random(); byte[] block = new byte[BLOCKSIZE]; for(int i=0;i<NBLOCKS;i++) { r.nextBytes(block); f.write(block); } f.close(); // let OS filesystem settle down Thread.sleep(5*1000); } public void tearDown() throws Exception { File f = new File(FILENAME); f.delete(); } public void testFileExists() { File f = new File(FILENAME); System.err.println("filesize is "+f.length()); } protected void doNonCachedImpl() throws Exception { RandomAccessFile f = new RandomAccessFile(FILENAME,"r"); byte[] block = new byte[BLOCKSIZE]; Random r = new Random(); for(int i=0;i<NREADS;i++) { long pos = BLOCKSIZE*(long)r.nextInt(NBLOCKS); f.seek(pos); f.readFully(block); } f.close(); } public void testNonCached() throws Exception { long stime = System.currentTimeMillis(); doNonCachedImpl(); long time = System.currentTimeMillis() - stime; System.err.println("non-cached time = "+time+", avg = "+(time/(double)NREADS)); } public void testThreadedNonCache() throws Exception { long stime = System.currentTimeMillis(); Thread[] threads = new Thread[NTHREADS]; for(int i=0;i<NTHREADS;i++){ threads[i] = new Thread(new Runnable(){ public void run() { try { doNonCachedImpl(); } catch (Exception e) { e.printStackTrace(); } }}); } for(int i=0;i<NTHREADS;i++){ threads[i].start(); } for(int i=0;i<NTHREADS;i++){ threads[i].join(); } long time = System.currentTimeMillis() - stime; System.err.println("non-cached threaded ("+NTHREADS+" threads) time = "+time+", avg = "+(time/(double)(NREADS*NTHREADS))); } protected void doCachedImpl() throws Exception { NioFile f = new NioFile(new File(FILENAME),"r"); byte[] block = new byte[BLOCKSIZE]; Random r = new Random(); for(int i=0;i<NREADS;i++) { long pos = BLOCKSIZE*(long)r.nextInt(NBLOCKS); f.read(block,0,BLOCKSIZE,pos); } f.close(); } public void testCached() throws Exception { long stime = System.currentTimeMillis(); doCachedImpl(); long time = System.currentTimeMillis() - stime; System.err.println("cached time = "+time+", avg = "+(time/(double)NREADS)); dumpStats(); } public void testThreadedCache() throws Exception { long stime = System.currentTimeMillis(); Thread[] threads = new Thread[NTHREADS]; for(int i=0;i<NTHREADS;i++){ threads[i] = new Thread(new Runnable(){ public void run() { try { doCachedImpl(); } catch (Exception e) { e.printStackTrace(); } }}); } for(int i=0;i<NTHREADS;i++){ threads[i].start(); } for(int i=0;i<NTHREADS;i++){ threads[i].join(); } long time = System.currentTimeMillis() - stime; System.err.println("cached threaded ("+NTHREADS+" threads) time = "+time+", avg = "+(time/(double)(NREADS*NTHREADS))); dumpStats(); } private void dumpStats() { System.err.println("cache hits "+NioFile.cachehits); System.err.println("cache misses "+NioFile.cachemisses); } public static void main(String[] args) throws Exception { TestNioFilePerf p = new TestNioFilePerf(); p.setUp(); p.testCached(); } }
package org.apache.lucene.util; /** * a read/write lock. allows unlimited simultaneos readers, or a single writer. A thread with * the "wrte" lock implictly owns a read lock as well. */ public class ReadWriteLock { int readlocks = 0; int writelocks = 0; Thread writethread = null; public synchronized void readLock() { while(true) { if(writelocks==0 || (Thread.currentThread()==writethread) ) { readlocks++; return; } else { try { wait(); } catch (InterruptedException e) { } } } } public synchronized void readUnlock() { readlocks--; notifyAll(); } public synchronized void writeLock() { while(true) { if(tryWriteLock()) return; try { wait(); } catch (InterruptedException e) { } } } /** * try to get the write lock * * @return true if the write lock could be acquired, else false */ public synchronized boolean tryWriteLock() { if(readlocks==0 && (writelocks==0 || writethread == Thread.currentThread())) { writethread = Thread.currentThread(); writelocks++; return true; } return false; } public synchronized void writeUnlock() { if(writelocks==0) throw new IllegalStateException("caller does not own write lock"); if(--writelocks==0) writethread=null; notifyAll(); } /** * checks if the calling thread owns the write lock * * @return true if the calling thread owns the write lock */ public synchronized boolean ownsWriteLock() { return Thread.currentThread()==writethread; } }
package org.apache.lucene.util; import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * wrapper for NIO FileChannel in order to circumvent problems with multiple threads reading the * same FileChannel, and to provide local cache. The current Windows implementation of FileChannel * has some synchronization even when performing positioned reads. See JDK bug #6265734. * * The NioFile contains internal caching to reduce the number of physical disk reads. */ public class NioFile { static final int BLOCKSIZE = Integer.getInteger("org.apache.lucene.BlockSize",4096).intValue(); static public int cachehits = 0; static public int cachemisses = 0; static float cachePercent = .10f; static { int percent = Integer.getInteger("org.apache.lucene.CachePercent",30).intValue(); if(percent<0 || percent >100) percent = 10; cachePercent = percent / 100.0f; } static long cacheSize = Long.getLong("org.apache.lucene.CacheSize",64*1024*1024L).longValue(); static public MemoryLRUCache cache; static { cache = new MemoryLRUCache((long) Math.max(cacheSize,Runtime.getRuntime().maxMemory()*cachePercent)); } File path; String mode; boolean open = true; FileChannel channel; public NioFile(File path,String mode) throws IOException { this.path = path; this.mode = mode; // System.err.println("opening nio file "+path.getAbsolutePath()); RandomAccessFile raf = new RandomAccessFile(path,mode); channel = raf.getChannel(); } public void close() throws IOException { channel.close(); open = false; } public boolean isOpen() { return open; } public void read(byte[] b, int offset, int len, long position) throws IOException { do { long blockno = (position/BLOCKSIZE); BlockKey bk = new BlockKey(this,blockno); byte[] block = cache.get(bk); if(block==null) { cachemisses++; block = new byte[BLOCKSIZE]; channel.read(ByteBuffer.wrap(block),blockno*BLOCKSIZE); cache.put(bk,block); } else cachehits++; int blockoffset = (int) (position % BLOCKSIZE); int i = Math.min(len,BLOCKSIZE-blockoffset); System.arraycopy(block,blockoffset,b,offset,i); offset += i; len -= i; position += i; } while (len >0); } static class BlockKey { NioFile file; long blockno; int hashCode; public BlockKey(NioFile file, long blockno) { this.file = file; this.blockno = blockno; hashCode = (int) (file.hashCode() ^ blockno); } public int hashCode() { return hashCode; } public boolean equals(Object o){ BlockKey bk0 = (BlockKey) o; // since the same file name can be reused (e.g. segments, etc.) and the cache entries are not cleared // when a file is closed, the comparison must be made on the object instance, not identity return file==bk0.file && blockno==bk0.blockno; } } }
package org.apache.lucene.util; import java.lang.ref.*; import java.util.*; /** * memory cache. maintains blockno to data block mapping, with a maximum size * for the entire cache. Cache data is held using soft references, so the cache * size may be less than the maximum under low memory conditions. */ public class MemoryLRUCache { private Map cache; private long maxsize; private long cachesize; private ReferenceQueue refq = new ReferenceQueue(); private ReadWriteLock lock = new ReadWriteLock(); private int interval = 0; /** * if TRUE, enforce cache size, else cache is limited via available memory, but will never cause * OOM since the references are held with "soft" references */ private static final boolean LIMITSIZE = false; /** * number of operations before ReferenceQueue is polled */ private static final int INTERVAL = 10000; public MemoryLRUCache(long size) { maxsize = size; if(LIMITSIZE){ cache = new LinkedHashMap(10000, .75F, true) { public boolean removeEldestEntry(Map.Entry eldest) { if (cachesize > maxsize) { CacheEntry ce = (CacheEntry) eldest.getValue(); cachesize -= ce.size; return true; } return false; } }; } else { cache = new HashMap(10000, .75F); } } public byte[] get(Object key) { pollQueue(); CacheEntry ce = null; lock.readLock(); try { ce = (CacheEntry) cache.get(key); } finally { lock.readUnlock(); } return ce != null ? ce.getData() : null; } private void pollQueue() { if(++interval>INTERVAL) { lock.writeLock(); try { CacheEntry ce = null; while ((ce = (CacheEntry) refq.poll()) != null) { remove(ce.key); } } finally { lock.writeUnlock(); } interval=0; } } public Object remove(Object key) { lock.writeLock(); try { CacheEntry ce = (CacheEntry) cache.remove(key); if (ce != null) { cachesize -= ce.size; return ce.getData(); } return null; } finally { lock.writeUnlock(); } } public void put(Object key, byte[] data) { lock.writeLock(); try { cachesize += data.length; CacheEntry ce = new CacheEntry(key, data, refq); CacheEntry old = (CacheEntry) cache.put(key, ce); if (old != null) cachesize -= old.size; if(LIMITSIZE) { if (cachesize > maxsize) { for (Iterator i = cache.values().iterator(); i.hasNext() && cachesize > maxsize;) { CacheEntry ce0 = (CacheEntry) i.next(); i.remove(); cachesize -= ce0.size; } } } pollQueue(); } finally { lock.writeUnlock(); } } public void clear() { lock.writeLock(); try { cache.clear(); cachesize = 0; } finally { lock.writeUnlock(); } } public int size() { lock.readLock(); try { return cache.size(); } finally { lock.readUnlock(); } } public long maxmem() { return maxsize; } public long memused() { return cachesize; } public boolean containsKey(Object key) { lock.readLock(); try { return cache.containsKey(key); } finally { lock.readUnlock(); } } class CacheEntry extends SoftReference { int size; Object key; CacheEntry(Object key, byte[] data, ReferenceQueue queue) { super(data, queue); size = data.length; this.key = key; } byte[] getData() { return (byte[]) get(); } } }
package org.apache.lucene.util; import java.io.*; import java.util.Random; import junit.framework.TestCase; /** * test NioFile performance using InputStream */ public class TestNioFilePerfShared extends TestNioFilePerf { private RandomAccessFile f=null; int count=0; protected void doNonCachedImpl() throws Exception { synchronized(this) { if(f==null) f = new RandomAccessFile(FILENAME,"r"); count++; } byte[] block = new byte[BLOCKSIZE]; Random r = new Random(); for(int i=0;i<NREADS;i++) { long pos = BLOCKSIZE*(long)r.nextInt(NBLOCKS); synchronized(f) { f.seek(pos); f.readFully(block); } } if(--count==0) f.close(); } private NioFile nio; protected void doCachedImpl() throws Exception { synchronized(this) { if(nio==null) nio = new NioFile(new File(FILENAME),"r"); count++; } byte[] block = new byte[BLOCKSIZE]; Random r = new Random(); for(int i=0;i<NREADS;i++) { long pos = BLOCKSIZE*(long)r.nextInt(NBLOCKS); nio.read(block,0,BLOCKSIZE,pos); } if(--count==0) nio.close(); } }
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]