GCC的学习(五)动态库接口可见性

为什么C++可见性支持这么重要?

简单来说,它隐藏了大多数早前是公有的(没必要)ELF符号,这也就是说:

  • 它极大的减少了装载动态库(DSO,Dynamic Shared Object)的时间。经测试,一个大型的模板库加载时间从超过6分钟变成了6秒!
  • 它使优化器产生更好的代码。PLT间接取值(当函数调用或变量访问必须通过全局偏移表(如PIC代码)进行查找时)可以完全避免,从而在很大程度上避免了现代处理器上的管道暂停,从而大大加快了代码的速度。此外,当大多数符号被本地绑定时,它们可以通过整个DSO被完全安全地省略(移除)。这给了内联线更大的自由度,特别是内联线不再需要保持一个“以防万一”的入口点;
  • 它将动态库的大小减少5-20%。 ELF导出的符号表格式相当耗费空间,可以提供完整的错误符号名称,如果使用大量模板,则平均大约需要1000个字节。 C ++模板占用了大量符号,一个典型的C ++库可以轻松超过30,000个符号,大约5-6Mb! 因此,如果您删除了60-80%的不必要符号,则DSO可以小数兆字节!
  • 更少的符号冲突几率。在此补丁的支持下,两个内部使用相同符号进行不同处理的库的老麻烦终于过去了。 哈利路亚!

尽管上面引用的库是极端情况,但新的可见性支持将导出的符号表从> 200,000个符号减少到少于18,000个。 二进制文件大小也降低了21Mb!有人可能建议GNU链接器版本脚本也可以做到。 也许对于C程序而言,这是正确的,但对于C ++而言,则不是正确的-除非您费力地指定每个符号以使其公开(及其复杂的名称混乱),否则必须使用通配符,因为通配符会导致大量虚假的符号。 如果决定更改类或函数的名称,则必须更新链接程序脚本。 对于上面的库,使用版本脚本无法获得40,000个符号以下的符号表。 此外,使用链接器版本脚本不允许GCC更好地优化代码。

Windows兼容

非Windows版本的GCC无法提供类似__declspec(dllexport)用于标记C/C++接口是共享库的接口,这让那些Windows和POSIX处理大型可移植程序应用程序的人感到沮丧[2]。良好的动态库接口设计方法编写良好代码和设计类的可见性一样重要。

虽然Windows DLL和ELF DSO语法有所区别,注意到Windows下所有代码选择宏编译选择时是不是使用dllimport就是使用dllexport,我们对程序进行简单的修补就能重用Windows下DLL编译支持,事实上它只需要花费您5分钟时间完成这个修补。

Windows与此处GCC功能语义不同体现在:__declspec(dllexport)void(* foo)(void)void(__declspec(dllexport)* foo)(void)表示的含义完全不同,他将会提示无法将该属性应用在GCC的非类型警告。

如何使用新的C++可见属性支持?

在你的头文件中,无论何时你想要你一个接口或者API变成公有的,只需要将__attribute__ ((visibility ("default")))放在结构体、类和函数声明的前边(如果你定义了宏这将会更加简单),而不需要在定义中指定。紧接着,在每次GCC编译源文件时附加参数-fvisibility=hidden给make系统。That’s All!如果你在抛出共享边界错误,请参考下边的“C++异常问题”,使用nm -C -D输出处理DSO前后的差异。

#if defined _WIN32 || defined __CYGWIN__
  #ifdef BUILDING_DLL
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllexport))
    #else
      #define DLL_PUBLIC __declspec(dllexport) // Note: actually gcc seems to also supports this syntax.
    #endif
  #else
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllimport))
    #else
      #define DLL_PUBLIC __declspec(dllimport) // Note: actually gcc seems to also supports this syntax.
    #endif
  #endif
  #define DLL_LOCAL
#else
  #if __GNUC__ >= 4
    #define DLL_PUBLIC __attribute__ ((visibility ("default")))
    #define DLL_LOCAL  __attribute__ ((visibility ("hidden")))
  #else
    #define DLL_PUBLIC
    #define DLL_LOCAL
  #endif
#endif

extern "C" DLL_PUBLIC void function(int a);
class DLL_PUBLIC SomeClass
{
   int c;
   DLL_LOCAL void privateMethod();  // Only for use within this DSO
public:
   Person(int _c) : c(_c) { }
   static void foo(int a);
};

这对于产生更加优化的代码是有帮助的:当你在编译单元外声明定义时,GCC无法判断当前编译单元是在DSO内部还是外部,将会考虑最差的情况,引入全局偏移表(GOT,Global Offset Table)这一机制,使得动态链接库承担的代码空间和额外重定位开销。为了减少这种开销,我们需要告知GCC当前DSO定义的类、结构、函数或者变量的可见性,方法是在其头文件中手动指定隐藏可见性属性hidden visibility(也就是上面这个例子的DLL_LOCAL),这个将会让GCC优化其代码。

为了解决(每次都要指定可见性)的麻烦,GCC添加了选项-fvisibility。 使用-fvisibility = hidden,您将告诉GCC,每个未显式标记为能见度属性的声明都具有隐藏的能见度。 就像上面的示例一样,即使对于标记为可见的类(从DSO导出),您可能仍要标记例如 私有成员是隐藏的,因此调用它们时(从DSO内部)将生成最佳代码,为了帮助您将旧代码转换为使用新系统,GCC现在还支持#pragma GCC可见性命令:

extern void foo(int);
#pragma GCC visibility push(hidden)
extern void someprivatefunct(int);
#pragma GCC visibility pop

#pragma GCC visibility强于-fvisibility。它也会影响外部声明。 -fvisibility仅影响定义,因此可以以最少的更改重新编译现有代码。 对于C而言,这比C ++更正确。C ++接口倾向于使用受-fvisibility影响的类。

最后,一个新的命令选项-fvisibility-inlines-hidden。这将导致所有内联类成员函数具有隐藏的可见性,从而导致显着的导出符号表和二进制大小减小,尽管不如使用-fvisibility = hidden那样大。 但是,-fvisibility-inlines-hidden可以在没有源文件更改的情况下使用,除非您需要对地址标识对于函数本身或函数本地静态数据很重要的inline进行覆盖,否则必须覆盖它。

C ++异常问题(请阅读!)

用二进制捕获用户定义类型的异常,而不是引发异常的二进制,则需要进行typeinfo查找。 返回并再次阅读最后一条语句。 当异常开始神秘地发生故障时,原因就在于此!就像函数和变量一样,在多个共享对象之间引发的类型是公共接口,并且必须具有默认可见性。 显而易见的第一步是将可跨共享对象边界抛出的所有类型始终标记为默认可见性。 您必须这样做,因为即使(例如)异常类型的实现代码位于DLL A中,当DLL B抛出该类型的实例时,DLL C中的catch处理程序也会在DLL B中寻找typeinfo
但是,这还不是全部-变得越来越难。 默认情况下,符号可见性为“默认”,但是如果链接程序仅遇到一个隐藏的定义(仅一个定义),则typeinfo符号将永久隐藏(记住C ++标准的ODR-一个定义规则)。 所有符号都是如此,但更有可能通过typeinfos影响您。 没有vtable的类的typeinfo符号是在每个使用EH的类的目标文件中按需定义的,并且定义较弱,因此在链接时将这些定义合并为一个副本。

这样做的结果是,如果您忘记了仅在一个目标文件中定义的预处理器,或者在任何时候未将可抛出类型声明为显式公共,则-fvisibility = hidden会将其标记为隐藏在该目标文件中,从而 覆盖所有其他具有默认可见性的定义,并导致typeinfo在输出的二进制文件中消失(然后,该类型的任何抛出都将导致在捕获的二进制文件中调用terminate())。 您的二进制文件将完美链接,即使它们无法正常运行,也可以正常工作。

对此发出警告虽然很不错,但是有很多合理的理由将可抛类型保留在公众视野之外。 直到整个程序优化都添加到GCC中,编译器才知道在本地捕获哪些异常。

其他模糊的链接实体(例如类模板的静态数据成员)也可能出现相同的问题。 如果该类具有隐藏的可见性,则该数据成员可以在多个DSO中实例化并单独引用,从而造成破坏。

使用类作为dynamic_cast的操作数时也会出现此问题。 确保导出所有此类。

手把手教学

以下说明是如何为您的库添加完全支持,从而产生质量最高的代码,并最大程度地减少二进制文件大小,加载时间和链接时间。 所有新代码从一开始就应该具有这种支持! 并且值得您花时间特别是在对速度有严格要求的库中花一些时间来完全实现它-这是一次一次性的时间投入,仅此而已。 但是,尽管不建议这样做,但是您可以在很短的时间内为您的库添加基本支持。

在主头文件(或将包含在所有地方的特定标头)中,将以下内容放入代码中。 该代码取自上述的TnFOX库:

// Generic helper definitions for shared library support
#if defined _WIN32 || defined __CYGWIN__
  #define FOX_HELPER_DLL_IMPORT __declspec(dllimport)
  #define FOX_HELPER_DLL_EXPORT __declspec(dllexport)
  #define FOX_HELPER_DLL_LOCAL
#else
  #if __GNUC__ >= 4
    #define FOX_HELPER_DLL_IMPORT __attribute__ ((visibility ("default")))
    #define FOX_HELPER_DLL_EXPORT __attribute__ ((visibility ("default")))
    #define FOX_HELPER_DLL_LOCAL  __attribute__ ((visibility ("hidden")))
  #else
    #define FOX_HELPER_DLL_IMPORT
    #define FOX_HELPER_DLL_EXPORT
    #define FOX_HELPER_DLL_LOCAL
  #endif
#endif

// Now we use the generic helper definitions above to define FOX_API and FOX_LOCAL.
// FOX_API is used for the public API symbols. It either DLL imports or DLL exports (or does nothing for static build)
// FOX_LOCAL is used for non-api symbols.

#ifdef FOX_DLL // defined if FOX is compiled as a DLL
  #ifdef FOX_DLL_EXPORTS // defined if we are building the FOX DLL (instead of using it)
    #define FOX_API FOX_HELPER_DLL_EXPORT
  #else
    #define FOX_API FOX_HELPER_DLL_IMPORT
  #endif // FOX_DLL_EXPORTS
  #define FOX_LOCAL FOX_HELPER_DLL_LOCAL
#else // FOX_DLL is not defined: this means FOX is a static lib.
  #define FX_API
  #define FOX_LOCAL
#endif // FOX_DLL

显然,您可能希望用适合您的库的前缀来替换FOX,对于也支持Win32的项目,您会发现许多上述熟悉的东西(您可以重用大多数Win32宏机制来支持GCC)。 解释:

  • 若定义了_WIN32(编译Windows时自动定义的)
  • 若定义了FOX_DLL_EXPORTS ,我们将编译我们的库并需要输出的符号。所以你在编译系统定义了FOX_DLL_EXPORTS来编译FOX DLL库。MSVC在所有IDE中都定义了以_EXPORTS结尾的内容(dito CMake默认设置,请参阅CMake Wiki BuildingWinDLL)。
  • 如果没有定义FOX_DLL_EXPORTS(也就是客户端使用库的情况), 我们将会使用输入库和符号
  • 如果WIN32没有定义(也就是Unix下编译GCC)
  • 如果__GNUC__>=4为真,意味着GCC版本大于4.0,所以支持这些新的特性
  • 对于每个库中的非模板非静态函数定义(头文件和源文件),决定他是公有的还是内部使用的
  • 如果对象以公有形式被使用了,使用FOX_API进行标记,像这样extern FOX_API PublicFunc()
  • 如果他只是内部使用的,使用FOX_LOCAL进行标记,像这样extern FOX_LOCAL PublicFunc()。请记住,静态函数不需要划分,也不需要模板化。
  • 对于每个定义在你库中的非模板类(头文件和源文件),决定它是公有的还是内部使用的
  • 若公有,FOX_API进行标记,像这样FOX_API PublicClass
  • 若内部使用,FOX_LOCAL标记,像这样FOX_LOCAL PublicClass
  • 导出类的不属于接口的各个成员函数,特别是私有的,并且未被友元使用的成员函数,应分别用FOX_LOCAL标记
  • 在您的构建系统(Makefile等)中,您可能希望将-fvisibility = hidden和-fvisibility-inlines-hidden选项添加到每个GCC调用的命令行参数中。 请记住,之后要彻底测试您的库,包括所有异常都正确遍历共享对象边界。

如果你想要查看前后的差异,请使用命令nm -C -D以便列举所有的导出符号以非混杂的的形式。


扩展阅读:https://developer.ibm.com/technologies/systems/articles/au-aix-symbol-visibility/
[1] https://gcc.gnu.org/wiki/Visibility
[2] “__declspec”是Microsoft c++中专用的关键字,它配合着一些属性可以对标准C++进行扩充。这些属性有:align、allocate、deprecated、 dllexport、dllimport、 naked、noinline、noreturn、nothrow、novtable、selectany、thread、property和uuid

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值