【发布时间】: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)花费的时间更长。为什么?不要误会我的意思,我没想到这是最好的解决方案,但我也没想到它会那么糟糕。
所以,需要注意三件事:
- 整体速度受网络约束,至少需要 26.250 秒。也许我可以调整一些 http 设置,或者我可以进一步优化服务器,但现在这可能不是我应该关注的。
- 我的解串器实现很可能仍然不完美,但它本身比网络快,所以我认为没有必要进一步改进它。
- 基于 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