我有一个客户端服务器应用程序,其中服务器向客户端发送一些二进制数据,并且客户端必须根据自定义二进制格式从该字节流中反序列化对象。数据通过HTTPS连接发送,客户端使用HttpsURLConnection.getInputStream()
读取数据。
我实现了一个DataDeserializer
,它使用InputStream
并将其完全反序列化。它的工作方式是使用小型缓冲区(通常少于100个字节)执行多个inputStream.read(buffer)
调用。为了获得更好的整体性能,我还在这里尝试了不同的实现。一项更改确实显着提高了此类的性能(我现在使用ByteBuffer
来读取基本类型,而不是通过字节移位手动进行操作),但与网络流结合使用时,不会出现差异。有关更多详细信息,请参见以下部分。
即使我证明网络和反序列化器本身都很快,但从网络流中反序列化花费的时间太长。我可以尝试一些常见的性能技巧吗?我已经用BufferedInputStream
包装了网络流。另外,我尝试了双缓冲并取得了一些成功(请参见下面的代码)。任何获得更好性能的解决方案都是值得欢迎的。
在我的测试场景中,服务器和客户端位于同一台计算机上,服务器发送约174 MB的数据。可以在本文的末尾找到代码片段。您在此处看到的所有数字都是5次测试运行的平均值。
首先,我想知道InputStream
中HttpsURLConnection
的读取速度。包裹到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)。为什么?不要误会我的意思,我没想到这会是最好的解决方案,但是我也没想到它会那么糟糕。
因此,需要注意三件事:
我正在寻找某种双缓冲,允许两个线程从中读取并并行写入。
标准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 */);
}
}
我对代码片段#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
的时间完全匹配。因此,我想我必须改进双缓冲区的“缓冲区切换算法”。
*)这个奇怪的发现有什么解释吗?我只能想象这种测量方法非常不准确。但是,最新测量值的读取和写入时间可以完美地总结为原始测量值,因此, 可能不准确...有人可以对此进行说明吗? 我无法在事件探查器中找到这些读写性能。我将尝试找到一些设置,使我能够观察这两种方法的分析结果。
答案 0 :(得分:0)
改进其中任何一种的最确定的方法是更改
connection.getInputStream()
到
new BufferedInputStream(connection.getInputStream())
如果这没有帮助,则输入流不是您的问题。
答案 1 :(得分:0)
显然,我的“错误”是使用32位JVM(精确的是jre1.8.0_172)。 在64位版本的JVM和tadaaa上运行完全相同的代码段,...速度很快并且在这里很有意义。
特别是有关相应代码段的这些新数字:
因此,显然,对Does Java 64 bit perform better than the 32-bit version?的回答不再正确。或者,此特定的32位JRE版本中存在严重的错误。我还没有测试其他人。
如您所见,#4仅比#2慢一点,这完全符合我最初的假设
基于1.和2。我假设应该以某种方式 以组合的方式完成整个工作(从网络中读取+ 反序列化)的时间不应超过26.250s。
我的问题的更新2 中描述的我的分析方法的非常奇怪的结果也不再发生。我还没有在64位中重复执行每个测试,但现在我 did 所做的所有性能分析结果似乎都是合理的,即,无论使用哪个代码段,相同的代码都会花费相同的时间。所以也许这确实是一个错误,或者有人有合理的解释吗?