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]

Reply via email to