关于OutputStream类中write(int b)与write(byte[] b)的效率问题

本文探讨了Java中OutputStream的write(int b)与write(byte[] b)的效率差异。通过对源码的分析和测试,发现write(int b)在内部调用write(byte[] b),并频繁申请和释放空间,导致效率较低。主要原因是多次调用socketWrite0方法申请缓冲区,次要原因是锁的获取和释放以及其他操作的开销。

据说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); :

  1. 都会申请长度为MAX_BUFFER_LEN的缓存数组;
  2. 如果长度超过了MAX_BUFFER_LEN,则会重新申请一个长度为MAX_HEAP_BUFFER_LEN的空间;
  3. 不断用分配的空间发出数据;
  4. 释放空间

所以每次调用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)效率不同的原因。

最终结论

  1. socket.getOutputStream()所返回的是OutputStream的子类FileOutputStream的子类SocketOutputStream,正如OutputStream中的注释那样,在子类里写了更有效率的方法。OutputStream中write(byte[] b)调用了write(int b),而SocketOutputStream中write(int b)调用了write(byte[] b)。

  2. 主要原因:每次调用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次空间,申请空间使用的时间不同是造成两个方法效率不同的主要原因。

  3. 次要原因:每一次调用write(byte[] b)时,方法中的判断、申请锁、释放锁、数据的增减、以及write(byte[] b)方法中调用的其他方法(即在多次循环中除去申请/释放缓冲空间的其他所有操作)所占用的时间消耗,是造成两个方法效率不同的次要原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值