据说write(int b)与 write(byte[] b)的效率不一样???测试发现,在发送相同内容的数据时(100000个26),write(int b)确实比write(byte[] b)效率要低,经过一层层查看代码与试验,得知了出现这个问题的原因。
一言以蔽之,write(byte[] b)与write(int a)都在OutputStream的子类SocketOutputStream中被重写,write(int a)中调用了write(byte[] b),而每次调用write(byte[] b),实际上都会申请一段空间来缓存b,假如这段空间的长度为65536,b的长度为65536,a的个数为65536,而则write(byte[] b)需要申请1次长度为65536的空间,而write(int a)则需要申请65536次长度为65536的空间。申请/释放是一个耗费时间的操作,两种方法在传输n个byte时,write(int a)需要申请/释放空间n次,而write(byte[] b)则至多需要两次,总的申请/释放空间不同导致了write(int b)与 write(byte[] b)的效率不同
STEP 1
推测:源码实现方式不同;
求证:不同查看源码,发现write(byte[] b)是调用了write(int b),不应当有效率差别,但是源码注释中说Subclasses are encouraged to override this method and provide a more efficient implementation.。
当传送一个很大很大的长度的数组时(length = 10000000,除此之外,每次测试对比,都是100000个int或者长度为100000的byte数组),发现报错位置at java.base/java.net.SocketOutputStream.socketWrite0 。
通过
System.out.println(outputStream.getClass());
发现object.getOutputStream()返回的OutputStream实例其实是OutputStream的子类SocketOutputStream的实体,查看源码发现write(int b)的实现为
public void write(int b) throws IOException {
temp[0] = (byte)b;
socketWrite(temp, 0, 1);
}
write(byte[] b)的实现为
public void write(byte b[]) throws IOException {
socketWrite(b, 0, b.length);
}
STEP 2
推测:是temp[0] = (byte)b;占用了时间
求证:比较1次传入长度为10000的byte数组,和传入10000次长度为1的byte数组,发现并没有什么明显改善。进入socketWrite方法查看,发现调用了socketWrite0(fd, b, off, len);
private void socketWrite(byte b[], int off, int len) throws IOException {
if (len <= 0 || off < 0 || len > b.length - off) {
if (len == 0) {
return;
}
throw new ArrayIndexOutOfBoundsException("len == " + len
+ " off == " + off + " buffer length == " + b.length);
}
FileDescriptor fd = impl.acquireFD();
try {
socketWrite0(fd, b, off, len);
} catch (SocketException se) {
if (impl.isClosedOrPending()) {
throw new SocketException("Socket closed");
} else {
throw se;
}
} finally {
impl.releaseFD();
}
}
STEP 3
推测:在上面的代码中我们看到了impl.releaseFD(); 有没有可能是因为使用了锁呢?
求证:给传入100000次长度为1的byte数组加上锁,很不幸,效率依然没有改变。
STEP 4
推测:申请锁与释放锁的操作,不会因为我们加了锁就取消,所以在socketWrite()方法内,write(int b)与 write(byte[] b)的效率原因可能因为锁的频繁获取与释放。
求证:首先查看FD相关代码
FileDescriptor acquireFD() {
synchronized (fdLock) {
fdUseCount++;
return fd;
}
}
void releaseFD() {
synchronized (fdLock) {
fdUseCount--;
if (fdUseCount == -1) {
if (fd != null) {
try {
socketClose();
} catch (IOException e) {
} finally {
fd = null;
}
}
}
}
}
发现socketWrite0(fd, b, off, len);并没有因为上述代码块而加锁。
在 write(byte[] b)计时区间内加入同样多次数的锁获取,发现确实耗时变长了,但也只是从1ms变成了4ms,而write(int b)的耗时是150ms左右……所以我们找到了一个原因但不是主要原因。
STEP 5
推测:socketWrite0(fd, b, off, len); 的实现方法才是速度快慢的秘密所在,但是这个方法是native method。
求证:遂查看在SocketOutputStream.c如何实现之SocketOutputStream.c,
JNIEXPORT void JNICALL
Java_java_net_SocketOutputStream_socketWrite0(JNIEnv *env, jobject this,
jobject fdObj,
jbyteArray data,
jint off, jint len) {
char *bufP;
char BUF[MAX_BUFFER_LEN];
int buflen;
int fd;
if (IS_NULL(fdObj)) {
JNU_ThrowByName(env, "java/net/SocketException", "Socket closed");
return;
} else {
fd = (*env)->GetIntField(env, fdObj, IO_fd_fdID);
/* Bug 4086704 - If the Socket associated with this file descriptor
* was closed (sysCloseFD), the the file descriptor is set to -1.
*/
if (fd == -1) {
JNU_ThrowByName(env, "java/net/SocketException", "Socket closed");
return;
}
}
if (len <= MAX_BUFFER_LEN) {
bufP = BUF;
buflen = MAX_BUFFER_LEN;
} else {
buflen = min(MAX_HEAP_BUFFER_LEN, len);
bufP = (char *)malloc((size_t)buflen);
/* if heap exhausted resort to stack buffer */
if (bufP == NULL) {
bufP = BUF;
buflen = MAX_BUFFER_LEN;
}
}
while(len > 0) {
int loff = 0;
int chunkLen = min(buflen, len);
int llen = chunkLen;
(*env)->GetByteArrayRegion(env, data, off, chunkLen, (jbyte *)bufP);
if ((*env)->ExceptionCheck(env)) {
break;
} else {
while(llen > 0) {
int n = NET_Send(fd, bufP + loff, llen, 0);
if (n > 0) {
llen -= n;
loff += n;
continue;
}
if (n == JVM_IO_INTR) {
JNU_ThrowByName(env, "java/io/InterruptedIOException", 0);
} else {
if (errno == ECONNRESET) {
JNU_ThrowByName(env, "sun/net/ConnectionResetException",
"Connection reset");
} else {
NET_ThrowByNameWithLastError(env, "java/net/SocketException",
"Write failed");
}
}
if (bufP != BUF) {
free(bufP);
}
return;
}
len -= chunkLen;
off += chunkLen;
}
}
if (bufP != BUF) {
free(bufP);
}
}
MAX_BUFFER_LEN 与MAX_HEAP_BUFFER_LEN的具体值取决于处理器,在64位处理器上运行时,编译器会有预定义宏 _LP64 1;
//LP64编译器选项生效时,_LP64为1,check it with "cpp -dM /dev/null"
#ifdef _LP64
#define MAX_BUFFER_LEN 65536
#define MAX_HEAP_BUFFER_LEN 131072
#else
#define MAX_BUFFER_LEN 8192
#define MAX_HEAP_BUFFER_LEN 65536
#endif
发现每次调用socketWrite0(fd, b, off, len); :
- 都会申请长度为MAX_BUFFER_LEN的缓存数组;
- 如果长度超过了MAX_BUFFER_LEN,则会重新申请一个长度为MAX_HEAP_BUFFER_LEN的空间;
- 不断用分配的空间发出数据;
- 释放空间
所以每次调用socketWrite0(fd, b, off, len); 无论发送多少数据都会至少分配/释放一次空间。这个才是浪费时间的关键点。于是我们在调用write(byte[] b)的计时区间内,增加
byte[] bytes = new byte[MAX_BUFFER_LEN];
当MAX_BUFFER_LEN=8192时,时间已经从4ms来到了243ms,当MAX_BUFFER_LEN=65536时,时间更是来到了1378ms。
STEP 6
推测:申请空间导致了时间增加,但是从时间来看,write(byte[] b)且申请100000次长度为MAX_BUFFER_LEN已经使总时间大于write(int b)的时间了,且当MAX_BUFFER_LEN=65536的时候,时间更是是write(int b)耗时的10余倍。那么导致效率不同的原因可能是因为申请/释放空间,而MAX_BUFFER_LEN取不同值时的差异则可能是java的特性。java和C++申请空间的耗时可能不同!
求证:经测试,在C++中申请2x100000次长度为65536的空间并释放的时长为100ms(这里使用2x100000是因为byte[]长度为100000的时候,就会需要申请2次,第一次是MAX_BUFFER_LEN,第二次是MAX_HEAP_BUFFER_LEN)至此我想我已经找到了write(int b)与 write(byte[] b)效率不同的原因。
最终结论
-
socket.getOutputStream()所返回的是OutputStream的子类FileOutputStream的子类SocketOutputStream,正如OutputStream中的注释那样,在子类里写了更有效率的方法。OutputStream中write(byte[] b)调用了write(int b),而SocketOutputStream中write(int b)调用了write(byte[] b)。
-
主要原因:每次调用write(byte[] b)都会调用一次socketWrite0(fd, b, off, len),每次调用socketWrite0(fd, b, off, len);都会至少申请一次缓冲空间,而为了提高效率(减少空间申请次数),并不会一次只申请1byte的空间,而是会申请MAX_BUFFER_LEN个byte的空间,当b的长度大于MAX_BUFFER_LEN时,还会重新申请MAX_HEAP_BUFFER_LEN的空间,所以通过write(int b)传输100000个值的时候就会申请100000次空间,而通过write(byte[] b)的时候只需要申请2次空间,申请空间使用的时间不同是造成两个方法效率不同的主要原因。
-
次要原因:每一次调用write(byte[] b)时,方法中的判断、申请锁、释放锁、数据的增减、以及write(byte[] b)方法中调用的其他方法(即在多次循环中除去申请/释放缓冲空间的其他所有操作)所占用的时间消耗,是造成两个方法效率不同的次要原因。
本文探讨了Java中OutputStream的write(int b)与write(byte[] b)的效率差异。通过对源码的分析和测试,发现write(int b)在内部调用write(byte[] b),并频繁申请和释放空间,导致效率较低。主要原因是多次调用socketWrite0方法申请缓冲区,次要原因是锁的获取和释放以及其他操作的开销。


被折叠的 条评论
为什么被折叠?



