快速序列化的性能下降

时间:2015-06-19 01:48:50

标签: java performance serialization

这个问题特别适用于快速序列化库。 https://github.com/RuedigerMoeller/fast-serialization

我在Windows 7上使用FSTLongOffheapMap(版本2.29),jdk 1.7来存储一些对象。我测试了存储物体的延迟,99.99百分位数达到约100微米。这是优秀,考虑到我在Windows上运行它+没有编写自定义序列化器+没有太多调整GC。

但是,如果按照以下方式更改设置,性能会急剧下降:

a)创建一个生产者线程和一个由ArrayBlockingQueue管道的消费者线程。

b)让生产者生产50,000个对象,并尽快将它们填入队列。

c)使用者线程拉出对象,将其分派给监听器,然后将其存储在堆外映射中。

在此设置中,第99.99百分位延迟达到 ~135毫秒! 但最大的惊喜是,如果我只是评论执行放置的线路,延迟下降到~400微秒。这对我来说似乎不合逻辑,因为我从之前的测试中得知,地图中放置的延迟仅为100微秒。

以下是重现我的发现的测试,我将不胜感激任何建议/提示/想法。运行测试所需的唯一外部库是Gil Tene的HdrHistogram。 https://github.com/HdrHistogram/HdrHistogram

- 非常感谢

package com.mine.serialization.perf;

import java.io.*;
import java.util.*;
import org.nustaq.offheap.*;
import org.nustaq.serialization.simpleapi.*;

public final class MyFSTSerializer{

    private final boolean toStore;
    private final String fileName;
    private final long memorySize;
    private final FSTCoder fastCoder;
    private final FSTLongOffheapMap<MktDataEvent> offHeapMap;

    public MyFSTSerializer(  boolean toStore, String location, String journalName, FSTCoder fastCoder, long memorySize, int count ) throws Exception{
        this.toStore        = toStore;
        this.fileName       = location + File.separator + journalName + ".mmf";
        this.memorySize     = memorySize;
        this.fastCoder      = fastCoder;
        this.offHeapMap     = new FSTLongOffheapMap<>( fileName, memorySize, count, fastCoder );
    }

    public final boolean toStore( ){
        return toStore;
    }

    public final String getFilename( ){
        return fileName;
    }

    public final void start( ){
        fastCoder.getConf().setCrossPlatform( false );
        fastCoder.getConf().setPreferSpeed( true );
        fastCoder.getConf().setShareReferences( false );
        fastCoder.getConf().registerClass( Long.class, MktDataEvent.class );    
        System.out.println("Journaling started at " + fileName + " with Memory " +  memorySize ) ;
    }

    public final void storeEvent( MktDataEvent event ){
        offHeapMap.put( event.getSequenceId(), event );
    }

    public final Collection<MktDataEvent> retrieveAllEvents( ){
        Map<Long, MktDataEvent> retrievedMap = new LinkedHashMap<>();

        for( Iterator<MktDataEvent> iterator = offHeapMap.values(); iterator.hasNext(); ){
            MktDataEvent event = (MktDataEvent) iterator.next();
            retrievedMap.put( event.getSequenceId(), event ); 
         }

        return retrievedMap.values();
    }

    public final void stop( ){
        try{
            offHeapMap.free( );
            System.out.println("Stopped Journal and freed memory." );
        }catch( Exception e ){
            e.printStackTrace( );
        }
    }
}
package com.mine.serialization.perf;

import java.io.Serializable;
import java.util.concurrent.atomic.AtomicLong;

public final class MktDataEvent implements Serializable{

    private final long sequenceId;
    private final long creationTime;
    private final String symbol;
    private final double bidPrice;
    private final long bidQuantity;
    private final double askPrice;
    private final long askQuantity;

    private final static long serialVersionUID = 1L;
    private final static AtomicLong SEQUENCE    = new AtomicLong();

    public MktDataEvent( String symbol, double bidPrice, long bidQuantity, double askPrice, long askQuantity ){

        this.creationTime   = System.nanoTime( );
        this.sequenceId     = SEQUENCE.incrementAndGet();
        this.symbol         = symbol;
        this.bidPrice       = bidPrice;
        this.bidQuantity    = bidQuantity;
        this.askPrice       = askPrice;
        this.askQuantity    = askQuantity;
    }       

       public final long getSequenceId( ){
        return sequenceId;
    }

    public final long getCreationTime( ){
        return creationTime;
    }

    public final String getSymbol(){
        return symbol;
    }

    public final double getBidPrice( ){
        return bidPrice;
    }

    public final long getBidQuantity( ){
        return bidQuantity;
    }

    public final double getAskPrice( ){
        return askPrice;
    }

    public final long getAskQuantity( ){
        return askQuantity;
    }
}

// --------------------------------------------- -------------------

package com.mine.serialization.perf;

import java.util.*;
import java.util.concurrent.*;

public final class MktDataDispatcher implements Runnable{

    private volatile boolean keepDispatching;
    private final ExecutorService service;
    private final MyFSTSerializer serializer;
    private final MktDataListener listener;
    private final AbstractQueue<MktDataEvent> eventQueue;

    public MktDataDispatcher( int queueSize, MyFSTSerializer serializer, MktDataListener listener ){
        this.serializer     = serializer;
        this.listener       = listener;
        this.eventQueue = new ArrayBlockingQueue<MktDataEvent>( queueSize );
        this.service        = Executors.newFixedThreadPool(  1 );
    }

    public final void start( ){
        serializer.start( );
        keepDispatching = true;
        service.execute( this );
    }

    public final boolean enqueue( final MktDataEvent event ){
        return eventQueue.offer( event );
    }

    @Override
    public final void run( ){

        while( keepDispatching ){

            try{
                MktDataEvent event  = eventQueue.poll();
                if( event == null ){
                    Thread.yield();
                    continue;
                }

                if( serializer.toStore() ){
                    serializer.storeEvent( event );
                }
                listener.update( event );

            }catch( Exception e ){
                e.printStackTrace( );                   
            }
            }
        }

    protected final int getQueueSize( ){
        return eventQueue.size( );
    }

    public final void stop(){
        serializer.stop( );
        keepDispatching = false;
        service.shutdown();
    }

    public interface MktDataListener{
        public boolean update( MktDataEvent event );
    }

}
package com.mine.serialization.perf;

import java.io.*;
import java.util.concurrent.*;

import org.HdrHistogram.*;
import org.nustaq.serialization.simpleapi.*;

import com.mine.serialization.perf.MktDataDispatcher.*;



public final class TestFSTSerializer{


    protected static void printResult( Histogram histogram ){
        System.out.println( "\nDetail Result (in micros)");
        System.out.println( "------------------------------------------------------------------");

        histogram.outputPercentileDistribution( System.out, 1000.0 );
        double valueAt99Percentile  = histogram.getValueAtPercentile( 99.99d );

        System.out.println( "\nValue 99.99th percentile >> " + valueAt99Percentile/1000.0 );
    }


    protected static MyFSTSerializer createFSTSerializer(  boolean toStore, int eventCount, int memorySizeOf1Object ) throws Exception{

        long expectedMemory     = memorySizeOf1Object * eventCount;
        String fileLocation     = "C:\\Temp";
        String journalName      = "Test";
        MyFSTSerializer ser     = new MyFSTSerializer( toStore, fileLocation, journalName, new DefaultCoder(), expectedMemory, eventCount );

        return ser;
    }


    protected static void destroyFSTSerializer( MyFSTSerializer serializer ){

        if( serializer != null ){
            serializer.stop();
            boolean deleted = new File( serializer.getFilename() ).delete();
            if( deleted ){
                System.out.println( "Deleted file from " +  serializer.getFilename());      
            }else{
                throw new RuntimeException( "TEST FAILED as we failed to delete file " + serializer.getFilename() );
            }
        }

    }


    public static void testOffHeapPersistence( ){

        MyFSTSerializer serializer= null;

        try{

            int eventCount          = 50000;
            int memorySizeOf1Object = 1000;
            Histogram  histogram    = new Histogram( TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS), 2);

            System.out.println( "Testing off heap persistence performance of FSTLongOffheapMap by storing " + eventCount + " events.");
            serializer              = createFSTSerializer( true, eventCount, memorySizeOf1Object );
            serializer.start( );

            for( int i =0; i<eventCount; i++ ){

                MktDataEvent event = new MktDataEvent( "EDM6", 99.0, (100 + i), 99.50, (200 + i) );
                serializer.storeEvent( event );
                histogram.recordValue(System.nanoTime() - event.getCreationTime() );

            }

            int retrievedEventSize  = serializer.retrieveAllEvents().size();
            if( eventCount != retrievedEventSize )
                throw new RuntimeException("Store failed as we stored " + eventCount + " events but retrieved " + retrievedEventSize );


            printResult( histogram );

        }catch( Exception e ){
            throw new RuntimeException("TEST FAILED as ", e);

        }finally{
            destroyFSTSerializer( serializer );
        }

    }



    public static void testDispatchAndPersistence( boolean toStore ) throws Exception{

        int eventCount                  = 50000;
        int memorySizeOf1Object         = 1000;

        DummyListener listener          = new DummyListener( );
        MyFSTSerializer serializer      = createFSTSerializer( toStore, eventCount, memorySizeOf1Object );
        MktDataDispatcher dispatcher    = new MktDataDispatcher( eventCount, serializer, listener );

        if( toStore ){
            System.out.println( "Testing off heap persistence with dispathcer performance of FSTLongOffheapMap by storing " + eventCount + " events."); 
        }else{
            System.out.println( "Testing off heap persistence with dispathcer performance of FSTLongOffheapMap WITHOUT storing " + eventCount + " events.");
        }

        dispatcher.start();
        System.gc();
        Thread.sleep( 3000 );

        for( int i = 0; i< eventCount; i++ ){
            MktDataEvent event = new MktDataEvent( "EDM6", 99.0, (100 + i), 99.50, (200 + i) );
            dispatcher.enqueue( event );
        }

        //Let the listener get all the elements
        while( (dispatcher.getQueueSize() != 0) ){
            Thread.yield();
        }

        Thread.sleep( 2000 );
        dispatcher.stop();
        listener.generateLatencyStats();
        destroyFSTSerializer( serializer );

    }


    public static class DummyListener implements MktDataListener{

        private final Histogram histogram;

        public DummyListener( ){
            this.histogram  = new Histogram( TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS), 2);
        }


        @Override
        public final boolean update( MktDataEvent event ){
            histogram.recordValue( (System.nanoTime() - event.getCreationTime()) );
            return true;
        }


        public final void generateLatencyStats( ){

            histogram.outputPercentileDistribution( System.out, 1000.0 );
            double valueAt99Percentile  = histogram.getValueAtPercentile( 99.99d );
            System.out.println( "\nValue at 99.99th percentile (micros) >> " + valueAt99Percentile/1000.0 );

        }

    }

    public static void main( String ... args ) throws Exception{

        testOffHeapPersistence( );

        System.gc();
        Thread.sleep( 2000 );
        testDispatchAndPersistence( false );

        System.gc();
        Thread.sleep( 2000 );
        testDispatchAndPersistence( true );

    }

}

2 个答案:

答案 0 :(得分:1)

您好(我是FST的作者):

我认为测试存在缺陷:

当在循环中运行同步(没有队列+线程上下文切换)测试时(=正确的预热)我得到 0.7 micros 的平均值和 14个微处理器的最大异常值(在地图中加倍数量的元素)存储单个事件   这是FST的性能,您看到的丢失和延迟是由排队/线程上下文切换引起的。此外,测试有一个缺陷:

在事件创建过程中,需要花费一些时间将一连串50k事件放入队列中。由于put事件比存储事件要快得多,你会得到积累:N'事件得到所有0..n-1事件积累的延迟;)。

由于缺少JVM预热,第一次运行似乎很好:事件创建速度慢,因此事件不会排队。

其他问题:

1)主要:没有WARMUP。在查看数字之前,先进行循环并让测试运行几次(如10)。

2)(次要)通过优惠进行入场而不检查结果

3)如果没有事件可用,则轮询队列进行收益,这可能导致不确定的延迟峰值。只有在退避后才能收益。

你应该在每个〜1-2微秒的时间内将一个事件放入队列中,以避免排队事件并以这种方式测量聚合时间。

将TestFSTSerializer更改为:

    for( int i = 0; i< eventCount; i++ ){
        MktDataEvent event = new MktDataEvent( "EDM6", 99.0, (100 + i), 99.50, (200 + i) );
        dispatcher.enqueue( event );
        long nanos = System.nanoTime();
        while( System.nanoTime() - nanos < 3000 )
            Thread.yield();

    }

和main方法(预热,忽略第一次运行):

public static void main( String ... args ) throws Exception{

    for (int i = 0; i < 1000; i++) {
        System.gc();
        Thread.sleep( 2000 );
        System.out.println("start test ==>");
        testDispatchAndPersistence( true );
        //testOffHeapPersistence();
    }

的产率:

[平均值= 5.19,StdDeviation = 29.67]

[最大= 544.77,总计数= 50000]

[Buckets = 23,SubBuckets = 256]

请注意,线程上下文切换花费您3-8微秒(因此高端内核绕过网络的速度几乎与线程之间的排队速度相当!!)。
您可以尝试使用比java.concurrent更快的队列来进一步减少延迟。

注意以后的测试:由于持久性依赖于操作系统的回写急切,您需要调整操作系统设置以非常急切地回写和/或使用SSD。

请参阅https://github.com/RuedigerMoeller/fast-serialization/tree/master/src/test/ser/offheaplatency了解修改后的来源

答案 1 :(得分:0)

我用来提高地图性能的一个技巧是为它们提供初始容量。
我的意思是你的LinkedHashMap应该初始化,其容量大约是事件数量级 你希望在检索之前推进。