Java和C/C++之间最快的(低延迟)进程间通信方法是什么?

108

我有一个Java应用程序,通过TCP套接字连接到一个用C/C++开发的“服务器”。

这两个应用程序都在同一台Solaris计算机上运行(但我们考虑最终迁移到Linux)。交换的数据类型是简单消息(登录、登录确认,然后客户端请求某些东西,服务器回复)。每条消息约为300字节。

目前我们正在使用Sockets,一切正常,但我正在寻找更快的数据交换方式(更低的延迟),使用IPC方法。

我已经在网上进行了研究,并找到以下技术的参考:

  • 共享内存
  • 管道
  • 队列
  • 以及所谓的DMA(直接内存访问)

但是我找不到它们各自表现的适当分析,也不知道如何在JAVA和C/C++中实现它们(使它们可以相互通信),除了我能想象如何做的管道。

有人可以评论这种情况下每种方法的性能和可行性吗?有没有有用的实现信息的指针/链接?


编辑/更新

根据我在这里得到的评论和答案,我找到了有关Unix域套接字的信息,它似乎是建立在管道之上的,并将节省整个TCP堆栈。这是平台特定的,因此我计划使用JNI或judsjunixsocket进行测试。

下一步可能是直接实现管道,然后是共享内存,尽管我已被警告存在额外的复杂性...


感谢您的帮助


7
这可能对你来说有些过度,但考虑一下 http://www.zeromq.org/。 - jfs
2
请参见https://dev59.com/LnNA5IYBdhLWcg3wk--A。 - MSalters
11
UDP比TCP慢???嗯……请提供证据。 - Boppity Bop
如果您的用例需要zeromq提供的功能,则应将zeromq的时间性能与基于“POSIX消息队列、管道或共享内存”的自定义解决方案的时间性能进行比较(这类似于手写汇编语言与优化编译器生成的代码的比较:确实可以手写更快的汇编语言。在大多数情况下是否值得实践是另一个问题)。 - jfs
@J.F.Sebastian,OP并不是在寻找功能最多的IPC,他在寻找哪种IPC速度最快(延迟最低)。 - Nautilus
显示剩余4条评论
10个回答

109

我刚测试了一下我的Corei5 2.8GHz的Java延迟,只发送/接收单个字节,同时开启了2个Java进程,没有使用taskset指定CPU核心:

TCP         - 25 microseconds
Named pipes - 15 microseconds

现在可以明确指定核心掩码,例如 taskset 1 java Srvtaskset 2 java Cli
TCP, same cores:                      30 microseconds
TCP, explicit different cores:        22 microseconds
Named pipes, same core:               4-5 microseconds !!!!
Named pipes, taskset different cores: 7-8 microseconds !!!!

so

TCP overhead is visible
scheduling overhead (or core caches?) is also the culprit

同时,Thread.sleep(0)(如strace所示)会导致执行单个sched_yield() Linux内核调用,耗时为0.3微秒——因此调度到单个核心的命名管道仍然有很大的开销。

一些共享内存的测量:

2009年9月14日-Solace Systems今天宣布,其统一消息平台API使用共享内存传输可以实现平均延迟低于700纳秒。

http://solacesystems.com/news/fastest-ipc-messaging/

P.S. - 第二天尝试了内存映射文件形式的共享内存,如果可以接受繁忙等待,我们可以通过以下代码将传递单个字节的延迟降至0.3微秒:

MappedByteBuffer mem =
  new RandomAccessFile("/tmp/mapped.txt", "rw").getChannel()
  .map(FileChannel.MapMode.READ_WRITE, 0, 1);

while(true){
  while(mem.get(0)!=5) Thread.sleep(0); // waiting for client request
  mem.put(0, (byte)10); // sending the reply
}

注意:需要使用Thread.sleep(0)方法,以便两个进程可以看到彼此的更改(我目前不知道其他方法)。如果使用taskset将两个进程强制分配到同一个核心,则延迟为1.5微秒 - 这是上下文切换的延迟。

P.P.S - 0.3微秒是一个很好的数字!以下代码仅执行原始字符串连接,却仅需要0.1微秒:

int j=123456789;
String ret = "my-record-key-" + j  + "-in-db";

P.P.P.S - 希望这不算太偏题,但最终我尝试用增加一个静态的volatile int变量来替换Thread.sleep(0)(JVM在这样做时会刷新CPU缓存),并获得了 - 记录!- 72纳秒的Java到Java进程通信延迟

然而,当被强制放置在同一CPU核心上时,增量volatile JVM永远不会将控制权让给彼此,从而产生完全相同的10毫秒延迟 - Linux时间量子似乎为5ms...因此,只有在有空余核心的情况下才应使用它,否则sleep(0)更安全。


谢谢Andriy!非常有用的学习内容,而且TCP的测量结果大致符合我的需求,这是一个很好的参考。我想我会研究一下命名管道。 - Bastien
如果您可以将进程固定到不同的核心,那么用增加易失性静态整数替换Thread(Sleep)应该只能这样做。此外,我没有意识到您可以这样做?我以为操作系统会决定? - intrigued_66
3
尝试使用LockSupport.parkNanos(1),应该会达到同样的效果。 - reccles
很好。你可以做得更好(例如5-7us的RTT延迟)用于TCP ping。在这里查看:http://psy-lob-saw.blogspot.com/2012/12/java-ping-performance-baseline-utility.html - Nitsan Wakart
1
进一步探索使用内存映射文件作为共享内存以支持Java中的IPC队列:http://psy-lob-saw.blogspot.com/2013/04/lock-free-ipc-queue.html,实现每秒135M条消息。此外,请参见我的下面的答案,进行方法的延迟比较研究。 - Nitsan Wakart

11

DMA是硬件设备可以直接访问物理RAM而不会中断CPU的一种方法。例如,一个常见的例子是硬盘控制器可以将字节直接从磁盘复制到RAM中。因此,它不适用于IPC。

共享内存和管道都直接由现代操作系统支持。因此,它们非常快速。队列通常是抽象的,例如在套接字、管道和/或共享内存之上实现。这可能看起来像是一种更慢的机制,但另一种选择是你自己创建这样的抽象。


为什么我可以阅读很多与RDMA(远程直接内存访问)相关的内容,这些内容适用于跨网络(特别是使用InfiniBand),并且可以执行相同的操作,但对于DMA,为什么不能在没有网络的情况下实现相同的操作呢?实际上,我正在尝试在没有网络的情况下实现相同的操作(因为所有内容都在同一台机器上)。 - Bastien
RDMA是相同的概念:在不中断任何一侧的CPU的情况下,在网络上复制字节。它仍然不能在进程级别操作。 - MSalters

11

这个问题在一段时间之前被提出,但您可能会对https://github.com/peter-lawrey/Java-Chronicle感兴趣,它支持典型延迟为200纳秒和每秒20 M条消息的吞吐量。它使用进程间共享的内存映射文件(还将数据持久化,使其成为最快的数据持久化方式)。


8

这是一个包含多种IPC传输性能测试的项目:

http://github.com/rigtorp/ipc-bench


(注:IPC即进程间通信,该项目旨在测试不同IPC方式的性能表现)

它并不包含“Java因素”,但看起来很有趣。 - user166390
刚刚发现了goldsborough的基准测试,并附有详细的自述文件。https://github.com/goldsborough/ipc-bench - Louis Go

8

虽然有些晚了,但我想指出一个专门使用Java NIO测量ping延迟的开源项目

在这篇博客文章中进一步探讨和解释。结果为(RTT以纳秒为单位):

Implementation, Min,   50%,   90%,   99%,   99.9%, 99.99%,Max
IPC busy-spin,  89,    127,   168,   3326,  6501,  11555, 25131
UDP busy-spin,  4597,  5224,  5391,  5958,  8466,  10918, 18396
TCP busy-spin,  6244,  6784,  7475,  8697,  11070, 16791, 27265
TCP select-now, 8858,  9617,  9845,  12173, 13845, 19417, 26171
TCP block,      10696, 13103, 13299, 14428, 15629, 20373, 32149
TCP select,     13425, 15426, 15743, 18035, 20719, 24793, 37877

以下是类似于被接受的答案的内容。System.nanotime()错误(通过测量什么也没有得出估计值)大约为40纳秒,因此对于IPC,实际结果可能会更低。祝你使用愉快。


6
如果您考虑使用本地访问(因为您的应用程序和“服务器”在同一台计算机上),请考虑使用JNA,它可以减少您需要处理的样板代码。

2

我对本地进程间通信不是很了解,但我猜想您需要使用本地代码进行通信,您可以使用JNI机制访问该本地代码。因此,从Java中,您将调用一个与另一个进程通信的本地函数。


1

0
你有没有考虑保持套接字开启,这样连接就可以被重复使用?

套接字确实保持打开状态。连接在应用程序运行期间一直保持活动状态(大约7小时)。消息更或多或少地连续交换(假设每秒5到10个)。当前延迟约为200微秒,目标是缩短1或2个数量级。 - Bastien
2毫秒的延迟?雄心勃勃。将C代码重写为共享库,然后使用JNI进行接口调用是否可行? - Thorbjørn Ravn Andersen
2毫秒是2000微秒,而不是200微秒。这使得2毫秒的目标更加容易实现。 - thewhiteambit

-1

Oracle关于JNI性能的错误报告:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4096069

JNI是一个慢速接口,因此Java TCP套接字是应用程序之间通知的最快方法,但这并不意味着您必须通过套接字发送负载。使用LDMA传输有效载荷,但正如以前的问题所指出的那样,Java对内存映射的支持不理想,因此您需要实现一个JNI库来运行mmap。


3
JNI为什么慢?考虑一下Java中低级别的TCP层是如何工作的,它不是用Java字节码编写的!(例如,必须通过本机主机进行过滤。)因此,我拒绝声称Java TCP套接字比JNI更快的说法。(但是,JNI不是IPC。) - user166390
4
如果您只使用原始数据类型,一次JNI调用仅需要9ns的时间(在英特尔i5上),因此并不慢。 - Martin Kersten

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