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

时间:2018-06-25 09:49:35

标签: java performance inputstream

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

我实现了一个DataDeserializer,它使用InputStream并将其完全反序列化。它的工作方式是使用小型缓冲区(通常少于100个字节)执行多个inputStream.read(buffer)调用。为了获得更好的整体性能,我还在这里尝试了不同的实现。一项更改确实显着提高了此类的性能(我现在使用ByteBuffer来读取基本类型,而不是通过字节移位手动进行操作),但与网络流结合使用时,不会出现差异。有关更多详细信息,请参见以下部分。

我的问题的简要摘要

即使我证明网络和反序列化器本身都很快,但从网络流中反序列化花费的时间太长。我可以尝试一些常见的性能技巧吗?我已经用BufferedInputStream包装了网络流。另外,我尝试了双缓冲并取得了一些成功(请参见下面的代码)。任何获得更好性能的解决方案都是值得欢迎的。


性能测试方案

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

首先,我想知道InputStreamHttpsURLConnection的读取速度。包裹到BufferedInputStream中后,花费了26.250s的时间将整个数据写入ByteArrayOutputStream 1

然后,我测试了解串器的性能,并将其全部174 MB作为ByteArrayInputStream传递给了它。在我改进解串器的实现之前,它花费了38.151s。改进后只花了23466秒。 2 我想就是这样,但是……。

我实际上想做的 是将connection.getInputStream()传递给解串器。奇怪的是:在反序列化器改进之前,反序列化花费了61.413s,而在改进之后是60.100s! 3

那怎么会发生?尽管解串器明显改善,但这里几乎没有任何改善。另外,与该改进无关,我感到惊讶的是,这花费的时间比总结出来的单独性能要长(60.100> 26.250 + 23.466)。为什么?不要误会我的意思,我没想到这会是最好的解决方案,但是我也没想到它会那么糟糕。

因此,需要注意三件事:

  1. 整体速度受网络约束,至少需要26.250s。也许我可以调整一些http设置,或者可以进一步优化服务器,但是现在这可能不是我应该关注的重点。
  2. 我的解串器实现很可能还不是很完美,但是就其本身而言,它比网络要快,因此我认为没有必要进一步改进它。
  3. 基于1和2。我假设以某种方式可以组合起来完成整个工作(从网络中读取+反序列化),这应该花费不多的时间。比26.250s。欢迎提供任何有关实现此目标的建议。

我正在寻找某种双缓冲,允许两个线程从中读取并并行写入。 标准Java中有类似的东西吗?最好是从InputStream继承的某些类允许并行写入?如果有类似的东西,但不是继承自InputStream,我也许可以将我的DataDeserializer更改为也可以从那个继承。

由于我没有找到任何这样的DoubleBufferInputStream,因此我自己实现了它。 该代码很长,可能并不完美,我不想打扰您为我做代码审查。它具有两个16kB缓冲区。使用它,我能够将整体性能提高到39.885s。 4 这比60.100s好得多,但比26.250s还差很多。选择不同的缓冲区大小并没有太大变化。因此,我希望有人可以引导我实现一些好的双缓冲区实现。


测试代码

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.466s)

InputStream inputStream = new ByteArrayInputStream(entire174MBbuffer);
DataDeserializer deserializer = new DataDeserializer(inputStream);

long start = System.nanoTime();
deserializer.Deserialize();
long end = System.nanoTime();

3(60.100s)

InputStream inputStream = new BufferedInputStream(connection.getInputStream());
DataDeserializer deserializer = new DataDeserializer(inputStream);

long start = System.nanoTime();
deserializer.Deserialize();
long end = System.nanoTime();

4(39.885s)

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.756s,而写入ByteArrayOutputStream则仅需要0.817s。这是有道理的,因为这两个数字几乎可以完美地合计为先前测得的26.250s(加上一些额外的测量开销)。

我以相同的方式修改了代码片段#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.096s。同样,这些数字几乎完美地总结为代码片段#4的测量时间。 另外,我使用Java VisualVM剖析了相同的代码。这说明我在该线程的run()方法中花费了40s,而这40s中的100%是CPU时间。另一方面,它在解串器内部也要花费40秒,但是这里CPU时间只有26秒,而等待时间只有14秒。这与从网络读取到ByteBufferOutputStream的时间完全匹配。因此,我想我必须改进双缓冲区的“缓冲区切换算法”。

*)这个奇怪的发现有什么解释吗?我只能想象这种测量方法非常不准确。但是,最新测量值的读取和写入时间可以完美地总结为原始测量值,因此, 可能不准确...有人可以对此进行说明吗? 我无法在事件探查器中找到这些读写性能。我将尝试找到一些设置,使我能够观察这两种方法的分析结果。

2 个答案:

答案 0 :(得分:0)

改进其中任何一种的最确定的方法是更改​​

connection.getInputStream()

new BufferedInputStream(connection.getInputStream())

如果这没有帮助,则输入流不是您的问题。

答案 1 :(得分:0)

显然,我的“错误”是使用32位JVM(精确的是jre1.8.0_172)。 在64位版本的JVM和tadaaa上运行完全相同的代码段,...速度很快并且在这里很有意义。

特别是有关相应代码段的这些新数字:

  • 片段1: 4.667s (相对于26.250s)
  • 片段2: 11.568s (相对于23.466s)
  • 片段3: 17.185s (相对于60.100s)
  • 片段4: 12.336s (与39.885s相比)

因此,显然,对Does Java 64 bit perform better than the 32-bit version?的回答不再正确。或者,此特定的32位JRE版本中存在严重的错误。我还没有测试其他人。

如您所见,#4仅比#2慢一点,这完全符合我最初的假设

  

基于1.和2。我假设应该以某种方式   以组合的方式完成整个工作(从网络中读取+   反序列化)的时间不应超过26.250s。

我的问题的更新2 中描述的我的分析方法的非常奇怪的结果也不再发生。我还没有在64位中重复执行每个测试,但现在我 did 所做的所有性能分析结果似乎都是合理的,即,无论使用哪个代码段,相同的代码都会花费相同的时间。所以也许这确实是一个错误,或者有人有合理的解释吗?