如何提高从 HttpsURLConnection.getInputStream() 反序列化对象的性能?
How to improve performance of deserializing objects from HttpsURLConnection.getInputStream()?
我有一个客户端-服务器应用程序,其中服务器向客户端发送一些二进制数据,客户端必须根据自定义二进制格式从该字节流中反序列化对象。数据通过 HTTPS 连接发送,客户端使用 HttpsURLConnection.getInputStream()
读取数据。
我实现了一个 DataDeserializer
,它接受一个 InputStream
并将其完全反序列化。它的工作方式是使用小缓冲区(通常小于 100 字节)执行多个 inputStream.read(buffer)
调用。在实现更好的整体性能的过程中,我也在这里尝试了不同的实现。一项更改确实显着提高了 class' 的性能(我现在使用 ByteBuffer
来读取原始类型,而不是通过字节移位手动读取),但是与网络流结合使用时没有显示出任何差异.有关详细信息,请参阅下面的部分。
我的问题的快速总结
从网络流反序列化花费的时间太长,即使我证明了网络和反序列化器本身很快。有没有我可以尝试的常见性能技巧?我已经用 BufferedInputStream
包装了网络流。另外,我尝试了双缓冲并取得了一些成功(见下面的代码)。欢迎任何实现更好性能的解决方案。
性能测试场景
在我的测试场景中,服务器和客户端位于同一台机器上,服务器发送约 174 MB 的数据。代码片段可以在这个 post 的末尾找到。您在此处看到的所有数字都是 5 次测试运行的平均值。
首先我想知道,HttpsURLConnection
中的InputStream
的读取速度有多快。包裹成一个BufferedInputStream
,将整个数据写入一个ByteArrayOutputStream
用了26.250s。1
然后我测试了我的解串器的性能,将所有 174 MB 作为 ByteArrayInputStream
传递给它。在我改进反序列化器的实现之前,它花费了 38.151s。改进后只用了23.466s2
所以这就是它,我想......但是没有。
我实际上想做的,不知何故,是将 connection.getInputStream()
传递给解串器。奇怪的事情来了:在反序列化器改进之前,反序列化花费了 61.413 秒,改进后是 60.100 秒!3
怎么会这样?尽管反序列化器有了显着改善,但这里几乎没有任何改善。此外,与该改进无关,令我惊讶的是,这比单独的性能总和 (60.100 > 26.250 + 23.466) 花费的时间更长。为什么?不要误会我的意思,我没想到这是最好的解决方案,但我也没想到它会那么糟糕。
因此,需要注意三件事:
- 整体速度受网络限制最少需要26.250s。也许有一些我可以调整的 http 设置或者我可以进一步优化服务器,但现在这可能不是我应该关注的。
- 我的解串器实现很可能仍然不完美,但它本身比网络更快,所以我认为没有必要进一步改进它。
- 基于 1. 和 2. 我假设应该以某种方式 可以以组合方式(从网络读取 + 反序列化)完成整个工作应该不会超过 26.250s。欢迎就如何实现这一目标提出任何建议。
我正在寻找某种双缓冲区,允许两个线程并行读取和写入。
标准 Java 中有类似的东西吗?最好是一些 class 继承自 InputStream
允许并行写入它?如果有类似的东西,但不是从 InputStream
继承的,我也许可以更改我的 DataDeserializer
以使用那个。
因为我还没有找到任何这样的 DoubleBufferInputStream
,所以我自己实现了它。
代码很长而且可能不完美,我不想打扰你为我做代码审查。它有两个 16kB 缓冲区。使用它我能够将整体性能提高到 39.885s。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
我稍微修改了代码片段 #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.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.096 秒。同样,这些数字几乎完美地总结了代码片段 #4 的测量时间。
此外,我使用 Java VisualVM 分析了这段完全相同的代码。这告诉我 40 秒花在了这个线程的 run()
方法上,而这 40 秒中的 100% 是 CPU 时间。另一方面,它在反序列化器内部也花费了 40 秒,但这里只有 26 秒是 CPU 时间,14 秒用于等待。这与从网络读取到 ByteBufferOutputStream
的时间完全匹配。所以我想我必须改进我的双缓冲区 "buffer-switching-algorithm"。
*) 这个奇怪的现象有什么解释吗?我只能想象这种测量方式非常不准确。但是,最新测量值的读取和写入时间完美地总结了原始测量值,因此 不可能不准确...有人可以解释一下吗?
我无法在分析器中找到这些读写性能...我会尝试找到一些设置,让我可以观察这两种方法的分析结果。
改进其中任何一项的最可靠方法是改变
connection.getInputStream()
到
new BufferedInputStream(connection.getInputStream())
如果这没有帮助,输入流不是你的问题。
显然,我的 "mistake" 是使用 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 稍慢,这完全符合我最初的假设
Based on 1. and 2. I'm assuming that it should be somehow possible to
do the entire job in a combined way (reading from the network +
deserializing) which should take not much more than 26.250s.
我的问题 更新 2 中描述的分析方法的非常奇怪的结果也不再出现。我还没有在 64 位中重复每一个测试,但是我 did 所做的所有分析结果现在都是合理的,即无论在哪个代码片段中,相同的代码都花费相同的时间。所以也许这真的是一个错误,或者有人有合理的解释吗?
我有一个客户端-服务器应用程序,其中服务器向客户端发送一些二进制数据,客户端必须根据自定义二进制格式从该字节流中反序列化对象。数据通过 HTTPS 连接发送,客户端使用 HttpsURLConnection.getInputStream()
读取数据。
我实现了一个 DataDeserializer
,它接受一个 InputStream
并将其完全反序列化。它的工作方式是使用小缓冲区(通常小于 100 字节)执行多个 inputStream.read(buffer)
调用。在实现更好的整体性能的过程中,我也在这里尝试了不同的实现。一项更改确实显着提高了 class' 的性能(我现在使用 ByteBuffer
来读取原始类型,而不是通过字节移位手动读取),但是与网络流结合使用时没有显示出任何差异.有关详细信息,请参阅下面的部分。
我的问题的快速总结
从网络流反序列化花费的时间太长,即使我证明了网络和反序列化器本身很快。有没有我可以尝试的常见性能技巧?我已经用 BufferedInputStream
包装了网络流。另外,我尝试了双缓冲并取得了一些成功(见下面的代码)。欢迎任何实现更好性能的解决方案。
性能测试场景
在我的测试场景中,服务器和客户端位于同一台机器上,服务器发送约 174 MB 的数据。代码片段可以在这个 post 的末尾找到。您在此处看到的所有数字都是 5 次测试运行的平均值。
首先我想知道,HttpsURLConnection
中的InputStream
的读取速度有多快。包裹成一个BufferedInputStream
,将整个数据写入一个ByteArrayOutputStream
用了26.250s。1
然后我测试了我的解串器的性能,将所有 174 MB 作为 ByteArrayInputStream
传递给它。在我改进反序列化器的实现之前,它花费了 38.151s。改进后只用了23.466s2
所以这就是它,我想......但是没有。
我实际上想做的,不知何故,是将 connection.getInputStream()
传递给解串器。奇怪的事情来了:在反序列化器改进之前,反序列化花费了 61.413 秒,改进后是 60.100 秒!3
怎么会这样?尽管反序列化器有了显着改善,但这里几乎没有任何改善。此外,与该改进无关,令我惊讶的是,这比单独的性能总和 (60.100 > 26.250 + 23.466) 花费的时间更长。为什么?不要误会我的意思,我没想到这是最好的解决方案,但我也没想到它会那么糟糕。
因此,需要注意三件事:
- 整体速度受网络限制最少需要26.250s。也许有一些我可以调整的 http 设置或者我可以进一步优化服务器,但现在这可能不是我应该关注的。
- 我的解串器实现很可能仍然不完美,但它本身比网络更快,所以我认为没有必要进一步改进它。
- 基于 1. 和 2. 我假设应该以某种方式 可以以组合方式(从网络读取 + 反序列化)完成整个工作应该不会超过 26.250s。欢迎就如何实现这一目标提出任何建议。
我正在寻找某种双缓冲区,允许两个线程并行读取和写入。
标准 Java 中有类似的东西吗?最好是一些 class 继承自 InputStream
允许并行写入它?如果有类似的东西,但不是从 InputStream
继承的,我也许可以更改我的 DataDeserializer
以使用那个。
因为我还没有找到任何这样的 DoubleBufferInputStream
,所以我自己实现了它。
代码很长而且可能不完美,我不想打扰你为我做代码审查。它有两个 16kB 缓冲区。使用它我能够将整体性能提高到 39.885s。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
我稍微修改了代码片段 #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.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.096 秒。同样,这些数字几乎完美地总结了代码片段 #4 的测量时间。
此外,我使用 Java VisualVM 分析了这段完全相同的代码。这告诉我 40 秒花在了这个线程的 run()
方法上,而这 40 秒中的 100% 是 CPU 时间。另一方面,它在反序列化器内部也花费了 40 秒,但这里只有 26 秒是 CPU 时间,14 秒用于等待。这与从网络读取到 ByteBufferOutputStream
的时间完全匹配。所以我想我必须改进我的双缓冲区 "buffer-switching-algorithm"。
*) 这个奇怪的现象有什么解释吗?我只能想象这种测量方式非常不准确。但是,最新测量值的读取和写入时间完美地总结了原始测量值,因此 不可能不准确...有人可以解释一下吗? 我无法在分析器中找到这些读写性能...我会尝试找到一些设置,让我可以观察这两种方法的分析结果。
改进其中任何一项的最可靠方法是改变
connection.getInputStream()
到
new BufferedInputStream(connection.getInputStream())
如果这没有帮助,输入流不是你的问题。
显然,我的 "mistake" 是使用 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 稍慢,这完全符合我最初的假设
Based on 1. and 2. I'm assuming that it should be somehow possible to do the entire job in a combined way (reading from the network + deserializing) which should take not much more than 26.250s.
我的问题 更新 2 中描述的分析方法的非常奇怪的结果也不再出现。我还没有在 64 位中重复每一个测试,但是我 did 所做的所有分析结果现在都是合理的,即无论在哪个代码片段中,相同的代码都花费相同的时间。所以也许这真的是一个错误,或者有人有合理的解释吗?