【问题标题】:How to improve performance of deserializing objects from HttpsURLConnection.getInputStream()?如何提高从 HttpsURLConnection.getInputStream() 反序列化对象的性能?
【发布时间】:2018-06-25 09:49:35
【问题描述】:

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

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

我的问题的快速总结

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


性能测试场景

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

首先我想知道,HttpsURLConnection 中的InputStream 的读取速度有多快。包装成BufferedInputStream,将整个数据写入ByteArrayOutputStream需要26.250s。1

然后我测试了我的解串器的性能,将 174 MB 作为ByteArrayInputStream 传递给它。在我改进解串器的实现之前,花了 38.151 秒。改进后只用了 23.466s。2 所以这就是它,我想......但不是。

我真正想做的,不知何故,将connection.getInputStream() 传递给反序列化器。奇怪的事情来了:在反序列化器改进之前,反序列化需要 61.413 秒,改进之后是 60.100 秒!3

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

所以,需要注意三件事:

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

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

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


测试代码

1 (26.250s)

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

我稍微修改了我的代码 sn-p #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 秒,而写入 ByteArrayOutputStream 只需要 0.817 秒。这是有道理的,因为这两个数字几乎完美地总结了之前测量的 26.250 秒(加上一些额外的测量开销)。

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

*) 这个奇怪的观察有什么解释吗?我只能想象这种测量方式是非常不准确的。但是,最新测量的读取和写入时间完美地总结了原始测量,因此它不能那个不准确......有人可以解释一下吗? 我无法在分析器中找到这些读写性能...我将尝试找到一些设置,以便我观察这两种方法的分析结果。

【问题讨论】:

  • 您是否尝试过调整缓冲区大小? BufferedInputStream(InputStream in, int size) 你的解串器可能太低了?也许你也可以分享那个反序列化器。
  • @GotoFinal 啊,我不知道那个构造函数。我会试试。谢谢!
  • @GotoFinal 我尝试了 16kB、64kB 和 256kB。完全没有变化:( sn-p 3 仍然需要约 60 秒,而 sn-p 4 仍然需要约 40 秒。我将尝试将我的反序列化器代码归结为它的核心并发布它。总的来说,它包含超过 300 行几个不感兴趣的自定义对象反序列化。
  • 在 TC #2 中,您已经通过了整个缓冲区,但是,您正在尝试使用小尺寸的 TC #3...
  • @mnmopazem 我知道。这就是我试图用双缓冲方法来弥补的,实际上取得了一些成功。但进一步增加双缓冲区的缓冲区大小并没有给我带来比约 40 秒更好的结果。

标签: java performance inputstream


【解决方案1】:

显然,我的“错误”是使用 32 位 JVM(准确地说是 jre1.8.0_172)。 在 64 位版本的 JVM 上运行完全相同的代码 sn-ps,然后 tadaaa... 它速度很快,而且在那儿很有意义。

特别是看到这些新数字对应的代码sn-ps:

  • sn-p #1:4.667s(与 26.250s 相比)
  • sn-p #2:11.568s(相对于 23.466s)
  • sn-p #3:17.185s(与 60.100s 相比)
  • sn-p #4:12.336s(对比 39.885s)

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

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

基于 1. 和 2. 我假设应该有可能 以组合的方式完成整个工作(从网络读取 + 反序列化),这应该不会超过 26.250 秒。

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

【讨论】:

  • 似乎接受的答案仍然是正确的 - “它取决于”。这里没有任何改变。
【解决方案2】:

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

connection.getInputStream()

new BufferedInputStream(connection.getInputStream())

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

【讨论】:

  • 好吧,正如您在我的代码 sn-ps 中看到的那样,我已经在任何地方都这样做了(除了第 2 点,但是将 ByteArrayInputStream 缓冲为它的缓冲区最终已经很棒了:))。我得出了同样的结论:输入流不是我的问题。
猜你喜欢
  • 2015-02-16
  • 1970-01-01
  • 2020-05-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多