是否有符合标准的方法在 C++ 中进行零拷贝 IPC?

Posted

技术标签:

【中文标题】是否有符合标准的方法在 C++ 中进行零拷贝 IPC?【英文标题】:Is there a standards-compliant way to do zero-copy IPC in C++? 【发布时间】:2017-06-22 12:57:59 【问题描述】:

我有一个应用程序,它当前从流(套接字、命名、管道、标准输入等)中读取数据到 char 缓冲区,然后使用 reinterpret_cast 指向 Foo *(其中 Foo 是POD) 进入缓冲区的中间,然后通过该指针处理缓冲区的内容。

现在,这违反了严格的别名规则,尽管我怀疑它实际上会在实践中引起问题。不过,在标准 C++ 中是否有一种可接受的方式来做到这一点?因为我们可能会以这种方式传输 100 GB 的数据,并且在任何情况下都不希望将这些数据从缓冲区复制到具有memcpy 的结构中的开销。

为了清楚起见,代码如下所示:

MessageData *msg = new MessageData();
while (ipc.we_have_data()) 
   ipc.read(msg);
   char *buf = msg->data();
   Header *h = reinterpret_cast<Header *>(buf);
   if (h->tag == 0) 
      Payload *p = reinterpret_cast<Payload *>(buf + sizeof(Header));
      do_stuff_with_payload(p);
    else if (h->tag == 1) 
      // etc...
   

我意识到可能存在对齐问题,但目前我并不关心它们。数据是由同一个编译器在同一个平台上生成的,所以结构成员的布局不同是没有问题的。但是,据我了解,这在技术上违反了严格的别名规则。

有没有一种不违反严格的别名规则的有效方法?

或者我完全错了,按照这些规则就好了?

如果是,为什么?

编辑: 一条已删除的评论指向此 definition of the aliasing rules,上面写着 char * 获得免费通行证。所以,我的例子实际上并没有违反严格的别名规则。有人知道这个标准的正确部分吗?

【问题讨论】:

【参考方案1】:

不幸的是,该标准对于处理读入字符缓冲区的结构化数据不是很友好。只允许相反的情况:如果您知道要读取一个 POD 对象,您可以构建一个并将其转换为 char 指针的地址传递给任何能够用实际数据填充它的函数,然后使用它正常。

char * 提供的free pass 只允许在字节级别处理对象,但严格的别名规则通常禁止确定 char 缓冲区实际上包含对象。无论如何,这里的较高风险将是对齐问题。

对于其余部分,编译器的特定实现完全可以忽略严格的别名规则。然后发生的事情是标准未定义的,但可以由编译器完美定义,只要您传递适当的标志。然后,您的程序可能会因使用不同的编译器或同一编译器的不同配置而中断,因此它会出现可移植性问题,但如果有明确的文档记录,这可能是可以接受的 - 并且您确定不会出现对齐问题...

【讨论】:

对齐问题通常可以得到有效处理。处理别名需要使用可以依赖的编译器来识别某些明显的别名结构,编写缓慢而低效的代码,并希望编译器可以将其转换为人们想要编写的合理代码,或者编写代码编译器希望不会弄清楚如何破解。我不确定为什么一些编译器编写者似乎死死地反对#1,因为识别某些明显的结构似乎比试图从符合标准的混乱中制作高效的代码更容易。 @supercat - 我也不知道为什么。这似乎真的应该添加到 C++ 标准中,这是一种经过批准的方式来执行某些类型的别名。做这样的事情的能力是人们首先使用 C 和 C++ 等语言的全部原因的一部分。 @Omnifarious:让整个情况真正令人难过的是,如果标准只是在有意义时允许实现进行别名假设,并处理某些实现可能无法识别某些类型的别名,这有点类似于如果程序将某些函数调用嵌套 30 深(或者,就此而言,3 深),某些实现可能会死掉的事实,但建议质量实现应该处理考虑到目标平台和应用领域,任何类型的别名都是有意义的...... ...程序员和编译器编写者的常识可以产生比标准中的规则更好、更可靠的结果,如果按照任何合理的衡量标准,如果按字面解释,这将是可怕的。不仅因为它们同时阻止了许多有用的优化,同时不必要地取缔了许多有用的结构,而且更因为它们是模棱两可的,没有一致的方式来一致地解释它们,也没有一致的方式来合理地解释它们。 @supercat - 我对“常识”的主要问题是,如果未指定,则很难编写可移植程序。我想要一种标准承诺的工作方式。这种方式可以基于常识,但我希望它指定。它可以保证确切的结果当然是实现定义的行为。它实际上必须。但它可以为这种行为指定一些松散的界限,就像 C99 通过联合对类型双关所做的那样。【参考方案2】:

由于您的目标是(大概)具有众所周知的特征和明确定义的行为的嵌入式平台,因此您可以做出明智的决定,使用-fno-strict-aliasing 禁用严格别名。未定义的行为只是不可移植的行为,如果定义了会先发制人地悲观某些系统。

如果您真的关心遵循标准,那么使用工会是您的最佳选择。您可以尝试通过某种不执行任何分配的专用类型安全联合或使用某种类型的跨度/视图类来对其进行 C++ 化,该类可以接受指向字符缓冲区的指针,但包装您的 POD-特定的 API。

【讨论】:

我实际上并不是针对嵌入式平台。这是一个数据库应用程序,其中数据库引擎之外的某些程序可能必须处理返回数百万甚至数十亿行的查询结果。我们可以访问数据库的源代码,但出于安全原因,在引擎内部运行处理并不是一个可行的解决方案。

以上是关于是否有符合标准的方法在 C++ 中进行零拷贝 IPC?的主要内容,如果未能解决你的问题,请参考以下文章

谷物和 Boost 序列化是不是使用零拷贝?

C++ lambda 友谊

是否有符合标准的方法来检测 C 标准库中的函数是否通过内部/内置实现?

操作系统-IO零拷贝

操作系统-IO零拷贝

操作系统-IO零拷贝