代码之家  ›  专栏  ›  技术社区  ›  sebrockm

如何提高从httpsurlConnection.getinputstream()反序列化对象的性能?

  •  0
  • sebrockm  · 技术社区  · 6 年前

    我有一个客户机-服务器应用程序,其中服务器向客户机发送一些二进制数据,客户机必须根据自定义二进制格式从该字节流反序列化对象。数据通过HTTPS连接发送,客户端使用 HttpsURLConnection.getInputStream() 阅读它。

    我实施了 DataDeserializer 这需要 InputStream 并完全反序列化。它的工作方式是执行多个 inputStream.read(buffer) 使用小缓冲区(通常小于100字节)调用。在实现更好的总体性能的过程中,我在这里也尝试了不同的实现。有一项改变确实显著提高了这门课的成绩(我用的是 ByteBuffer 现在要读取基元类型而不是手动进行字节移位),但是结合网络流,不会出现任何差异。有关详细信息,请参阅下面的部分。

    我问题的快速总结

    从网络流进行反序列化花费的时间太长,即使我证明了网络和反序列化程序本身的速度很快。我可以尝试一些常见的表演技巧吗?我已经用 BufferedInputStream .另外,我尝试了一些成功的双重缓冲(见下面的代码)。欢迎使用任何能提高性能的解决方案。


    性能测试场景

    在我的测试场景中,服务器和客户机位于同一台机器上,服务器发送大约174MB的数据。代码片段可以在本文末尾找到。这里看到的所有数字都是5次测试运行的平均值。

    首先我想知道,那有多快 输入流 HttpsURLConnection 可以读取。包装成 缓冲输入流 将整个数据写入 ByteArrayOutputStream . 1个

    然后我测试了反序列化程序的性能,将174MB作为 ByteArrayInputStream .在我改进反序列化程序的实现之前,它花了38.151秒,改进后只花了23.466秒。 所以我想这就是它了…但没有。

    我真正想做的, 以某种方式 ,正在通过 connection.getInputStream() 到反序列化程序。奇怪的是:在反序列化程序改进之前,反序列化花费了61.413秒,改进之后是60.100秒!

    怎么会这样?尽管反序列化程序显著改进,但这里几乎没有改进。此外,与改进无关,我惊讶地发现,这比单独的性能总和(60.100>26.250+23.466)所花费的时间更长。为什么?别误会我,我没想到这是最好的解决办法,但我也没想到会那么糟糕。

    所以,有三件事要注意:

    1. 总体速度受网络限制,至少需要26.250秒。可能有一些HTTP设置我可以调整,或者我可以进一步优化服务器,但目前这可能不是我应该关注的。
    2. 我的反序列化程序实现很可能仍然不完美,但它本身比网络更快,所以我认为没有必要进一步改进它。
    3. 基于1。和2.我想应该是 以某种方式 可以以组合方式完成整个工作(从网络读取+反序列化),所需时间不应超过26.250秒。欢迎就如何实现这一点提出任何建议。

    我在寻找某种双缓冲区,允许两个线程从中读取并并行写入。 标准Java中有类似的东西吗?最好是继承自 输入流 允许并行写入?如果有相似的东西,但不是继承自 输入流 ,我可以改变我的 数据反序列化程序 也可以从中消费。

    因为我没有找到这样的 DoubleBufferInputStream ,我自己实现了。 代码很长,可能不完美,我不想打扰您为我做代码检查。它有两个16KB的缓冲区。使用它,我可以将整体性能提高到39.885秒。 4 这比60.100秒要好得多,但比26.250秒差得多,选择不同的缓冲区大小变化不大。所以,我希望有人能引导我实现一些好的双缓冲区。


    测试代码

    1(26.250秒)

    InputStream inputStream = new BufferedInputStream(connection.getInputStream());
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    
    byte[] buffer = new byte[16 * 1024];
    int count = 0;
    
    long start = System.nanoTime();
    while ((count = inputStream.read(buffer)) >= 0) {
        outputStream .write(buffer, 0, count);
    }
    long end = System.nanoTime();
    

    2(23.466秒)

    InputStream inputStream = new ByteArrayInputStream(entire174MBbuffer);
    DataDeserializer deserializer = new DataDeserializer(inputStream);
    
    long start = System.nanoTime();
    deserializer.Deserialize();
    long end = System.nanoTime();
    

    3(60.100秒)

    InputStream inputStream = new BufferedInputStream(connection.getInputStream());
    DataDeserializer deserializer = new DataDeserializer(inputStream);
    
    long start = System.nanoTime();
    deserializer.Deserialize();
    long end = System.nanoTime();
    

    4(39.885秒)

    MyDoubleBufferInputStream doubleBufferInputStream = new MyDoubleBufferInputStream();
    
    new Thread(new Runnable() {
    
        @Override
        public void run() {
    
            try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) {
                byte[] buffer = new byte[16 * 1024];
                int count = 0;
                while ((count = inputStream.read(buffer)) >= 0) {
                    doubleBufferInputStream.write(buffer, 0, count);
                }
            } catch (IOException e) {
            } finally {
                doubleBufferInputStream.closeWriting(); // read() may return -1 now
            }
        }
    
    }).start();
    
    DataDeserializer deserializer = new DataDeserializer(doubleBufferInputStream);
    long start = System.nanoTime();
    deserializer.deserialize();
    long end = System.nanoTime();
    

    更新

    根据要求,这里是我的反序列化程序的核心。我认为最重要的方法是 prepareForRead() 它执行流的实际读取。

    class DataDeserializer {
        private InputStream _stream;
        private ByteBuffer _buffer;
    
        public DataDeserializer(InputStream stream) {
            _stream = stream;
            _buffer = ByteBuffer.allocate(256 * 1024);
            _buffer.order(ByteOrder.LITTLE_ENDIAN);
            _buffer.flip();
        }
    
        private int readInt() throws IOException {
            prepareForRead(4);
            return _buffer.getInt();
        }
        private long readLong() throws IOException {
            prepareForRead(8);
            return _buffer.getLong();
        }
        private CustomObject readCustomObject() throws IOException {
            prepareForRead(/*size of CustomObject*/);
            int customMember1 = _buffer.getInt();
            long customMember2 = _buffer.getLong();
            // ...
            return new CustomObject(customMember1, customMember2, ...);
        }
        // several other built-in and custom object read methods
    
        private void prepareForRead(int count) throws IOException {
            while (_buffer.remaining() < count) {
                if (_buffer.capacity() - _buffer.limit() < count) {
                    _buffer.compact();
                    _buffer.flip();
                }
    
                int read = _stream.read(_buffer.array(), _buffer.limit(), _buffer.capacity() - _buffer.limit());
                if (read < 0)
                    throw new EOFException("Unexpected end of stream.");
    
                _buffer.limit(_buffer.limit() + read);
            }
        }
    
        public HugeCustomObject Deserialize() throws IOException {
            while (...) {
                // call several of the above methods
            }
            return new HugeCustomObject(/* deserialized members */);
        }
    }
    

    更新2

    我稍微修改了一下代码片段1,以便更准确地了解时间花在哪里:

    InputStream inputStream = new BufferedInputStream(connection.getInputStream());
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    byte[] buffer = new byte[16 * 1024];
    
    long read = 0;
    long write = 0;
    while (true) {
        long t1 = System.nanoTime();
        int count = istream.read(buffer);
        long t2 = System.nanoTime();
        read += t2 - t1;
        if (count < 0)
            break;
        t1 = System.nanoTime();
        ostream.write(buffer, 0, count);
        t2 = System.nanoTime();
        write += t2 - t1;
    }
    System.out.println(read + " " + write);
    

    这告诉我,从网络流中读取需要25.756秒,而写入到 按earrayOutputstream 只需要0.817秒,这是有意义的,因为这两个数字几乎完美地加起来就是先前测量的26.250秒(加上一些额外的测量开销)。

    以同样的方式,我修改了代码片段4:

    MyDoubleBufferInputStream doubleBufferInputStream = new MyDoubleBufferInputStream();
    
    new Thread(new Runnable() {
    
        @Override
        public void run() {
            try (InputStream inputStream = new BufferedInputStream(httpChannelOutputStream.getConnection().getInputStream(), 256 * 1024)) {
                byte[] buffer = new byte[16 * 1024];
    
                long read = 0;
                long write = 0;
                while (true) {
                    long t1 = System.nanoTime();
                    int count = inputStream.read(buffer);
                    long t2 = System.nanoTime();
                    read += t2 - t1;
                    if (count < 0)
                        break;
                    t1 = System.nanoTime();
                    doubleBufferInputStream.write(buffer, 0, count);
                    t2 = System.nanoTime();
                    write += t2 - t1;
                }
                System.out.println(read + " " + write);
            } catch (IOException e) {
            } finally {
                doubleBufferInputStream.closeWriting();
            }
        }
    
    }).start();
    
    DataDeserializer deserializer = new DataDeserializer(doubleBufferInputStream);
    deserializer.deserialize();
    

    现在我希望测量的读取时间与前面的示例中的完全相同。但是,相反, read 变量的值为39.294s( 这怎么可能??与前一个25.756s的示例中测量的代码完全相同! ) * 当写入我的双缓冲区时,只需要0.096秒。同样,这些数字几乎完美地加起来就是代码段4的测量时间。 另外,我使用Java VisualVM对这个代码进行了剖析。这告诉我这条线花了40秒 run() 方法和这40秒中的100%是CPU时间。另一方面,它在反序列化程序内部也花费40秒,但这里只有26秒是CPU时间,14秒是等待时间。这与从网络读取到 ByteBufferOutputStream .所以我想我必须改进我的双缓冲器的“缓冲器切换算法”。

    *)对这种奇怪的观察有什么解释吗?我只能想象这种测量方法是非常不准确的。但是,最新测量的读写时间与原始测量值完全相同,因此不能 那个 不准确的。。。有人能帮我解释一下吗? 我在档案中找不到这些读写性能…我将尝试找到一些允许我观察这两个方法的分析结果的设置。

    2 回复  |  直到 6 年前
        1
  •  0
  •   user207421    6 年前

    改善这些问题的最确定方法是改变

    connection.getInputStream()
    

    new BufferedInputStream(connection.getInputStream())
    

    如果这不起作用,那么输入流不是您的问题。

        2
  •  0
  •   sebrockm    6 年前

    显然,我的“错误”是使用32位的JVM(JRE1.8.0 U172是精确的)。 正在运行 非常相同的代码片段 在64位版本的JVM和TADAA上…它很快而且很有意义。

    具体来说,请参见这些新数字以了解相应的代码段:

    • 代码段1: 4.667秒 (与26.250秒相比)
    • 代码段2: 11.568秒 (与23.466相比)
    • 代码段3: 17.185秒 (与60.100秒相比)
    • 代码段4: 12.336秒 (与39.885相比)

    很明显,答案是 Does Java 64 bit perform better than the 32-bit version? 不再是真的了。或者,在这个特定的32位JRE版本中有一个严重的错误。我还没有测试其他人。

    如你所见,4只比2稍慢一点,这完全符合我最初的假设

    基于1。和2.我想可能 以组合方式完成整个工作(从网络中读取+ 反序列化)。

    此外,我的分析方法的非常奇怪的结果在 更新2 我的问题不会再发生了。我还没有在64位中重复每一个测试,但是所有的分析结果 Do现在似乎是合理的,即无论代码片段在哪个代码片段中,相同的代码都需要相同的时间。所以也许这真的是个错误,或者有人有合理的解释吗?