Recently in C/C++ Category

关于静态编译(二)

静态编译有其优势,但有些情况下,静态编译反而会造成一些不便。

工厂类注册问题

工厂类一般使用模板或宏的方式,如下列代码申明:

class ExpressionFactoryRegisterer
{
public:
   ExpressionFactoryRegisterer(const std::string &name, const ExpressionFactoryPtr &factory)
  {
       GetExpressionTable()->RegisterExpressionFactory(name, factory);
  }
};

#define REGISTER_EXPRESSION_CONCAT_IMPL(a, b) a##b
#define REGISTER_EXPRESSION_CONCAT(a, b) REGISTER_EXPRESSION_CONCAT_IMPL(a, b)

#define REGISTER_EXPRESSION_FACTORY(name, factory)                                                       \
   namespace                                                                                           \
   {                                                                                                   \
   BS_NAMESPACE(expression)::ExpressionFactoryRegisterer                                               \
   REGISTER_EXPRESSION_CONCAT(r, __COUNTER__)(name, factory);                                           \
   }

#define REGISTER_EXPRESSION_TYPE(name, type)                                                             \
   namespace                                                                                           \
   {                                                                                                   \
   BS_NAMESPACE(expression)::ExpressionFactoryRegisterer REGISTER_EXPRESSION_CONCAT(r, __COUNTER__)(   \
           name, std::make_shared<BS_NAMESPACE(expression)::TypedExpressionFactory<type>>());           \
   }

代码注册如下:

REGISTER_EXPRESSION_FACTORY("log", std::make_shared<UnaryMathExpressionFactory<Func__log>>());

这样注册之后,一个简单的"反射"机制建立起来。但一般情况下,为了避免多重定义,会降代码注册的部分写到 .cc 文件中,但静态编译的时候, .cc 生成的 .o 文件,会通过 ar 工具打包成 .a 文件,此时 .a 文件直接去链接,静态区过程会滞后,导致 RegisterExpressionFactory 动作变成运行态执行。当程序真正执行时,初始化从静态区获取不到注册的类,导致空结果或指针。

正确的方式有2种:

  • 动态编译:通过 -shared 生成 so 文件,然后链接;

  • .o 直接链接:将生成的 .o 文件直接链接,或通过 ar 工具解压 .a 文件,然后再链接 .o 文件;

这样程序会再链接的时候,将注册内容真正的放入静态区,程序启动时,直接加载到对应变量中,就不会为空啦。

关于静态编译(一)

一般C/C++语言的编译,我还是喜欢使用automake,虽然它缺少通配符的支持,但其支持Makefile的原生语法以及一旦生成了Makefile,在任意有Makefile的目录修改Makefile.am之后都可以直接make;且结合libtool之后,动态库和静态库可以随心编译;这个是cmake不能比的,但cmake也有其优点,比如通配等。

这里不是比较automake和cmake,但在工作当中,当一个三方库(sse_pb)既有动态库,又有静态库时,我们使用-lsse_pb连接时,系统会默认使用动态库,但我们需要优先使用静态库链接时, automake方式比较简单,但对于cmake,这里提供3种办法:

1.使用GCC编译参数

我们可以使用GCC的编译参数-Wl,-Bstatic-Wl,-Bdynamic来实现指定优先静态编译:

find_package(PkgConfig)
pkg_check_modules(brpc REQUIRED IMPORTED_TARGET brpc)
pkg_search_module(sse_pb REQUIRED sse_pb)
add_library(basic_search SHARED)
target_link_libraries(basic_search
  PRIVATE
  basic_search_common
  basic_search_util

...

  PUBLIC
  PkgConfig::brpc
  -Wl,-Bstatic
   ${sse_pb_LIBRARIES}
  -Wl,-Bdynamic
   )

值得注意的是 pkg_check_modules 会将 pc 文件内的链接 -lsse_pb 预先生成 /usr/lib64/libsse_pb.so 形式,导致即使添加GCC参数也不会生效。所以正确的方式是使用 pkg_search_module 来生成cmake的宏变量,一般存在两组值:

  • 用于普通情况, pkg-config 在使用--libs 选项调用时提供的信息,如<XXX> = <prefix>

  • 用于 pkg-config 在额外添加--static 选项调用时提供的信息 (<XXX> = <prefix>_STATIC)

对应的宏变量有下列这些:

<XXX>_FOUND:如果模块存在,则设置为 1
<XXX>_LIBRARIES:只有库 (没有 "-l")
<XXX>_LINK_LIBRARIES:库及其绝对路径
<XXX>_LIBRARY_DIRS:库的路径 (没有 "-L")
<XXX>_LDFLAGS:所有必需的链接器标志
<XXX>_LDFLAGS_OTHER:所有其他链接器标志
<XXX>_INCLUDE_DIRS:预处理器标记'-I'(没有'-I')
<XXX>_CFLAGS:所有必需的 cflags
<XXX>_CFLAGS_OTHER:其他编译器标志
<YYY>_VERSION:模块版本
<YYY>_PREFIX:模块的前缀目录
<YYY>_INCLUDEDIR:包含模块的目录
<YYY>_LIBDIR:模块的 Lib 目录

如上述 ${sse_pb_LIBRARIES} 的设置,就是将 sse_pb 库以静态优先的方式链接起来。

2.使用CMAKE_FIND_LIBRARY_SUFFIXES

我们还可以使用 find_library ,通过改变查找 .a 文件的顺序来获得静态文件优先规则,如下所示:

if (WIN32 OR MSVC)
   set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib")
elseif (UNIX)
   # 仅查找静态库,强制后缀为 .a
   set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")

   # 如果只是优先查找静态库,保证 .a 后缀在前面即可,把默认的后缀加上
   # set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES})
endif()

find_library(sse_pb_lib sse_pb)
target_link_libraries(basic_search
   ${sse_pb_lib}
)

3.写死路径

在这种 set_property 方式下,主要依赖路径:

add_library(sse_pb STATIC IMPORTED)
set_property(TARGET sse_pb PROPERTY IMPORTED_LOCATION /path/to/libsse_pb.a)
target_link_libraries(basic_search sse_pb)

其中 /path/to/libsse_pb.a 就可以随意发挥了。

从上可以看出,最灵活也是最优雅的还是第一种方式,可以依赖pkg-config建立很好的环境变量,从而进行静态编译。

使用debmake创建debian模板时,发现出现如下错误:

FileNotFoundError: [Errno 2] No such file or directory: '//share/debmake/extra0export/compiler'

SeaTalk_IMG_1674898499.png

跟随python堆栈,了解到:

    # get prefix for install --user/ ,, --prefix/ ,, --home
    fullparent = os.path.dirname(sys.argv[0])
    if fullparent == '.':
        para['base_path'] = '..'
    else:
        para['base_path'] = os.path.dirname(fullparent)
  para = debmake.para.para(para)

出现了问题,修复方式也简单,因为base_path判断出现了bug,故强制在文件/usr/lib/python3/dist-packages/debmake/__init__.py中指定

para['base_path'] = '/usr'

搞定。此debmake版本是4.3.1,等官方修复之后再更新上去。

有点郁闷,被同一个石头绊倒了二次,好记性还是不如乱笔头!事情是这样子的......

对官方tensorflow-1.15.5做了移除abseil-cpp之后,发现一些link问题,程序虽然编译和链接时,都没有问题,但是打的RPM包之后,程序运行时,会hang住。

本地正常运行也没有问题,主要体现在最后link的参数上:

 -Wl,-z -Wl,relro -Wl,-z -Wl,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld

如果一旦链接上-specs=/usr/lib/rpm/redhat/redhat-hardened-ld,就表现不正常,此问题已经在红帽子官网报告过。当前笔者使用的是CentOS8,和fedora23相当,所以此处bug仅记录。

解决办法也比较简单,spec文件中加入自定义ldflags即可:

%define build_ldflags  %{nil}

这里为不加。

本着寻根究底的精神,确认link过程中有问题,于是,查询去掉一部分link,发现并不会hang住,罪魁祸首是opennmt-tokenizer,其依赖了sentencepiece,而sentencepiece代码中依赖了protobuf和absl,在0.1.96版本中,由于SPM_USE_EXTERNAL_ABSL写反了(BUG),导致使用了thirdparty目录下的protobuf的源文件和abseil的源文件加入其中一起编译,从而导致了符号冲突。

BTW:二进制运行时提示:

[libprotobuf ERROR google/protobuf/descriptor_database.cc:641] File already exists in database: tensorflow/core/profiler/tfprof_log.proto

有几种情况:

(1)tensorflow的C库和C++库一起链接导致;

(2)其它protobuf的版本内置导致,如(abseil)

对于特定的函数或者变量属性检测一个特定的参数类:

#include <iostream>
#include <boost/smart_ptr.hpp>

#include <type_traits>
using namespace boost;
using namespace std;

#define _CAT(A, B) A##B
#define STR(s) #s
#define CAT(A,B) _CAT(A,B)
#define CHECKER_PREFIX __has_member_function_
#define HAS_MEMBER_FUNC_CHECKER_NAME(FUNC) CAT(CHECKER_PREFIX, FUNC)

#define HAS_MEMBER_FUNC_CHECKER(FUNC) template <typename T> \
struct HAS_MEMBER_FUNC_CHECKER_NAME(FUNC){ \
template <typename C> static std::true_type test(decltype(&C::FUNC)); \
template <typename C> static std::false_type test(...); \
static constexpr bool value = std::is_same<decltype(test<T>(nullptr)),std::true_type>::value; \
};

#define HAS_MEMBER_FUNC(TYPE, FUNC) (HAS_MEMBER_FUNC_CHECKER_NAME(FUNC)<TYPE>::value)


class TestA{
public:
   int* foo(){

       int* a =new int(1);
       return a;

  }

   static int foo2(){
       return  1;
  }

   static const int foo3;
};

HAS_MEMBER_FUNC_CHECKER(foo);
HAS_MEMBER_FUNC_CHECKER(foo2);
HAS_MEMBER_FUNC_CHECKER(foo3);
int main() {

   static_assert(HAS_MEMBER_FUNC(TestA, foo));
   std::cout<<HAS_MEMBER_FUNC(TestA, foo) <<std::endl;

   return 0;
}

对于确定参数类型的泛型成员函数:

#include <iostream>
#include <boost/smart_ptr.hpp>

#include <type_traits>
using namespace boost;
using namespace std;

#define _CAT(A, B) A##B
#define STR(s) #s
#define CAT(A,B) _CAT(A,B)
// 对于固定个数的模版类,检测某一个属性或者函数是否存在
#define CHECKER_PREFIX2 _has_member_fcuntoin2_
#define HAS_MEMBER_FUNC_CHECK_NAME2_(FUNC) CAT(CHECKER_PREFIX2,FUNC)

#define HAS_MEMBER_FUNC_CEHCKER2(FUNC) template<typename T, typename R> \
   struct HAS_MEMBER_FUNC_CHECK_NAME2_(FUNC){ \
   template<typename C> static std::true_type test(decltype(&C::template FUNC<R>)); \
   template<typename C> static std::false_type test(...); \
   static constexpr bool value = std::is_same<decltype(test<T>(nullptr)),std::true_type>::value; \
}

// #define IF_HAS_MEMBER_FUNC2(FUNC,CLASS_,CLASS_F_T) (HAS_MEMBER_FUNC_CEHCK_NAME2_(FUNC)<CLASS_ ,CLASS_F_T>::value)


class TestA{
public:
   int* foo(){

       int* a =new int(1);
       return a;

  }

   static int foo2(){
       return  1;
  }

   template <typename T>
   static  int foo4(){
       return 1;
  }

   static const int foo3;
};


//含有固定类型的模版类
HAS_MEMBER_FUNC_CEHCKER2(foo4);

int main() {
   // 输出 1
   std::cout<< HAS_MEMBER_FUNC_CHECK_NAME2_(foo4)<TestA,char>::value << std::endl;
   // 输出 1
   std::cout<< HAS_MEMBER_FUNC_CHECK_NAME2_(foo4)<TestA,int>::value << std::endl;

   return 0;
}

See also:

https://blog.csdn.net/TH_NUM/article/details/90968219

https://harrychen.xyz/2019/06/04/cpp-17-mock-concept/

关于tensorflow编译链接问题

Tensorflow的编译工程化做得不好,主要表现以下几个方面

  1. 工程的依赖库默认从远程下载,而对于网络条件不好或中国地区用户,无法获得远程的依赖;且在repo.bzl中固化了mirror.tensorflow.org字样的代码,导致无法替换有效的镜像地址;
  2. 工程编译速度非常慢(64Core大于10分钟),由于所有依赖都采用源代码进行编译,虽然编译工具bazel有缓存,但是仍然非常之慢;整个源代码编译在一个库里,好处是对于不同的操作系统,尤其是分布式集群,分发看似方便一些,不需要依赖安装,但同时带来了隐患,依赖缺乏有效管理,导致底层的依赖有问题,需要升级,是很难覆盖,且不知道tensorflow依赖它;
  3. 工程依赖可视化管理比较差,上述已经提到,由于bazel的基因它只是一个编译工具,但它又想把依赖这件事情做好,具备很好的灵活性,有点像node和go库,但对于C/C++工程来说,每个库是远远比node和go库重一些,且很多是多层多次依赖,一般由操作系统来进行包的依赖管理,但bazel想自己做成一个王国,就有点勉强了。

回到tensorflow上来说,很现实的问题,系统的protobuf库使用的是高版本,而tensorflow 1.15.5版本内置的protobuf源码版本很低,如果tensorflow版本默认编译,低版本的protobuf符号会连接到tensorflow的库中,当我们应用需要protobuf高版本以及tensorflow库时,该应用的编译和连接没有问题,但运行时,会出现protobuf符号连接版本问题,那么这个时候需要解决版本冲突问题,当然问题不仅仅只有protobuf库,其他的库也是一样;故需要去指定系统环境的依赖库达到目的,由于bazel只有基本语法规范,但重要的还是tensorflow本身自己的编译工程需要去解决。
前面提到了tensorflow工程上的一些问题,对于系统提供的库,我们需要额外的cc_library和new_local_repository来代替new_http_archive,幸运的是,tensorflow考虑到了本地系统的依赖库,其配置在third_party/systemlibs中,syslibs_configure.bzl文件中有详细的依赖说明,故根据源文件查到环境变量TF_SYSTEM_LIBS来控制那些第三方依赖使用系统的库,故编译选项如下:

bazel build --action_env TF_SYSTEM_LIBS='boringssl,com_google_protobuf,grpc,zlib_archive,pcre,jsoncpp_git,curl,com_googlesource_code_re2,snappy,hwloc,lmdb,org_sqlite' --copt=-mavx2 --copt=-mfma --copt=-mavx512f //tensorflow/tools/lib_package:libtensorflow

即解决依赖库用系统已经编译好的库的问题

gcc11.2 与 c++11 的一些编译问题

gcc这几年发展很快,C++20标准之后,gcc版本也随之到了GCC11了,老的代码还是在C++11的标准上,使用-std=c++11来进行编译。

GRPC一个退出发生Core的问题

使用google grpc库的时候,发现退出的时候,老出现

E0926 14:45:25.194821726 255651 completion_queue.cc:247] assertion failed: queue.num_items() == 0
已放弃 (核心已转储)

其解决办法是,在编译链接选项上加入

-lgrpc++_reflection

即可避免

突然发现gcc 7出了

在fedora26上,发现gcc版本已经升级到gcc7了,gcc更新节奏越来越快了,更多的特性涌出。

我们现在还在gcc4.1.2版本上,应该需要跨一个时代了。:)

Monthly Archives

Pages

Powered by Movable Type 7.9.4

About this Archive

This page is an archive of recent entries in the C/C++ category.

Linux世界 is the next category.

Find recent content on the main index or look in the archives to find all content.