在C++代码中使用纯C库是否会导致性能下降/惩罚?

16

19
不会有性能惩罚。 - Steve
C++是基于C构建的,因此自然而然地具有向后兼容性。 - Gary Hayes
4
如果没有使用extern关键字,你的意思是什么?如果C++中没有以某种方式声明为extern "C",那么你是如何从C++中调用C函数的? - BeeOnRope
8
抱歉,但这是胡说八道。C++并非“基于C”而建立的。并非所有C++代码都是合法的C语言代码,同样,并非所有C语言代码都是合法的C++代码。C++不是C语言,因此,如果将C++视为C2.0来谈论“向后”兼容性是不正确的。两种语言之间存在一定的兼容性,但不存在“向后”兼容性。 - Bakuriu
1
@Lưu Vĩnh Phúc:这有点偏离重点了。我们谈论的是函数调用的价格,而不是函数的效率。 - Steve
显示剩余2条评论
3个回答

35

C和C++都是编程语言的规范(用英语编写,例如参见n1570中的C11规范),它们不涉及性能问题(而是关于程序的行为,即semantics)。

然而,您可能会使用像GCCClang这样的编译器,它们不会带来任何性能损失,因为它们为C和C ++语言构建相同类型的中间内部表示(例如GCC的GIMPLE和Clang的LLVM),并且因为C和C ++代码使用兼容的ABIcalling conventions

在实践中,extern "C"不会改变任何调用约定,但会禁用name mangling。然而,它对编译器的确切影响取决于该编译器。它可能(也可能不)禁用inlining(但在GCC中考虑使用-flto进行链接时优化)。

一些C编译器(例如tinycc)生成的代码性能较差。即使使用GCCClang,当使用-O0或未明确启用optimization(例如通过passing -O1-O2等...)时,也可能会产生缓慢的代码(并且默认情况下禁用优化)。

顺便提一下,C++旨在与C互操作(这个强约束解释了C++的大多数不足之处)。

在某些情况下,真正的C++代码可能会比相应的真正的C代码稍微快一些。例如,要对数字数组进行排序,您将在真正的C++中使用std::arraystd::sort,并且排序中的比较操作很可能会被内联。对于C代码,您将只使用qsort,每个比较都通过间接函数调用进行(因为编译器没有内联qsort,即使在理论上它可以...)。
在其他一些情况下,真正的C++代码可能会稍微慢一些;例如,几个(但不是全部)::operator new的实现只是调用malloc(然后检查失败),但没有被内联。
实际上,从C ++代码调用C代码或从C代码调用C ++代码没有任何惩罚,因为调用约定是兼容的。
C longjmp设施可能比抛出C++异常更快,但它们没有相同的语义(请参见stack unwinding),而longjmp在C++代码中不太适用。
如果你非常关注性能,可以用真正的C语言和真正的C++编写两次代码并进行基准测试。你可能会观察到C和C++之间的轻微变化(最多几个百分点),所以我完全不担心(你的性能担忧实际上是没有必要的)。

上下文切换是与操作系统多任务处理相关的概念,在进程抢占期间运行机器码可执行文件时发生。如何获得可执行文件(从C编译器、C++编译器、Go编译器、SBCL编译器或作为某种语言的解释器,如Perl或字节码Python)都是完全无关紧要的(因为上下文切换可以在任何机器指令中,在中断期间发生)。阅读一些书籍,比如操作系统:三个简单部分


3
我认为OP假设从C++跳转到C以及相反的情况可能会发生堆栈切换。有时候在从一种语言转换到另一种语言时这是必需的,尽管在C和C++中并非如此。当执行外部C代码时,Go可能需要切换堆栈,因为它使用分段堆栈来进行协程处理,因此需要将特殊生成的代码插入每个函数中 - 这不适用于典型的C库。 - user1143634
据我所知,Go在这种情况下不会切换堆栈,但即使它这样做了,也不是上下文切换,并且运行非常快。 - Basile Starynkevitch
2
他在“上下文切换”中加了引号。我猜他指的是C++程序和C库之间的某种thunking/proxy代码。 - Steve
@BasileStarynkevitch 不确定他们需要多少信息,但是在单独的堆栈上执行的代码需要一些信息才能返回,因此需要进行一些额外的工作。可能是上下文切换的一半。 - user1143634
1
例如,几个(但不是全部)::operator new的实现只是调用malloc而没有内联。情况可能比这更糟:在我的机器上,一个实现为通过调用malloc()来调用::operator new()的函数比标准的::operator new()每次调用快大约一百个CPU周期... - cmaster - reinstate monica
显示剩余4条评论

13

基本上来说,从C++代码调用C库时,你不会看到任何类型的"切换"性能惩罚。会有性能损失。例如,从C++中调用在另一个翻译单元中定义的C方法,与在另一个翻译单元中以相同的C方式实现的C++中调用相同的方法应该具有大致相同的性能。

这是因为常见的C和C++编译器实现最终将源代码编译成本地代码,并使用与C++调用可能发生的相同类型的call高效支持调用extern "C"函数。调用约定通常基于平台ABI,在任一情况下都是类似的。

除了这个基本事实之外,调用C函数而不是在C++中实现相同函数仍可能存在一些性能缺陷:

  • 用C实现并声明为extern "C"的函数通常不会被内联(因为按定义它们不是在头文件中实现的),这抑制了许多可能非常强大的优化0
  • C++代码中使用的大多数数据类型1不能直接被C代码使用,因此例如,如果您在C++代码中有一个std::string,则需要选择不同的类型将其传递给C代码-char*很常见,但会丢失关于显式长度的信息,这可能比C++解决方案慢。许多类型没有直接的C等价物,因此您可能会陷入昂贵的转换中。
  • C代码使用mallocfree进行动态内存管理,而C++代码通常使用newdelete(并且通常更喜欢将这些调用隐藏在其他类后面)。如果您需要在一种语言中分配将在另一种语言中释放的内存,则可能会导致不匹配,需要回调到“其他”语言进行释放,或者可能会出现不必要的副本等情况。
  • C代码通常大量使用C标准库例程,而C++代码通常使用C++标准库中的方法。由于有很多功能重叠,因此混合使用C和C++可能具有比纯C++代码更大的代码占用空间,因为使用了更多的C库方法2
上面的问题仅适用于将纯C++实现与C进行对比时,并不意味着调用C时性能会降低:它真正回答的是“为什么以C和C++混合编写应用程序可能比纯C++慢?”此外,上述问题大多是短暂调用的问题,其中上述开销可能是显著的。如果您在C中调用一个长函数,则这不太成问题。 “数据类型不匹配”仍然可能会影响您,但这可以在C++方面进行设计。

0 有趣的是,链接时优化实际上允许将C方法内联到C++代码中,这是LTO很少提到的好处。当然,这通常取决于使用适当的LTO选项从源代码构建C库。

1 例如,几乎任何非标准布局类型。

2 这至少部分地得到缓解,因为许多C ++标准库调用最终会委托给C库例程来处理“重”工作,例如std :: copy在可能时调用memcpymemset以及大多数new实现最终调用malloc.


4

C++自诞生以来已经发生了很多变化,但是它的设计使它与C向后兼容。C++编译器通常是从C编译器构建而成的,但是更加现代化,具有链接时优化。我想很多软件都可以可靠地混合使用C和C++代码,无论是在用户空间还是在使用的库中。最近我回答了一个问题,涉及将C++类成员函数指针传递给一个由C实现的库函数。发帖者说它对他有效。因此,C++可能比任何程序员或用户想象的更兼容C。

然而,C++ 的范式比 C 更多,因为它是面向对象的,并实现了整个抽象、新数据类型和运算符的光谱。某些数据类型易于转换(从 char * C 字符串到 std::string),而其他数据类型则不然。GNU.org 上关于 C++ 编译器选项的本节 可能会引起一些兴趣。
当混合使用这两种语言时,我不会过于担心性能下降。除非涉及大量数据的抽象处理,否则最终用户甚至程序员都几乎不会注意到任何可测量的性能变化。

1
你所提供的GCC编译器选项链接非常过时(因为自GCC3.0之后就已经过时了)。请使用https://gcc.gnu.org/onlinedocs/gcc/Invoking-GCC.html。 - Basile Starynkevitch

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