在Java中高效地通过套接字发送大型int[]数组

14
我正在开发一个Java应用程序,需要尽可能快地将包含500,000个整数的数组从一个Android手机发送到另一个Android手机。目前主要的瓶颈是将整数转换为套接字所需的格式,无论是使用ObjectOutputStreams、ByteBuffers还是低级别的掩码和移位转换方式。什么是从一个Java应用程序向另一个发送int[]最快的方法?
下面是我已经尝试过的所有代码,以及我在测试的LG Optimus V上(600 MHz ARM处理器,Android 2.2)的基准测试结果。
低级别的掩码和移位:0.2秒
public static byte[] intToByte(int[] input)
{
    byte[] output = new byte[input.length*4];

    for(int i = 0; i < input.length; i++) {
        output[i*4] = (byte)(input[i] & 0xFF);
        output[i*4 + 1] = (byte)((input[i] & 0xFF00) >>> 8);
        output[i*4 + 2] = (byte)((input[i] & 0xFF0000) >>> 16);
        output[i*4 + 3] = (byte)((input[i] & 0xFF000000) >>> 24);
    }

    return output;
}

使用ByteBuffer和IntBuffer:0.75秒

public static byte[] intToByte(int[] input)
{
    ByteBuffer byteBuffer = ByteBuffer.allocate(input.length * 4);        
    IntBuffer intBuffer = byteBuffer.asIntBuffer();
    intBuffer.put(input);

    byte[] array = byteBuffer.array();

    return array;
}

ObjectOutputStream:3.1秒(我尝试使用DataOutPutStream和writeInt()的不同变体来代替writeObject(),但没有太大的区别)


public static void sendSerialDataTCP(String address, int[] array) throws IOException
{
    Socket senderSocket = new Socket(address, 4446);

    OutputStream os = senderSocket.getOutputStream();
    BufferedOutputStream  bos = new BufferedOutputStream (os);
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(array);

    oos.flush();
    bos.flush();
    os.flush();
    oos.close();
    os.close();
    bos.close();

    senderSocket.close();
}

最后,我用来发送byte[]的代码:比intToByte()函数要多花费0.2秒的时间。

public static void sendDataTCP(String address, byte[] data) throws IOException
{
    Socket senderSocket = new Socket(address, 4446);

    OutputStream os = senderSocket.getOutputStream();
    os.write(data, 0, data.length);
    os.flush();

    senderSocket.close();
}

我正在编写套接字两端的代码,以便可以尝试任何类型的字节序、压缩、序列化等操作。在Java中肯定有更有效率的方式来进行这种转换,请帮忙提供一下!


1
如果您首先刷新输出流会发生什么? - huseyin tugrul buyukisik
500,000个整数是2 Mb,或约20 Mbits,在100Mbit网络上大约需要0.2秒。忽略网络开销和操作系统在每一端引入的任何处理延迟。您的网络速度是多少,您期望的性能是什么? - parsifal
你为什么要使用.writeObject()?既然你的数组是int类型,应该写.writeInt() :D - huseyin tugrul buyukisik
1
我看到的一个问题是你正在创建一个大的目标数组,这在内存受限的设备上从来都不是好事。我建议只使用DataOutputStream包装BufferedOutputStream,而不是尝试自己进行转换。 - parsifal
1
@grieve - 他现在使用的方法与DataOutputStream(而不是ObjectOutputStream)写入原始二进制数据没有区别。 - parsifal
显示剩余18条评论
4个回答

5

正如我在评论中提到的那样,我认为您正在挑战处理器的极限。为了对其他人有所帮助,我将对此进行分解。以下是将整数转换为字节的循环:

    for(int i = 0; i < input.length; i++) {
        output[i*4] = (byte)(input[i] & 0xFF);
        output[i*4 + 1] = (byte)((input[i] & 0xFF00) >>> 8);
        output[i*4 + 2] = (byte)((input[i] & 0xFF0000) >>> 16);
        output[i*4 + 3] = (byte)((input[i] & 0xFF000000) >>> 24);
    }

这个循环会执行500,000次。你的600Mhz处理器每秒可以大约处理600,000,000次操作。因此,每次迭代循环将消耗大约1/1200秒的时间来完成每个操作。
再次使用非常粗略的数字(我不知道ARM指令集,所以可能有更多或更少的操作),以下是操作计数:
- 测试/分支:5(检索计数器,检索数组长度,比较,分支,增加计数器) - 掩码和移位:10 x 4(检索计数器,检索输入数组基址,添加,检索掩码,与,移位,乘以计数器,添加偏移量,添加到输出基址,存储)
好的,因此在粗略的数字中,这个循环需要至少55/1200秒,即0.04秒。但是,您并不是在处理最佳情况。首先,对于这么大的数组,您不会从处理器缓存中受益,因此您将在每个数组存储和加载中引入等待状态。
此外,我描述的基本操作可能直接转换为机器代码,也可能不是(我怀疑不是),因此循环的成本将超过我所描述的。
最后,如果你真的很不幸,JVM没有JIT编译你的代码,因此在某些部分(或全部)循环中,它正在解释字节码而不是执行本机指令。我对Dalvik不了解足够的信息来发表评论。

1
我同意运行您发布的循环可能会使我的处理器达到最大值。我想我的真正问题是:为什么Java没有更好的方法来通过套接字发送int[],而不是采用蛮力掩码和移位方法? - Jeremy Fowers
4
我很想说“因为没有魔法”,但实际上这是一系列的实现选择,其中之一就是内存类型化。如果你使用C语言,你可以创建一个缓冲区,在该缓冲区中使用'int*'写入数据,而不必担心将整数转换为字节。如果你有一个能够从进程内存到设备内存进行DMA传输的操作系统,你也可以获得更多提升。Java的目标是让你远离硬件。如果你需要接近硬件,那么你需要考虑一种能够让你接近硬件的语言。 - parsifal
1
你认为转换到Android NDK并使用本地代码进行转换和套接字传输会有所帮助吗?还是当我将巨大的int[]数组发送到本地代码时会遇到相同的问题? - Jeremy Fowers
没什么好办法。你仍然需要在Java/本地边界传输数据。我会更多地关注改变我的代码,这样我就不必一次推送500k个值了。或者规格更好的硬件。 - parsifal
这实际上是一个研究项目,我正在尝试找出在使用较慢的硬件处理大量数据时可以有多快的速度,因此较小的数组和更快的硬件不是选项 :( - Jeremy Fowers
@JeremyFowers 我建议你尝试使用 NDK 来完成,也许你可以直接访问 Java 内存并且无需转换即可发送数据。通过 NDK 可以获得套接字和访问 Java 数据等功能。例如,直接使用 ByteBuffer 可以使用本地分配的内存(来源)。 - zapl

1

如果您不反对使用库,建议您查看谷歌推出的Protocol Buffers。它专为更复杂的对象序列化而构建,但我敢打赌他们努力研究如何在Java中快速序列化整数数组。

编辑:我查看了Protobuf源代码,发现它使用类似于您的低级掩码和移位操作。


很不幸,我时间有点紧,而且这个问题相当复杂。有人知道它是否能够工作吗? - Jeremy Fowers
让我重新表述一下:我的意思是,这个方法能否使性能至少提高25%? - Jeremy Fowers

1

我认为Java从来没有打算像C语言那样可以高效地重新解释从int[]byte[]的内存区域。 它甚至没有这样的内存地址模型。

您可以使用本机方法发送数据,也可以尝试查找一些微小的优化。 但我怀疑您会获得很多收益。

例如,如果这个版本能够工作,那么它可能比您的版本略快一些。

public static byte[] intToByte(int[] input)
{
    byte[] output = new byte[input.length*4];

    for(int i = 0; i < input.length; i++) {
        int position = i << 2;
        output[position | 0] = (byte)((input[i] >>  0) & 0xFF);
        output[position | 1] = (byte)((input[i] >>  8) & 0xFF);
        output[position | 2] = (byte)((input[i] >> 16) & 0xFF);
        output[position | 3] = (byte)((input[i] >> 24) & 0xFF);
    }
    return output;
}

我不一定需要将 int[] 转换为 byte[]。我只是需要快速发送 int[],而目前将其转换为 byte[] 是最快的方法。有没有办法在不进行转换的情况下发送 int[]? - Jeremy Fowers
OutputStream 是“用于字节的可写入的接收器”。无法直接发送 int。所有子类最终都会进行一些转换。 - zapl
我不明白为什么使用ObjectOutputStream编写int[]的原始序列化字节比手动提取int[]的字节数慢得多(正如您所建议的那样)。 - Jeremy Fowers
1
ObjectOutputStream 做的事情差不多,但会在流上写入对象描述符和其他一些东西。这是不必要的额外工作。 - zapl
1
@Jeremy:ObjectOutputStream将数组的序列化版本作为Java对象进行写入,因此它包含了更多的信息,比如它的实现方式,因为接收方必须能够重构整个数组,甚至不需要知道它是一个int数组。 - Robert

0
我会这样做:
Socket senderSocket = new Socket(address, 4446);

OutputStream os = senderSocket.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(os);
DataOutputStream dos = new DataOutputStream(bos);

dos.writeInt(array.length);
for(int i : array) dos.writeInt(i);
dos.close();

另一方面,像这样阅读:

Socket recieverSocket = ...;
InputStream is = recieverSocket.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
DataInputStream dis = new DataInputStream(bis);

int length = dis.readInt();
int[] array = new int[length];

for(int i = 0; i < length; i++) array[i] = dis.readInt();
dis.close();

我尝试了一下,DataOutputStream的性能几乎与ObjectOutputStream完全相同。 - Jeremy Fowers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接